From c531a075a428e02623a972b6feee9d0ed9ed9f55 Mon Sep 17 00:00:00 2001 From: Sebastien Marchand Date: Wed, 29 Jul 2026 17:26:18 -0400 Subject: [PATCH 1/2] [AArch64][Windows] Fix swift async context slot placement On Windows, assignCalleeSavedSpillSlots allocated the swift async context object as the first callee-save frame index, before the loop that creates the register slots. Every other target creates it inside that loop, immediately below the FP slot. The Windows placement left MachineFrameInfo believing the slot sits above the frame record, while computeCalleeSaveRegisterPairs reserves an expanded 24-byte slot and makes the prologue store it below the record at FP-8. The two views disagreed by eight bytes, which left an unclaimed range in the middle of what MachineFrameInfo considered the callee-save block. PrologEpilogInserter's stack slot scavenger then handed that range to a live local, placing it on the saved caller frame pointer: the function stored through [x29], reloaded it, and the epilogue restored the corrupted value before returning. Scavenging is gated on the optimization level, so this only reproduced at -O2 and above. Create the object inside the callee-save loop so both views agree. The CSI array is only reversed on the needsWinCFI path while the async block was keyed on isTargetWindows, and those differ for a nounwind funclet; inserting the object between the two members of the FP/LR pair would violate the frame index adjacency that computeCalleeSaveRegisterPairs asserts, so restrict the in-loop creation to the reversed case. The remaining path is unaffected because there the alignment gap is created by setObjectAlignment on a callee-save object, making it padding rather than a range the scavenger can claim. The two updated tests pinned literal offsets; both still hold their stated invariants, and the frame in the slot-offset test shrinks from 64 to 48 bytes now that the hole no longer needs padding. --- .../Target/AArch64/AArch64FrameLowering.cpp | 10 +++-- .../swift-async-context-frame-record-win.ll | 37 +++++++++++++++++++ .../AArch64/swift-async-context-seh.ll | 2 +- .../swift-async-context-slot-offset-win.ll | 17 +++++---- 4 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 llvm/test/CodeGen/AArch64/swift-async-context-frame-record-win.ll diff --git a/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp b/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp index de52122ab9c82..f9c8c785ca2e9 100644 --- a/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp +++ b/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp @@ -4479,7 +4479,11 @@ bool AArch64FrameLowering::assignCalleeSavedSpillSlots( auto *AFI = MF.getInfo(); bool UsesWinAAPCS = isTargetWindows(MF); - if (UsesWinAAPCS && hasFP(MF) && AFI->hasSwiftAsyncContext()) { + // Only the WinCFI path reverses CSI above, which is what places the frame + // record at the top of the callee-save area and lets the async context slot + // be allocated directly below FP inside the loop. Without that reversal the + // slot has to be carved out up front. + if (UsesWinAAPCS && !NeedsWinCFI && hasFP(MF) && AFI->hasSwiftAsyncContext()) { int FrameIdx = MFI.CreateStackObject(8, Align(16), true); AFI->setSwiftAsyncContextFrameIdx(FrameIdx); if ((unsigned)FrameIdx < MinCSFrameIndex) @@ -4548,8 +4552,8 @@ bool AArch64FrameLowering::assignCalleeSavedSpillSlots( MaxCSFrameIndex = FrameIdx; // Grab 8 bytes below FP for the extended asynchronous frame info. - if (hasFP(MF) && AFI->hasSwiftAsyncContext() && !UsesWinAAPCS && - Reg == AArch64::FP) { + if (hasFP(MF) && AFI->hasSwiftAsyncContext() && + (!UsesWinAAPCS || NeedsWinCFI) && Reg == AArch64::FP) { FrameIdx = MFI.CreateStackObject(8, Alignment, true); AFI->setSwiftAsyncContextFrameIdx(FrameIdx); if ((unsigned)FrameIdx < MinCSFrameIndex) diff --git a/llvm/test/CodeGen/AArch64/swift-async-context-frame-record-win.ll b/llvm/test/CodeGen/AArch64/swift-async-context-frame-record-win.ll new file mode 100644 index 0000000000000..9ef516d812cf4 --- /dev/null +++ b/llvm/test/CodeGen/AArch64/swift-async-context-frame-record-win.ll @@ -0,0 +1,37 @@ +; RUN: llc -mtriple aarch64-unknown-windows-msvc -O2 %s -o - | FileCheck %s + +; The swift async context slot is allocated directly below the frame record. If +; it is instead allocated above it, MachineFrameInfo's view of the callee-save +; area disagrees with the prologue by 8 bytes, leaving a hole that PEI's stack +; slot scavenger hands to a live local -- landing it on the saved caller x29. +; +; Check that x29 is only ever used as a base for loads here, never stored +; through at the frame record itself. + +; CHECK-LABEL: test: +; CHECK: add x29, sp, #[[FPOFF:[0-9]+]] +; CHECK-NOT: str {{[wx][0-9]+}}, [x29] +; CHECK-NOT: str {{[wx][0-9]+}}, [x29, #8] +; CHECK-NOT: stur {{[wx][0-9]+}}, [x29] +; CHECK-NOT: stp {{[wx][0-9]+}}, {{[wx][0-9]+}}, [x29] +; CHECK: ldp x29, x30, [sp, #[[FPOFF]]] + +declare ptr @llvm.swift.async.context.addr() nounwind +declare swiftcc void @swift_task_dealloc() + +define swifttailcc void @test(ptr %0, ptr %1, ptr %2, ptr %3, ptr %4, ptr %5, + ptr %6, ptr %7, ptr %8, ptr %9) { +entryresume.0: + %10 = tail call ptr @llvm.swift.async.context.addr() + %.reload71 = load ptr, ptr null, align 8 + call swiftcc void @swift_task_dealloc() + %Destroy24 = load ptr, ptr %0, align 8 + tail call void %Destroy24(ptr %.reload71, ptr %9) + %Destroy25 = load ptr, ptr %6, align 8 + tail call void %Destroy25(ptr %3, ptr null) + %Destroy26 = load ptr, ptr %8, align 8 + tail call void %Destroy26(ptr %2, ptr %7) + %Destroy27 = load ptr, ptr %4, align 8 + tail call void %Destroy27(ptr %1, ptr %5) + ret void +} diff --git a/llvm/test/CodeGen/AArch64/swift-async-context-seh.ll b/llvm/test/CodeGen/AArch64/swift-async-context-seh.ll index 852c97743a10c..0d1cd593f4729 100644 --- a/llvm/test/CodeGen/AArch64/swift-async-context-seh.ll +++ b/llvm/test/CodeGen/AArch64/swift-async-context-seh.ll @@ -8,7 +8,7 @@ ; CHECK: orr x29, x29, #0x1000000000000000 ; CHECK-NEXT: .seh_nop -; CHECK: str x22, [sp, #16] +; CHECK: str x22, [sp] ; CHECK-NEXT: .seh_nop ; CHECK: and x29, x29, #0xefffffffffffffff ; CHECK-NEXT: .seh_nop diff --git a/llvm/test/CodeGen/AArch64/swift-async-context-slot-offset-win.ll b/llvm/test/CodeGen/AArch64/swift-async-context-slot-offset-win.ll index 86e459a4af717..4e9d0d6f65d6b 100644 --- a/llvm/test/CodeGen/AArch64/swift-async-context-slot-offset-win.ll +++ b/llvm/test/CodeGen/AArch64/swift-async-context-slot-offset-win.ll @@ -5,13 +5,16 @@ ; saving it won't overwrite the saved value of the callee-saved ; register. ; -; CHECK: sub sp, sp, #64 -; CHECK: str x19, [sp, #16] -; CHECK: str x21, [sp, #24] -; CHECK-NOT: stp x29, x30, [sp, #32] -; CHECK: stp x29, x30, [sp, #40] -; CHECK-NOT: str x22, [sp, #24] -; CHECK: str x22, [sp, #32] +; The async context slot sits directly below the frame record, so the callee +; saves below it stay clear of both. +; +; CHECK: str x19, [sp, #-48]! +; CHECK: str x21, [sp, #8] +; CHECK-NOT: stp x29, x30, [sp, #16] +; CHECK: stp x29, x30, [sp, #24] +; CHECK-NOT: str x22, [sp, #8] +; CHECK: str x22, [sp, #16] +; CHECK: add x29, sp, #24 declare ptr @llvm.swift.async.context.addr() declare swiftcc i64 @foo(i64 %0, i64 %1) From 97876efc94e8ff071d8f2ffdee9ce165b07c099d Mon Sep 17 00:00:00 2001 From: Sebastien Marchand Date: Thu, 30 Jul 2026 13:47:23 -0400 Subject: [PATCH 2/2] Use autogenerated assertions for the frame record test CHECK-NOT passes silently if the store comes back in a form the pattern does not match, such as a paired store or a different register, which is the regression this test exists to catch. Generating the assertions locks the whole frame layout instead, so any object placed at or above the frame record shows up as a diff. This matches the test in the upstream version of the fix. --- .../swift-async-context-frame-record-win.ll | 116 ++++++++++++++---- 1 file changed, 91 insertions(+), 25 deletions(-) diff --git a/llvm/test/CodeGen/AArch64/swift-async-context-frame-record-win.ll b/llvm/test/CodeGen/AArch64/swift-async-context-frame-record-win.ll index 9ef516d812cf4..9b673f013c135 100644 --- a/llvm/test/CodeGen/AArch64/swift-async-context-frame-record-win.ll +++ b/llvm/test/CodeGen/AArch64/swift-async-context-frame-record-win.ll @@ -1,37 +1,103 @@ -; RUN: llc -mtriple aarch64-unknown-windows-msvc -O2 %s -o - | FileCheck %s +; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 5 +; RUN: llc -mtriple=aarch64-unknown-windows-msvc -O2 < %s | FileCheck %s ; The swift async context slot is allocated directly below the frame record. If -; it is instead allocated above it, MachineFrameInfo's view of the callee-save +; it is allocated above it instead, MachineFrameInfo's view of the callee-save ; area disagrees with the prologue by 8 bytes, leaving a hole that PEI's stack -; slot scavenger hands to a live local -- landing it on the saved caller x29. +; slot scavenger hands to a live local, which then shares an address with the +; saved caller x29. ; -; Check that x29 is only ever used as a base for loads here, never stored -; through at the frame record itself. - -; CHECK-LABEL: test: -; CHECK: add x29, sp, #[[FPOFF:[0-9]+]] -; CHECK-NOT: str {{[wx][0-9]+}}, [x29] -; CHECK-NOT: str {{[wx][0-9]+}}, [x29, #8] -; CHECK-NOT: stur {{[wx][0-9]+}}, [x29] -; CHECK-NOT: stp {{[wx][0-9]+}}, {{[wx][0-9]+}}, [x29] -; CHECK: ldp x29, x30, [sp, #[[FPOFF]]] +; The full frame layout is checked so that any object placed at or above the +; frame record shows up here as a diff: x29 is saved at sp+88, the async context +; sits below it at sp+80, and the spills land at sp+8 and sp+104. declare ptr @llvm.swift.async.context.addr() nounwind declare swiftcc void @swift_task_dealloc() -define swifttailcc void @test(ptr %0, ptr %1, ptr %2, ptr %3, ptr %4, ptr %5, - ptr %6, ptr %7, ptr %8, ptr %9) { +define swifttailcc void @test(ptr %ctx, ptr %vw0, ptr %vw1, ptr %vw2, ptr %vw3, ptr %obj0, ptr %obj1, ptr %obj2, ptr %obj3, ptr %obj4) { +; CHECK-LABEL: test: +; CHECK: .seh_proc test +; CHECK-NEXT: // %bb.0: // %entryresume.0 +; CHECK-NEXT: orr x29, x29, #0x1000000000000000 +; CHECK-NEXT: .seh_nop +; CHECK-NEXT: sub sp, sp, #112 +; CHECK-NEXT: .seh_stackalloc 112 +; CHECK-NEXT: str x19, [sp, #16] // 8-byte Folded Spill +; CHECK-NEXT: .seh_save_reg x19, 16 +; CHECK-NEXT: str x21, [sp, #24] // 8-byte Folded Spill +; CHECK-NEXT: .seh_save_reg x21, 24 +; CHECK-NEXT: stp x23, x24, [sp, #32] // 16-byte Folded Spill +; CHECK-NEXT: .seh_save_regp x23, 32 +; CHECK-NEXT: stp x25, x26, [sp, #48] // 16-byte Folded Spill +; CHECK-NEXT: .seh_save_regp x25, 48 +; CHECK-NEXT: stp x27, x28, [sp, #64] // 16-byte Folded Spill +; CHECK-NEXT: .seh_save_regp x27, 64 +; CHECK-NEXT: stp x29, x30, [sp, #88] // 16-byte Folded Spill +; CHECK-NEXT: .seh_save_fplr 88 +; CHECK-NEXT: str xzr, [sp, #80] +; CHECK-NEXT: .seh_nop +; CHECK-NEXT: add x29, sp, #88 +; CHECK-NEXT: .seh_add_fp 88 +; CHECK-NEXT: .seh_endprologue +; CHECK-NEXT: str x7, [sp, #8] // 8-byte Folded Spill +; CHECK-NEXT: mov x21, xzr +; CHECK-NEXT: mov x22, x6 +; CHECK-NEXT: ldp x20, x27, [x29, #24] +; CHECK-NEXT: ldr x28, [x21] +; CHECK-NEXT: str x5, [x29, #16] // 8-byte Folded Spill +; CHECK-NEXT: mov x23, x4 +; CHECK-NEXT: mov x24, x3 +; CHECK-NEXT: mov x25, x2 +; CHECK-NEXT: mov x26, x1 +; CHECK-NEXT: mov x19, x0 +; CHECK-NEXT: bl swift_task_dealloc +; CHECK-NEXT: ldr x8, [x19] +; CHECK-NEXT: mov x0, x28 +; CHECK-NEXT: mov x1, x27 +; CHECK-NEXT: blr x8 +; CHECK-NEXT: ldr x8, [x22] +; CHECK-NEXT: mov x0, x24 +; CHECK-NEXT: mov x1, xzr +; CHECK-NEXT: blr x8 +; CHECK-NEXT: ldr x8, [x20] +; CHECK-NEXT: ldr x1, [sp, #8] // 8-byte Folded Reload +; CHECK-NEXT: mov x0, x25 +; CHECK-NEXT: blr x8 +; CHECK-NEXT: ldr x2, [x23] +; CHECK-NEXT: ldr x1, [x29, #16] // 8-byte Folded Reload +; CHECK-NEXT: mov x0, x26 +; CHECK-NEXT: .seh_startepilogue +; CHECK-NEXT: ldp x29, x30, [sp, #88] // 16-byte Folded Reload +; CHECK-NEXT: .seh_save_fplr 88 +; CHECK-NEXT: ldp x27, x28, [sp, #64] // 16-byte Folded Reload +; CHECK-NEXT: .seh_save_regp x27, 64 +; CHECK-NEXT: ldp x25, x26, [sp, #48] // 16-byte Folded Reload +; CHECK-NEXT: .seh_save_regp x25, 48 +; CHECK-NEXT: ldp x23, x24, [sp, #32] // 16-byte Folded Reload +; CHECK-NEXT: .seh_save_regp x23, 32 +; CHECK-NEXT: ldr x21, [sp, #24] // 8-byte Folded Reload +; CHECK-NEXT: .seh_save_reg x21, 24 +; CHECK-NEXT: ldr x19, [sp, #16] // 8-byte Folded Reload +; CHECK-NEXT: .seh_save_reg x19, 16 +; CHECK-NEXT: and x29, x29, #0xefffffffffffffff +; CHECK-NEXT: .seh_nop +; CHECK-NEXT: add sp, sp, #112 +; CHECK-NEXT: .seh_stackalloc 112 +; CHECK-NEXT: .seh_endepilogue +; CHECK-NEXT: br x2 +; CHECK-NEXT: .seh_endfunclet +; CHECK-NEXT: .seh_endproc entryresume.0: - %10 = tail call ptr @llvm.swift.async.context.addr() - %.reload71 = load ptr, ptr null, align 8 + %ctxaddr = tail call ptr @llvm.swift.async.context.addr() + %reloaded = load ptr, ptr null, align 8 call swiftcc void @swift_task_dealloc() - %Destroy24 = load ptr, ptr %0, align 8 - tail call void %Destroy24(ptr %.reload71, ptr %9) - %Destroy25 = load ptr, ptr %6, align 8 - tail call void %Destroy25(ptr %3, ptr null) - %Destroy26 = load ptr, ptr %8, align 8 - tail call void %Destroy26(ptr %2, ptr %7) - %Destroy27 = load ptr, ptr %4, align 8 - tail call void %Destroy27(ptr %1, ptr %5) + %destroy0 = load ptr, ptr %ctx, align 8 + tail call void %destroy0(ptr %reloaded, ptr %obj4) + %destroy1 = load ptr, ptr %obj1, align 8 + tail call void %destroy1(ptr %vw2, ptr null) + %destroy2 = load ptr, ptr %obj3, align 8 + tail call void %destroy2(ptr %vw1, ptr %obj2) + %destroy3 = load ptr, ptr %vw3, align 8 + tail call void %destroy3(ptr %vw0, ptr %obj0) ret void }