[llvm] [AArch64][Windows] Fix swift async context slot placement (PR #212922)
Sébastien Marchand via llvm-commits
llvm-commits at lists.llvm.org
Thu Jul 30 07:34:36 PDT 2026
https://github.com/sebmarchand updated https://github.com/llvm/llvm-project/pull/212922
>From 9ff2ebe514cdd4a35cd1c4f09c00039429633761 Mon Sep 17 00:00:00 2001
From: Sebastien Marchand <seb at thebrowser.company>
Date: Wed, 29 Jul 2026 23:27:47 -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, leaving 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], read it back, and the epilogue restored the
corrupted value before returning. Scavenging is gated on the
optimization level, which is why this only reproduced at -O2 and above.
Create the object inside the callee-save loop on Windows too, so that
both views agree. Keying on FP stays correct for the frame index
adjacency computeCalleeSaveRegisterPairs asserts on: CSR_Win_AArch64_AAPCS
lists FP before LR and the array is reversed for Windows, while
CSR_AArch64_AAPCS lists LR before FP and is not reversed, so FP is the
second-created member of the frame record either way and the new object
never lands between the pair.
Frames shrink where the hole previously forced padding, which is what the
test updates reflect. swift-async-win.ll and win-sve.ll are regenerated;
the two hand-written tests keep their stated invariants.
This is the root cause of a miscompile reported downstream as
https://github.com/swiftlang/swift/issues/90920, where large Swift async
funclets on aarch64-unknown-windows-msvc returned to their caller with a
garbage frame pointer.
---
.../Target/AArch64/AArch64FrameLowering.cpp | 9 +----
.../swift-async-context-frame-record-win.ll | 37 +++++++++++++++++++
.../AArch64/swift-async-context-seh.ll | 2 +-
.../swift-async-context-slot-offset-win.ll | 17 +++++----
llvm/test/CodeGen/AArch64/swift-async-win.ll | 14 +++----
llvm/test/CodeGen/AArch64/win-sve.ll | 3 --
6 files changed, 55 insertions(+), 27 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 6ef64b06932ab..5e9c22501f066 100644
--- a/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
+++ b/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
@@ -2815,12 +2815,6 @@ bool AArch64FrameLowering::assignCalleeSavedSpillSlots(
MachineFrameInfo &MFI = MF.getFrameInfo();
auto *AFI = MF.getInfo<AArch64FunctionInfo>();
- if (IsWindows && hasFP(MF) && AFI->hasSwiftAsyncContext()) {
- int FrameIdx = MFI.CreateStackObject(8, Align(16), true);
- AFI->setSwiftAsyncContextFrameIdx(FrameIdx);
- MFI.setIsCalleeSavedObjectIndex(FrameIdx, true);
- }
-
// Insert VG into the list of CSRs, immediately before LR if saved.
if (requiresSaveVG(MF)) {
CalleeSavedInfo VGInfo(AArch64::VG);
@@ -2858,8 +2852,7 @@ bool AArch64FrameLowering::assignCalleeSavedSpillSlots(
MFI.setIsCalleeSavedObjectIndex(FrameIdx, true);
// Grab 8 bytes below FP for the extended asynchronous frame info.
- if (hasFP(MF) && AFI->hasSwiftAsyncContext() && !IsWindows &&
- Reg == AArch64::FP) {
+ if (hasFP(MF) && AFI->hasSwiftAsyncContext() && Reg == AArch64::FP) {
FrameIdx = MFI.CreateStackObject(8, Alignment, true);
AFI->setSwiftAsyncContextFrameIdx(FrameIdx);
MFI.setIsCalleeSavedObjectIndex(FrameIdx, true);
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)
diff --git a/llvm/test/CodeGen/AArch64/swift-async-win.ll b/llvm/test/CodeGen/AArch64/swift-async-win.ll
index c74e9667f9c75..b6035377d8149 100644
--- a/llvm/test/CodeGen/AArch64/swift-async-win.ll
+++ b/llvm/test/CodeGen/AArch64/swift-async-win.ll
@@ -18,12 +18,11 @@ declare void @llvm.lifetime.end.p0(i64 immarg, ptr nocapture) #0
define hidden swifttailcc void @"$ss23withCheckedContinuation8function_xSS_yScCyxs5NeverOGXEtYalFTQ0_"(ptr nocapture readonly %0) #1 {
; CHECK-LABEL: $ss23withCheckedContinuation8function_xSS_yScCyxs5NeverOGXEtYalFTQ0_:
; CHECK: // %bb.0: // %entryresume.0
-; CHECK-NEXT: sub sp, sp, #48
-; CHECK-NEXT: str x19, [sp, #16] // 8-byte Spill
+; CHECK-NEXT: str x19, [sp, #-32]! // 8-byte Folded Spill
+; CHECK-NEXT: stp x29, x30, [sp, #16] // 16-byte Folded Spill
+; CHECK-NEXT: add x29, sp, #16
; CHECK-NEXT: adrp x19, __imp_swift_task_dealloc
-; CHECK-NEXT: stp x29, x30, [sp, #32] // 16-byte Folded Spill
-; CHECK-NEXT: add x29, sp, #32
-; CHECK-NEXT: str xzr, [sp, #24]
+; CHECK-NEXT: str xzr, [sp, #8]
; CHECK-NEXT: ldr x8, [x0]
; CHECK-NEXT: stur x8, [x29, #-8]
; CHECK-NEXT: ldr x20, [x0]
@@ -33,11 +32,10 @@ define hidden swifttailcc void @"$ss23withCheckedContinuation8function_xSS_yScCy
; CHECK-NEXT: blr x19
; CHECK-NEXT: mov x0, x22
; CHECK-NEXT: blr x19
-; CHECK-NEXT: ldp x29, x30, [sp, #32] // 16-byte Folded Reload
+; CHECK-NEXT: ldp x29, x30, [sp, #16] // 16-byte Folded Reload
; CHECK-NEXT: mov x0, x20
; CHECK-NEXT: ldr x1, [x20, #8]
-; CHECK-NEXT: ldr x19, [sp, #16] // 8-byte Reload
-; CHECK-NEXT: add sp, sp, #48
+; CHECK-NEXT: ldr x19, [sp], #32 // 8-byte Folded Reload
; CHECK-NEXT: br x1
entryresume.0:
%1 = load ptr, ptr %0, align 8
diff --git a/llvm/test/CodeGen/AArch64/win-sve.ll b/llvm/test/CodeGen/AArch64/win-sve.ll
index dea7781ba16e2..797c4de27704b 100644
--- a/llvm/test/CodeGen/AArch64/win-sve.ll
+++ b/llvm/test/CodeGen/AArch64/win-sve.ll
@@ -1582,14 +1582,11 @@ define void @f16(ptr swiftasync %ctx, <vscale x 2 x i64> %foo) {
; CHECK-NEXT: add x29, sp, #8
; CHECK-NEXT: .seh_add_fp 8
; CHECK-NEXT: .seh_endprologue
-; CHECK-NEXT: sub sp, sp, #16
; CHECK-NEXT: //APP
; CHECK-NEXT: //NO_APP
; CHECK-NEXT: ldr x8, [x22]
; CHECK-NEXT: stur x8, [x29, #-8]
; CHECK-NEXT: .seh_startepilogue
-; CHECK-NEXT: add sp, sp, #16
-; CHECK-NEXT: .seh_stackalloc 16
; CHECK-NEXT: ldp x29, x30, [sp, #8] // 16-byte Folded Reload
; CHECK-NEXT: .seh_save_fplr 8
; CHECK-NEXT: add sp, sp, #32
>From 97904aefb0a679c510a4aa9f63976b905c101a32 Mon Sep 17 00:00:00 2001
From: Sebastien Marchand <seb at thebrowser.company>
Date: Thu, 30 Jul 2026 10:16:54 -0400
Subject: [PATCH 2/2] Use autogenerated assertions for the new frame record
test
CHECK-NOT assertions pass silently if the store reappears 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 with update_llc_test_checks.py locks the whole frame layout
instead, so any object placed at or above the frame record shows up as a
diff, and it follows the guidance in TestingGuide to prefer generated
assertions where they are feasible.
---
.../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..8e3a297dbf919 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 6
+; 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 Spill
+; CHECK-NEXT: .seh_save_reg x19, 16
+; CHECK-NEXT: str x21, [sp, #24] // 8-byte 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 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 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 Reload
+; CHECK-NEXT: mov x0, x25
+; CHECK-NEXT: blr x8
+; CHECK-NEXT: ldr x2, [x23]
+; CHECK-NEXT: ldr x1, [x29, #16] // 8-byte 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 Reload
+; CHECK-NEXT: .seh_save_reg x21, 24
+; CHECK-NEXT: ldr x19, [sp, #16] // 8-byte 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
}
More information about the llvm-commits
mailing list