Skip to content

Deterministic GC-stress SIGSEGV: PERRY_GC_SCHEDULE_SEED=1 PERRY_GC_SCHEDULE_RATE=1 crashes cc at safepoint 4266 #9446

Closed

Description

@proggeramlug
PERRY_GC_SCHEDULE_SEED=1 PERRY_GC_SCHEDULE_RATE=1 <cc binary> …
[gc-schedule] FAILURE (signal 11) under seed=1

Reproducible SIGSEGV at safepoint 4266, core dumped. A deterministic seed makes this repeatable, which is rare and valuable for a GC bug — it is a ready-made bisection target rather than a flaky one.

This is separate from #9417, whose residual root cause is a codegen precise-root gap (a receiver in a plain %rbp slot in a function emitted with zero shadow-frame binds). It surfaced while stress-testing the GC to characterise that bug.

Related observations from the same session, not chased

Recording them here rather than dropping them, but flagged as unexamined — I judged them likely instrument false positives and did not verify that judgement:

  • PERRY_CONSERVATIVE_STACK_SCAN=1 segfaults cc.
  • PERRY_GC_VERIFY_MARK=1 reports marked->UNMARKED edges on the copying minor — 18, then 76806.
  • PERRY_GC_FROMSPACE_SCAN=1 occasionally reports OFFENDERS whose owners are all type=5 (Promise) with +0 bare targets carrying nonsense headers.

Anyone picking this up should treat those three as leads to confirm or dismiss, not as established defects. If VERIFY_MARK is telling the truth, it is a much larger problem than the seeded crash.

Environment

cc = cli_2.1.112.js compiled from origin/main@70eaabe57. Symbolized binaries and tooling kept on the build host at /root/claude-9417/ (app_sym, symbolize.py, gdbloop.sh).

Activity

  1. proggeramlug commented on Sep 2, 2026

    @proggeramlug
    ContributorAuthor

    Root-caused and fixed in #9497. The crash is not a rooting bug in generated code — it is the runtime's all-f64 vtable call trampoline being un-unwindable while its callee runs.

    What safepoint 4266 is

    Under gdb the SIGSEGV is inside libgcc's unwinder, called from the copying minor's native-root walk (_Unwind_Backtrace ← run_copied_minor_attempt ← gc_safepoint_moving_minor ← a loop poll), and the frame chain breaks at perry_runtime::abi_trampoline::call_all_f64_x86_64 → cli_2_1_112_js.w74_constructor (OpenTelemetry's LoggerProvider class expression, constructed through the runtime constructor table). The frame the unwinder derives above the trampoline is 0xa; rax = 0xa at the faulting cmpb $0x48,(%rax), which is x86_64_fallback_frame_state probing a PC it got from a garbage return address.

    readelf -wF on the binary gives the reason: the trampoline's FDE says CFA = rsp+32 at the call *%rdi, but the asm! block has already done mov %rsp,%r12; sub %rdx,%rsp; and $-16,%rsp to spill arguments past the eighth. For any callee with more than eight f64 arguments (this counts; a synthesized capture-stashing constructor has dozens) the return address is stack_bytes away from where the description says. LLVM keeps no frame pointer for that function on x86-64 Linux; on aarch64 it happened to, which is why this never showed on the Macs.

    What it costs without the seed

    Every unwinder stepping through the trampoline reads the wrong slot: the GC root walk stops there when the garbage is mapped — every frame above the trampoline silently leaves the root set for that collection — and crashes as above when it is not; and a throw inside such a callee never finds the catch above the trampoline. Deterministic, no GC knobs: test-files/test_gap_9446_trampoline_unwind.ts segfaults before its first line on the old x86-64 toolchain.

    The three unexamined leads

    Something else the quarantine caught on the base binary

    PERRY_GC_PROTECT_FROMSPACE=1 …_DEPTH=800 with the seed faults much earlier, at safepoint 818, on a stale receiver in perry_closure_cli_2_1_112_js__14587 — zod's q.passthrough = () => q.clone({...q._zod.def, catchall: Kj()}), the receiver held across an allocating argument. That is the #9417 class, which #9479 has since closed for the dispatch-receiver shape; the quarantine replay on the fixed build will say whether this site is covered.

  2. proggeramlug commented on Sep 2, 2026

    @proggeramlug
    ContributorAuthor

    Replay on the fixed build (#9497 at f1e9c370a):

    • PERRY_GC_SCHEDULE_SEED=1 PERRY_GC_SCHEDULE_RATE=1 now completes — done: seed=1 safepoints=6645 scheduled_collections=6645 copying_minors=6645 moved_objects=960167, answering node's Not logged in · Please run /login. The base build dies at 4266.
    • The first unexamined lead is settled the other way from my guess above: PERRY_CONSERVATIVE_STACK_SCAN=1 segfaults 3/3 on the base build and runs correctly on the fixed one — the same trampoline frame, not a false positive.
    • The quarantine pairing (…_SEED=1 …_RATE=1 PERRY_GC_PROTECT_FROMSPACE=1 …_DEPTH=800) on the fixed build no longer faults at safepoint 818 either; its first find is now at 4746, a different bug — the awaited value handed to js_async_step_chain naming a Promise retired two minors earlier. Filed as Stale awaited Promise reaches js_async_step_chain under PERRY_GC_SCHEDULE_SEED=1 RATE=1 + PROTECT_FROMSPACE (cc, safepoint 4746) #9506 with the symbolized backtrace and artifacts.

    Closes via #9497.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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