Repository navigation
gc: the platform-unwinder stack-map walker segfaults on Linux/ELF — and on x86-64 it is the only walker #7333
Description
PERRY_STACKMAP_WALKER=unwind segfaults on Linux/ELF: 3 of the 8
statepoint-compilable probes die with SIGSEGV, on a build where the default
(fast x29-chain) walker runs all 8 clean against the pinned Node oracle.
Why this is worse than it looks
On aarch64 this is a bisection control going wrong, because the fast chain walk
is the default and it works. On x86-64 the unwinder IS the walker — fp_chain
is compiled only for any(macos, linux) + aarch64 and stubbed to None
everywhere else, so WalkerMode::Fast falls through to unwind::visit. #7328
already stated the general form of this hazard: "on any host where the x29 chain
walk is unavailable the unwinder is the walker, so a configuration that finds no
roots enumerates through the same path and forced evacuation stays quiet."
So this is very likely the next blocker for statepoints on x86-64, sitting
directly behind the compact-map defect (#7321 / #7331) — and it is one that
fails by crashing or scanning wrong memory, not by refusing.
Evidence
Linux/aarch64 (aarch64-unknown-linux-gnu, ELF), perry built in a Debian
container, probes compiled with PERRY_STATEPOINTS=1, run against Node 26.5.1:
baseline (fast walker): exit=0
forced evac only: exit=0 (PERRY_GC_FORCE_EVACUATE + VERIFY_EVACUATION)
trace only: exit=0 (PERRY_GC_TRACE=1)
unwind only: exit=139 <-- SIGSEGV
unwind + forced evac: exit=139
verify only: exit=139 (verify runs the unwinder too)
PERRY_STACKMAP_WALKER=unwind alone is sufficient; neither the trace flag
nor forced evacuation is involved. Failing probes: 02_survivor_promotion,
03_cross_gen_writes, 08_map_set_sidetables. Passing: 01, 04, 05, 06,
07. (09_try_catch_roots cannot compile under the explicit bridge at all
since #7330.)
The full matrix under the default walker on the same binaries is 8/8, output
byte-identical to the oracle, with .perry_gcmap present and .llvm_stackmaps
absent asserted per probe — so the maps themselves decode correctly and the
roots are right. It is specifically the unwinder's idea of where the frame is.
Suspected mechanism, not yet proven
Every root in a Perry aarch64 module is SP-relative (DWARF 31; measured 57
of 57 on one module, 92 of 92 on another). The walker resolves those with
_Unwind_GetGR(context, dwarf_reg).
For the stack-pointer column, an unwinder is entitled to return the frame's
CFA rather than the in-body SP — they differ by the frame size, and the
DWARF model defines the SP after unwinding as the CFA. fp_chain does not use
_Unwind_GetGR at all: it decodes the prologue's add x29, sp, #imm to
recover the body SP (#7173, refined by #7329). If libgcc's _Unwind_GetGR
returns the CFA where Apple's libunwind returns the body SP, that exactly
explains "clean on macOS/aarch64, wrong on Linux/aarch64, and wrong on any
x86-64" while leaving the fast path correct.
That is a hypothesis consistent with all the above; it has not been confirmed by
reading back the register value.
Reproducing
Needs a Linux host (or container). aarch64 Linux additionally needs
#7331, without which
perry-runtime does not link and the compact-map rewriter refuses every module.
PERRY_STATEPOINTS=1 perry benchmarks/gc_ratchet/probes/02_survivor_promotion.ts -o /tmp/p02
PERRY_STATEPOINTS=1 PERRY_STACKMAP_WALKER=unwind /tmp/p02 # SIGSEGV
Activity
proggeramlug commentedon Aug 3, 2026
Root cause, measured:
_Unwind_GetGRsegfaults for DWARF reg 7The walker does this once per root (
gc/roots/stack_maps.rs:856):On x86-64 every root is
Indirect [RSP + off], i.e.dwarf_reg == 7— thatpart is already established in #7324. What was not established is what that call
does. It does not return an unreliable value. It crashes the process.
Probed on x86-64 Linux (glibc 2.39, gcc 13.3.0), calling
_Unwind_GetGRfrom an_Unwind_Backtracecallback on a live frame, one register per run:The pattern is callee-saved vs not: libgcc's unwind context tracks the registers
CFI restores, and asking it for one it does not track dereferences a null column
pointer. RSP is not a tracked column — it is derived from the CFA — so reg 7 is
the one lookup guaranteed to fault.
That makes the fault site the
_Unwind_GetGRcall itself, not a later writethrough a wild address, which is worth knowing because it rules out the
"conservative scan writes through garbage" reading and means no amount of
address validation after the call can help.
Small correction to the framing carried in
docs/engine-plan.mdand #7324:"
_Unwind_GetGRdoes not reliably return the stack pointer" understates it.It is not unreliable, it is fatal, and it is fatal 100% of the time on the only
register x86-64 roots use.
What this implies for the fix
_Unwind_GetCFAworks on this host, and reg 6 (RBP) works, and Perry alreadyemits
"frame-pointer"="non-leaf"on every generated function under nativeroots. So the recoverable bases exist — what is missing is the delta from either
of them to the body RSP the stack map's offsets are relative to. That delta is
per-function and constant, and the unwinder does not expose it.
The cheapest place to close it looks like the compact-map rewriter (#7314),
which already parses the emitted assembly — where the prologue is visible.
Rebasing
RSP + offonto a CFA- or RBP-relative offset at rewrite time is thesame technique the aarch64 fast walker uses at runtime (decode the prologue,
accumulate the frame adjustment), and #7329 just finished making that decoder
correct — including the trailing
sub sp, sp, #immit originally missed, whichis the exact analogue of x86-64's
sub rsp, N.Doing it in the rewriter rather than the walker also keeps the runtime free of a
second architecture-specific prologue decoder.
Reproducer, if useful:
Built
gcc -O1 -fno-omit-frame-pointer -g, run once per register.docs(gc): record the full-suite RS4GC result and correct the x86-64 m…
fix(gc): split the precise-root analysis from its lowering (#7340)
proggeramlug commentedon Aug 5, 2026
Fixed. The fatal call site is gone:
gc/roots/stack_maps.rs:1071now branchesif location.dwarf_reg == ARCH_DWARF_SP { _Unwind_GetCFA(context) }instead of_Unwind_GetGR(context, 7)(#7349), and #7400 corrected the CFA base that commit introduced. Verified on currentmain.Note:
gc-native-roots.ymlstill carries a comment naming this issue as open on aarch64-Linux — written ~2 hours before #7400 merged, so it is stale. OnePERRY_STACKMAP_WALKER=unwindrun on that arm would settle it beyond doubt.