Skip to content

Windows ARM64 (PE) ontop_fcontext: epilog deallocates 0xc0 of the 0xd0 bytes the prolog allocates — 16-byte SP skew, breaks /GS and SEH-based unwind on resume #336

Description

@FaithfulAudio

Summary

Both PE-target ARM64 assembly implementations of ontop_fcontext
(src/asm/ontop_arm64_aapcs_pe_armasm.asm, MSVC ARMASM64 syntax, and
src/asm/ontop_arm64_aapcs_pe_armclang.S, Clang/GAS syntax) have an asymmetric
prolog/epilog: the prolog reserves 0xd0 (208) bytes of stack, but the epilog releases
only 0xc0 (192) before tail-jumping into the ontop-function. The missing 0x10 (16)
bytes are never reclaimed, so sp sits 16 bytes below where it should be for the
remainder of the resumed context's execution.

Confirmed present, byte-for-byte, on develop @ d2142b6925… (current HEAD as of this
report). Confirmed not present in either of the two structurally analogous,
already-correct siblings on the same target:

  • src/asm/jump_arm64_aapcs_pe_armasm.asm — symmetric, 0xd0/0xd0.
  • src/asm/ontop_arm64_aapcs_elf_gas.S (the ELF/GAS target's ontop_fcontext) —
    symmetric, 0xb0/0xb0 (smaller frame: no Windows TEB/TIB save/restore, but internally
    consistent).

This is a structural, input-independent skew — every invocation of the ontop path on
Windows ARM64 hits it, not a data-dependent corruption.

Where it bites: /GS and SEH-based unwind, downstream

ontop_fcontext is reached via a tail-jump (ret x2, not a call), and the
ontop-function it jumps to eventually returns via the target context's own restored
LR, back into that target's original suspended call site — a site that expects sp
restored to its pre-suspend value, exactly as the symmetric jump_fcontext epilog would
leave it. The asymmetric ontop_fcontext epilog leaves sp 16 bytes below that value
instead, for the remaining lifetime of the resumed frame.

Any caller built with /GS (MSVC, on by default) or -fstack-protector-all (Clang) that
stores a stack cookie in a frame which gets resumed via this path will have its
__security_check_cookie read the wrong stack slot on that frame's own exit, and fail
fast with STATUS_STACK_BUFFER_OVERRUN (0xc0000409) — a __fastfail, not a catchable
SEH/C++ exception. A related failure mode hits boost::context::fiber's
forced_unwind-driven teardown (which also uses the ontop path to inject an unwind
marker into the target context): Windows' _CxxFrameHandler3 walks frames using static
.pdata/.xdata unwind metadata that encodes each frame's expected SP delta, and a
frame that's silently 16 bytes off from that expectation can make the unwinder
misidentify frame boundaries mid-walk.

We hit this downstream, three vendoring hops removed from this repo: hermes-windows
(Microsoft's Windows fork of Meta's Hermes JS engine, used by react-native-windows)
vendors boost_1_86_0 wholesale, including this exact file pair, unmodified. On Windows
ARM64, running Hermes' interpreter on a custom fiber stack, this produces a deterministic
crash in a production React Native app under load (root-caused from 8 crash dumps across
2 engine builds and 4 ASLR bases — all module-relative-identical, confirming the
structural nature of the bug). Full writeup and a downstream fix for hermes-windows:
the companion downstream fix for hermes-windows (link added below once posted).

Anyone else running boost::context::fiber (or anything built on it — Boost.Fiber,
Boost.Coroutine2) on Windows ARM64 under /GS or -fstack-protector-all, exercising the
ontop/resume_with/fiber-teardown path under load, is exposed to the same defect.

History: where this was introduced

The asymmetry was introduced in
#201 "Windows arm64 fcontext support"
(merged 2022-07-05), specifically in commit abf8e04e23cf05a499594e674d1c90db39117662
("Spport Windows arm64 cpp exception", 2022-06-26). That commit extended the frame from
0xb0 to 0xd0 to save/restore four TEB fields (TeStackBase, TeStackLimit,
TeDeallocationStack, TeFiberData) so C++ exception unwinding sees a consistent TIB
across a fiber switch — a good and necessary change. It correctly rewired every other
offset in the function (the PC-save slot moved 0xa00xc0, the new TIB fields landed
at 0xa0/0xb0, the prolog's sub moved 0xb00xd0) but only bumped the epilog's
add from 0xb0 to 0xc0 — a 0x10 step, not the 0x20 step every other offset in
the same commit got. The diff (against
src/asm/ontop_arm64_aapcs_pe_armasm.asm) makes the off-by-one-constant plain:

 ontop_fcontext proc BOOST_CONTEXT_EXPORT
     ; prepare stack for GP + FPU
-    sub  sp, sp, #0xb0
+    sub  sp, sp, #0xd0
     ...
     ; save LR as PC
-    str  x30, [sp, #0xa0]
+    str  x30, [sp, #0xc0]
+
+    ; save current stack base and limit
+    ldp  x5,  x6,  [x18, #0x08]
+    stp  x5,  x6,  [sp, #0xa0]
+    ; save current fiber data and deallocation stack
+    ldr  x5, [x18, #0x1478]
+    ldr  x6, [x18, #0x20]
+    stp  x5,  x6,  [sp, #0xb0]
     ...
+    ; restore stack base and limit
+    ldp  x5,  x6,  [sp, #0xa0]
+    stp  x5,  x6,  [x18, #0x08]
+    ; restore fiber data and deallocation stack
+    ldp  x5,  x6,  [sp, #0xb0]
+    str  x5, [x18, #0x1478]
+    str  x6, [x18, #0x20]
     ...
     ; skip pc
     ; restore stack from GP + FPU
-    add  sp, sp, #0xb0
+    add  sp, sp, #0xc0

A second commit, e878e8edb2b2… ("Convert ARM64 armasm to armclang for Windows clang",
2024-06-19), mechanically transcribed this file's ARMASM64 syntax into the Clang/GAS
syntax now at src/asm/ontop_arm64_aapcs_pe_armclang.S, carrying the same 0x10
shortfall forward verbatim into the second file. Neither commit touched
jump_fcontext's analogous epilog, which is why it stayed correct throughout.

Proposed fix

Make the epilog symmetric with the prolog (add sp, sp, #0xd0) in both files, matching
jump_fcontext on the same target and ontop_fcontext on the ELF target. No other
register logic changes.

--- a/src/asm/ontop_arm64_aapcs_pe_armasm.asm
+++ b/src/asm/ontop_arm64_aapcs_pe_armasm.asm
@@ -122,7 +122,7 @@ ontop_fcontext proc BOOST_CONTEXT_EXPORT

     ; skip pc
     ; restore stack from GP + FPU
-    add  sp, sp, #0xc0
+    add  sp, sp, #0xd0

     ; jump to ontop-function
     ret x2
--- a/src/asm/ontop_arm64_aapcs_pe_armclang.S
+++ b/src/asm/ontop_arm64_aapcs_pe_armclang.S
@@ -128,7 +128,7 @@ ontop_fcontext:

     // skip pc
     // restore stack from GP + FPU
-    add  sp, sp, #0xc0
+    add  sp, sp, #0xd0

     // jump to ontop-function
     ret x2

(Exact line numbers as of develop @ d2142b6925…: src/asm/ontop_arm64_aapcs_pe_armasm.asm
prolog at line 65, epilog at line 127; src/asm/ontop_arm64_aapcs_pe_armclang.S prolog
at line 71, epilog at line 133.)

Validation status

  • Root cause: confirmed by direct inspection of develop HEAD (both files, both
    currently 0xd0/0xc0) and by disassembly of a downstream production crash (8 dumps,
    hermes-windows on Windows ARM64 — see linked report above). The skew is structural and
    input-independent: it does not depend on data, timing, or heap state, only on whether
    the ontop path is taken.
  • Fix: validated by symmetry against the two already-correct sibling implementations in
    this same file family (jump_fcontext PE variant, ontop_fcontext ELF variant), and
    independently against a downstream engine that carries an identical fix — see the
    linked hermes-windows PR for that fix's own validation status (empirical repro or
    build+inspection, whichever applies at time of reading).
  • We have not run this repo's own test suite (libs/context/test) against the fix —
    we don't have a boost ARM64-on-Windows test harness in our environment. If maintainers
    want a PR rather than (or in addition to) this issue, we're happy to open one with the
    diff above; it's a minimal, mechanical, two-line change and should be straightforward
    to verify against existing Boost.Context/Boost.Fiber ARM64 CI if one exists.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions