Repository navigation
Test stackman_call's return value and previous stack pointer. - #21
Conversation
test_04 and test_05 now check that the callback result comes back unchanged, that a caller local survives writes on the alternate stack, and that the pointer argument is the previous stack. The audit note records the register findings these checks come from. qemu-riscv64 is expected to fail until switch_riscv64_gcc.S skips a null stack and passes the previous stack pointer.
A null stack was installed as sp, which faulted test_05, and the callback received that new pointer instead of the previous one, which failed test_04. Skip a zero stack and pass the saved stack pointer.
x18 is the TEB for the whole time user code runs. stackman_switch now keeps the callback in x20, and stackman_call keeps the old stack pointer in x19. Both routines still spill and restore x18 so a switched context gets the value it saved. SEH unwind codes are unchanged.
stackman_switch now applies the stack delta to x29 before the restore callback, in the checked-in assembly and in the inline sequence that generates it. The epilogue still reloads the saved x29. SME lazy save is left for a target that enables SME.
The control bits of MXCSR are non-volatile. stackman_switch stores the register in the unused 8-byte slot at the top of the xmm frame and loads it back before releasing that frame, including after the stack has moved. stackman_call returns to the same caller and does not spill it.
SysV stores it with fstcw at the pointer passed to the callback, in both the checked-in assembly and the inline block it is copied from. Windows does the same inside stackman_switch_raw, next to the callee-saved pushes, and the C wrapper still restores the SEH chain. XMM registers stay unsaved.
The checked-in assembly is soft-float and does not spill d8-d15, so a __ARM_PCS_VFP include is now a compile error. stackman_switch applies the stack delta to r11 before the restore callback. The RISC-V s0 delta stays omitted: s0 is not a frame chain there, and stackman_call uses it for the previous stack pointer.
CHANGELOG 1.0.0 already dropped win_arm. The sources were still selected for _M_ARM, and the VS projects still built them. That assembly pushed an odd number of registers and then vpush'd at 4 mod 8. Delete the sources, the platform branch, the Debug|ARM project configurations, and the prebuilt lib/win_arm archive. Windows ARM64 is unchanged.
The Copilot file still described version 1.0.1, seven ABIs, and six tests, and told agents to trust it ahead of the tree. AGENTS.md points at the current sources instead. The Copilot path is a short pointer so that file does not drift again.
| __asm__ ( | ||
| "sub r1, sp, %[result]\n\t" | ||
| "mov sp, %[result]\n\t" | ||
| "sub r11, r11, r1" |
There was a problem hiding this comment.
[bug] sub r11, r11, r1 is not attached to a live frame pointer. stackman_switch is built with optimize("O", "omit-frame-pointer") (line 75), r11 is an ordinary member of NV_REGISTERS, and the checked-in prologue in switch_arm_gcc.S is frame_needed = 0: it pushes fp but never writes it. Unlike switch_aarch64_gcc.S, where mov x29, sp makes the delta land on this frame, the live r11 here is still whatever the caller left in it (or whatever GCC parked there). The asm clobbers only r1 and the comment forbids listing r11, so a STACKMAN_INLINE_ASM build (tests/test_static.c forces that and make test runs it) may keep the callback or the new stack pointer in r11; a non-zero delta then corrupts it before STACKMAN_OP_RESTORE. The frozen .S currently happens to use r4/r5 instead, but sub r11, r11, r1 there still only perturbs the incoming register across the second blx. pop {..., fp, pc} reloads the unadjusted saved value, so the caller never observes the delta. AGENTS.md repeats the same "do not clobber r11" rule.
Suggestion: Drop the r11 adjust from this header and from switch_arm_gcc.S (the integer spill is already complete). Do not copy the AArch64 sequence here unless the function is changed to no-omit-frame-pointer, r11 is removed from NV_REGISTERS, and the prologue actually sets r11 to this frame the way x29 is set. Leave AArch64 as it is.
| * ebp, ebx, esi, edi are callee-preserved | ||
| * ebp, ebx, esi, edi and the x87 control word are callee-preserved. | ||
| * The control word is stored at the stack pointer passed to the callback | ||
| * so a copied stack image carries it. fstcw does not wait on pending |
There was a problem hiding this comment.
[suggestion] The new comment says fstcw does not wait on pending exceptions. It does the opposite. FSTCW is FWAIT plus FNSTCW and handles pending unmasked x87 exceptions before the store; only FNSTCW skips that check. The mnemonic does match switch_x86_64_gcc.S, so the "matching" half of the sentence is right and the spill placement at 0x10(%esp) / [esp] is the right slot for the copied image. The exception claim is not.
Suggestion: Delete the "does not wait" sentence, or switch both i386 sites and the SysV x86-64 file to fnstcw if the intent was to avoid an FWAIT in the middle of the switch. Do not leave the comment describing the wrong instruction.
stackman_switch is built with omit-frame-pointer. The prologue pushes fp and never sets it, so live r11 is not this frame, and the epilogue pops the saved value. The inline build can keep the callback or the new stack pointer in r11, and the delta then corrupts it before the restore callback. AArch64 still adjusts x29, which the prologue sets from sp.
fstcw is FWAIT plus FNSTCW, so it does wait on a pending unmasked x87 exception. The spill stays fstcw, matching switch_x86_64_gcc.S, which delivers that exception on this stack before the switch.
test_04 and test_05 now check that the callback result comes back unchanged, that a caller local survives writes on the alternate stack, and that the pointer argument is the previous stack. The audit note records the register findings these checks come from.
qemu-riscv64 is expected to fail until switch_riscv64_gcc.S skips a null stack and passes the previous stack pointer.