Skip to content

gc: the platform-unwinder stack-map walker segfaults on Linux/ELF — and on x86-64 it is the only walker #7333

Description

@proggeramlug

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 walkerfp_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

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