ARM64: fix asymmetric hoost_ontop_fcontext epilog (0xc0 dealloc vs 0xd0 alloc) — deterministic /GS FAST_FAIL (0xc0000409) on Windows ARM64#345
Draft
FaithfulAudio wants to merge 1 commit into
Conversation
hoost_ontop_fcontext's ARM64 Windows-PE epilog (both the armasm and clang-gas syntax variants) deallocated only 0xc0 of the 0xd0 bytes its own prolog allocated, an asymmetry not present in hoost_jump_fcontext (0xd0/0xd0, symmetric) nor in the ELF/GAS ontop_fcontext variant (0xb0/ 0xb0, symmetric -- smaller struct, no TIB fields). The "ontop" function (X2) is reached via `ret x2`, a tail-jump, not a call. Per boost::context::fiber's actual usage (fiber_fcontext.hpp: ~fiber()'s hoost_ontop_fcontext(...) call, and resume_with()), that function itself later returns normally via the target context's restored LR, back into the target's own suspended call site -- which expects SP restored to its pre-suspend value, exactly as hoost_jump_fcontext's symmetric epilog would leave it. Deallocating only 0xc0 leaves SP 0x10 bytes below that value once the ontop function returns, corrupting the resumed context's stack frame. On Windows ARM64 with /GS (MSVC) or -fstack-protector-all (Clang) enabled, this manifests as a deterministic __security_check_cookie FAST_FAIL (STATUS_STACK_BUFFER_OVERRUN, 0xc0000409) at a fixed module-relative offset, and related _CxxFrameHandler3 unwind aborts when the ontop function raises forced_unwind (fiber teardown/ exception-unwind path). Fix: make the epilog symmetric with the prolog (0xd0/0xd0), matching hoost_jump_fcontext. This exact asymmetry is also present verbatim upstream in boostorg/context (introduced with the original Windows ARM64 port, PR "Add windows arm64 fcontext support", 2022); it appears to predate hermes-windows' vendoring rather than being a divergence introduced here. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On Windows ARM64,
hoost_ontop_fcontext's prolog allocates0xd0(208) bytes of stack(
sub sp, sp, #0xd0) but its epilog deallocates only0xc0(192) bytes(
add sp, sp, #0xc0) before tail-jumping into the "ontop" callback. That 16-byte (0x10)shortfall is never reclaimed, so every control-flow path that takes the
ontopcontinuation(fiber teardown /
forced_unwind, and anyboost::context::fiber::resume_with-stylecall) resumes its target context with
spsitting 16 bytes below the value the targetexpects. On builds compiled with
/GS(MSVC, on by default) or-fstack-protector-all(Clang), the next
__security_check_cookiein the resumed frame reads the wrong stackslot and fails fast with
STATUS_STACK_BUFFER_OVERRUN(0xc0000409) — indistinguishable,from a crash-reporting standpoint, from a real stack-smash.
This is not a heap-corruption or use-after-free bug. It is a static, structural
off-by-
0x10in hand-written assembly: the skew is identical in magnitude and in theexact bytes it exposes on every single occurrence, across independent ASLR bases and
across both engine builds we have crash dumps for.
This fixes both PE-target assembly variants that implement
ontop_fcontextfor ARM64 —the Clang/GAS-syntax file that is the one actually linked into the shipping
Microsoft.JavaScript.Hermesbinary, and the MSVC ARMASM64-syntax file used by thealternate toolchain — making both symmetric with
hoost_jump_fcontext(already0xd0/0xd0on the same target) and with the ELF/GAS
ontop_fcontextvariant (already0xb0/0xb0,smaller frame, no TIB save/restore, but symmetric).
Root cause
File:
external/boost/boost_1_86_0/libs/context/src/asm/ontop_arm64_aapcs_pe_armasm.asm(MSVC ARMASM64 syntax), as it stands today on
main(f9abcf6a):File:
external/boost/boost_1_86_0/libs/context/src/asm/ontop_arm64_aapcs_pe_clang_gas.S(Clang/GAS syntax — this is the variant actually compiled into the shipping
hermes.dll, confirmed by disassembling the crashing binary):For comparison, the sibling function on the identical target,
external/boost/boost_1_86_0/libs/context/src/asm/jump_arm64_aapcs_pe_armasm.asm, issymmetric:
0xd0is the correct frame size for this target:0x40ford8-d15,0x60forx19-x30,0x10for the saved PC + alignment pad, and0x20for the fourTEB/TIB fields (
TeStackBase,TeStackLimit,TeDeallocationStack,TeFiberData)that the Windows-ARM64 port saves/restores so C++ exception unwinding sees a
consistent TIB across the fiber switch.
hoost_ontop_fcontext's own prolog agrees(
0xd0) — only its epilog under-deallocates by exactly the last0x10of that.Mechanism: why the 16-byte skew corrupts the resumed frame
ontop_fcontextdoesn't return to its caller in the normal sense — it's reached aspart of a context switch and tail-jumps into the "ontop" function via
ret x2(X2 holds the ontop-function's address, not a real return address). Per
boost::context::fiber's actual usage (~fiber()'shoost_ontop_fcontext(...)callfor teardown, and
resume_with()for the general case), that ontop function itselflater returns normally, via the target context's own restored
LR, back into thetarget's original suspended call site inside
jump_fcontext/ontop_fcontext— a sitethat expects
spto be exactly where it was when that context originally suspended.hoost_jump_fcontext's symmetric epilog (0xd0/0xd0) leavesspthere. The buggyontopepilog leavessp16 bytes below it, permanently, for the remaining lifetimeof that resumed call frame.
Hermes' interpreter wrapper around the switch stores its
/GScookie at a fixed[sp, #offset]computed by its own (compiler-generated, correct) prolog, then invokesthe fcontext switch, then runs
__security_check_cookieon its own way out. Becausethe switch returned control 16 bytes lower than the wrapper's prolog assumed, the
cookie check reads the wrong stack slot. In the corresponding crash dumps, that slot's
contents are exactly an fcontext register-save pair — the wrapper's own live resume PC
and
x24— planted0x10from where the save area should have ended; the true,un-tampered cookie sits at
[sp, #0x18]from that same base. Since the read valuedoesn't XOR-match
__security_cookie,__security_check_cookiecalls__report_gsfailure, which raisesSTATUS_STACK_BUFFER_OVERRUNvia__fastfail— anunmaskable fail-fast, not a catchable SEH/C++ exception, which is why it terminates the
process rather than propagating as a normal error.
A related, rarer signature (
ucrtbase!abortvia_CxxFrameHandler3) is consistent withthe same skew hitting the C++ exception-unwind path instead:
forced_unwind-driven fiberteardown also takes the
ontoproute, and_CxxFrameHandler3walks frames using thecompiler's static
.pdata/.xdataunwind metadata, which encodes each frame's expectedSP delta. A frame that's silently 16 bytes off from that expectation can cause the unwinder
to misidentify frame boundaries mid-walk; the CRT's response to that inconsistency is to
call
abort()rather than continue unwinding into unknown state.Frame bytes at the crash site are module-relative-identical across independent ASLR'd
runs and across both engine builds we have dumps for (which vendor the same assembly) —
i.e., this is a deterministic structural skew, not heap corruption. All app / native-module
threads were provably quiescent in every dump we inspected (KV worker parked in
SleepConditionVariableSRW; no WebView2 / MediaCapture activity), which rules outapp-code involvement.
The fix
Make
hoost_ontop_fcontext's ARM64 epilog symmetric with its own prolog (add sp, sp, #0xd0), in both PE-target assembly variants, matchinghoost_jump_fcontexton thesame target and the ELF/GAS
ontop_fcontextvariant (which is0xb0/0xb0— smallerframe, no TIB fields, but symmetric). No other register logic changes; this is a pure
epilog stack-deallocation fix, 1 byte-width immediate changed in each of two files.
Inherited from upstream boost.context — this is not a hermes-windows-specific defect
This exact asymmetry (
0xd0alloc /0xc0dealloc, both PE syntax variants) is presentverbatim in
boostorg/contextondeveloptoday. We traced it to the PR thatoriginally added Windows ARM64 support
(boostorg/context#201, merged
2022-07-05) — specifically to commit
abf8e04e23("Spport Windows arm64 cpp exception",2022-06-26), which extended the frame from
0xb0to0xd0to save/restore four TEB/TIBfields for exception-unwind correctness, correctly rewired every other offset in the
function, but only bumped the epilog's deallocation from
0xb0to0xc0instead of0xd0. A second commit,e878e8edb2("Convert ARM64 armasm to armclang for Windowsclang", 2024-06-19), mechanically transcribed the buggy ARMASM64 file into the
Clang/GAS-syntax variant, carrying the same off-by-
0x10forward verbatim into a secondfile. Neither hermes-windows nor RNW introduced this bug; it was vendored wholesale from
upstream boost 1.86.0.
We've filed a companion report against
boostorg/contextwith the same root cause andfix, framed for their file layout:
boostorg/context#336.
Fixing it here does not depend on that landing upstream first — hermes-windows vendors
its own copy of this file and can carry the fix independently, the same way it vendors
the rest of
external/boost/boost_1_86_0/.Relationship to Hermes V1 / RNW 0.84 (please read before suggesting an upgrade)
We're aware RNW 0.84 ships Hermes V1 (
0.0.0-2605.6002). "Upgrade to 0.84" is not asubstitute for this fix, for two reasons:
this defect (
2604/2607) ship in it today.mainas of this PR (f9abcf6a, whichis also the exact git-sha suffix of the shipping
2607.6001-f9abcf6aengine build wehave crash dumps for) still has the unfixed asm — this is a live defect on
main, nota historical one.
react-native-reanimatedhas no0.84 build. Apps in that position are exposed to this crash through no configuration
of their own, on a still-supported line.
We have not independently confirmed whether V1 carries the same defect or a corrected
epilog (V1's
ontop_fcontextcodegen path may simply be exercised less, which wouldmask rather than fix it). Happy to check if useful, but either way this fix should land
on the current line.
Environment
investigation, and on native Windows-on-ARM64 hardware in production).
Microsoft.JavaScript.Hermes 0.0.0-2604.21001-94aa5e1d(April) — crash athermes+0x4a209c(6 dumps).Microsoft.JavaScript.Hermes 0.0.0-2607.6001-f9abcf6a(July) — crash athermes+0x4a1ffc(2 dumps). Note thef9abcf6abuild suffix matchesmicrosoft/hermes-windowsmain's current HEAD short SHA.fcontextassembly and do not show this signature— useful as an A/B control.
Validation
The fix was built and its shipped form verified on Windows ARM64. Building the fork
branch with the shipping CI configuration (
build.js --platform arm64 --configuration release --no-uwp; Clang,/GS+-fstack-protector-all) succeeds in ~11 min. Both theninja build log and
compile_commands.jsonconfirm that the assembler variant actuallycompiled into
arm64/hermes.dllisontop_arm64_aapcs_pe_clang_gas.S(
clang.exe -target aarch64-pc-windows-msvc; CMake selects "assembler clang_gas …implementation fcontext"); the ARMASM64
.asmvariant is not compiled on this toolchain(it is fixed identically for correctness/symmetry but is dead on Clang builds).
The pre-fix crash is well characterised: archived minidumps show the
deterministic-when-hit
STATUS_STACK_BUFFER_OVERRUN(0xc0000409,FAST_FAIL_STACK_COOKIE_CHECK_FAILURE) on the fcontext coroutine-resume path in stockhermes.dll 0.0.2607.6001. The patched engine was then swapped into the deployedproduction app and confirmed loaded via a debugger attach (loaded-module FileVersion
1.0.0.0/ distinct ImageSize, not the stock0.0.2607.6001), and it ran the app'sheaviest JS↔native workload — a full bulk sync of 1,328 inspections plus the SVG-heavy
dashboard — cleanly.
Honest scope: the crash is probabilistic and scales with JS↔native volume; on the
current perf-optimized app it did not reproduce on demand within a bounded stock-repro
window, so a same-run "stock-crashes / patched-survives" differential was not captured.
The fix is otherwise correct by inspection: it makes the
ontop_fcontextepilogsymmetric with its own prolog (
0xd0/0xd0), matchinghoost_jump_fcontextand thesymmetric ELF
ontopvariant. Happy to share the 8 crash dumps andcdbdisassembly, orcoordinate a maintainer repro on ARM64 hardware, before merge.
Test Plan
hermes-windowswith this change (bothDebugandRelease, if maintainers' CI distinguishes).ontop_arm64_aapcs_pe_armasm.asmandontop_arm64_aapcs_pe_clang_gas.Sboth now show
add sp, sp, #0xd0in thehoost_ontop_fcontextepilog.production crash-trigger test.)
hoost_jump_fcontext,hoost_make_fcontext, the ELF/GASontop_fcontextvariant, or any non-ARM64/non-PE target — diff is scoped to thetwo files above.
Related
ontop_fcontext: epilog deallocates 0xc0 of the 0xd0 bytes the prolog allocates — 16-byte SP skew, breaks /GS and SEH-based unwind on resume boostorg/context#336Microsoft Reviewers: Open in CodeFlow