Repository navigation
gc: the platform-unwinder stack-map walker segfaults on Linux/ELF — and on x86-64 it is the only walker #7333
Description
Activity
Root cause, measured:
_Unwind_GetGRsegfaults for DWARF reg 7The walker does this once per root (
gc/roots/stack_maps.rs:856):let base = _Unwind_GetGR(context, i32::from(location.dwarf_reg));
On x86-64 every root is
Indirect [RSP + off], i.e.dwarf_reg == 7— that
part 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:DWARF reg register result 0 RAX SIGSEGV (139) 3 RBX returns correctly 6 RBP returns correctly 7 RSP SIGSEGV (139) 16 RIP returns correctly 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 write
through 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 already
emits"frame-pointer"="non-leaf"on every generated function under native
roots. 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.
RebasingRSP + offonto a CFA- or RBP-relative offset at rewrite time is the
same 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 trailingsub sp, sp, #immit originally missed, which
is the exact analogue of x86-64'ssub 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:
static _Unwind_Reason_Code cb(struct _Unwind_Context *ctx, void *a) { if (hits++ != 1) return _URC_NO_REASON; /* our caller's frame */ printf("calling GetGR(%d)\n", which); fflush(stdout); uintptr_t v = _Unwind_GetGR(ctx, which); /* faults for which==7 */ printf("returned 0x%lx\n", (unsigned long)v); return _URC_NO_REASON; }
Built
gcc -O1 -fno-omit-frame-pointer -g, run once per register.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.
PERRY_STACKMAP_WALKER=unwindsegfaults on Linux/ELF: 3 of the 8statepoint-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_chainis compiled only for
any(macos, linux) + aarch64and stubbed toNoneeverywhere else, so
WalkerMode::Fastfalls through tounwind::visit. #7328already 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 Debiancontainer, probes compiled with
PERRY_STATEPOINTS=1, run against Node 26.5.1:PERRY_STACKMAP_WALKER=unwindalone is sufficient; neither the trace flagnor 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_rootscannot compile under the explicit bridge at allsince #7330.)
The full matrix under the default walker on the same binaries is 8/8, output
byte-identical to the oracle, with
.perry_gcmappresent and.llvm_stackmapsabsent 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_chaindoes not use_Unwind_GetGRat all: it decodes the prologue'sadd x29, sp, #immtorecover the body SP (#7173, refined by #7329). If libgcc's
_Unwind_GetGRreturns 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-runtimedoes not link and the compact-map rewriter refuses every module.