Skip to content

[AArch64][Windows] Fix swift async context slot placement - #13571

Draft
sebmarchand wants to merge 2 commits into
swiftlang:stable/21.xfrom
sebmarchand:fix-swiftasync-frame-record-21x
Draft

[AArch64][Windows] Fix swift async context slot placement#13571
sebmarchand wants to merge 2 commits into
swiftlang:stable/21.xfrom
sebmarchand:fix-swiftasync-frame-record-21x

Conversation

@sebmarchand

@sebmarchand sebmarchand commented Jul 29, 2026

Copy link
Copy Markdown

This is the Windows ARM64 miscompile from swiftlang/swift#90920, where an async continuation returns and the caller carries on with a garbage frame pointer.

On Windows assignCalleeSavedSpillSlots creates the swift async context object before the callee-save loop instead of inside it next to the FP slot. MachineFrameInfo then records it above the frame record while the prologue stores it below at FP-8, and that 8 byte disagreement leaves a hole inside what MachineFrameInfo thinks is the callee-save area. PEI's stack slot scavenger hands the hole to a live local, which ends up with the address of the saved caller x29. Only reproduces at -O2 and above because scavenging is gated on the optimization level.

This creates the object inside the loop on the needsWinCFI path so the two agree there. It is narrower than what I put up upstream (llvm#212922), which drops the special case entirely, because only that path reverses the CSI array and keying on FP without the reversal would insert the object between the FP/LR pair.

The other path still has the same problem, exposing the saved x19 instead of the frame record. Covering both would need f406210 backported, and that touches AArch64PrologueEpilogue.cpp which does not exist on this branch. Given stable/* is supposed to take cherry-picks rather than original fixes, this probably should not land as its own patch and is better held until the upstream change is in and then adapted. Happy to close it if that is the preference.

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.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant