You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Deterministic GC-stress SIGSEGV: PERRY_GC_SCHEDULE_SEED=1 PERRY_GC_SCHEDULE_RATE=1 crashes cc at safepoint 4266 #9446
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).
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.
PERRY_GC_FROMSPACE_SCANtype=5 (Promise) +0 bare: the seven padding bytes after the one-byte state field, holding recycled arena bits that no rewrite descriptor reads. Known scanner false positive; the scanner-side skip is a separate small change. Dismissed.
PERRY_CONSERVATIVE_STACK_SCAN=1: not chased; it gets run on the fixed cc binary with the replay, reported on the PR.
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.
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=1segfaults 3/3 on the base build and runs correctly on the fixed one — the same trampoline frame, not a false positive.
Activity
proggeramlug commentedon Sep 2, 2026
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 atperry_runtime::abi_trampoline::call_all_f64_x86_64→cli_2_1_112_js.w74_constructor(OpenTelemetry'sLoggerProviderclass expression, constructed through the runtime constructor table). The frame the unwinder derives above the trampoline is0xa;rax = 0xaat the faultingcmpb $0x48,(%rax), which isx86_64_fallback_frame_stateprobing a PC it got from a garbage return address.readelf -wFon the binary gives the reason: the trampoline's FDE saysCFA = rsp+32at thecall *%rdi, but theasm!block has already donemov %rsp,%r12; sub %rdx,%rsp; and $-16,%rspto spill arguments past the eighth. For any callee with more than eight f64 arguments (thiscounts; a synthesized capture-stashing constructor has dozens) the return address isstack_bytesaway 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
throwinside such a callee never finds thecatchabove the trampoline. Deterministic, no GC knobs:test-files/test_gap_9446_trampoline_unwind.tssegfaults before its first line on the old x86-64 toolchain.The three unexamined leads
PERRY_GC_VERIFY_MARKmarked->UNMARKEDon a copying minor: old-generation children a MINOR legitimately leaves unmarked — the known false positive of that verifier (gc(runtime): natively-compiled cli.js agent-loop init hits non-deterministic GC/heap corruption (number-for-Map, undefined reads, SIGSEGV @ tagged ptr) — cc-boot layer 5 #8770's ladder). Dismissed.PERRY_GC_FROMSPACE_SCANtype=5 (Promise) +0 bare: the seven padding bytes after the one-bytestatefield, holding recycled arena bits that no rewrite descriptor reads. Known scanner false positive; the scanner-side skip is a separate small change. Dismissed.PERRY_CONSERVATIVE_STACK_SCAN=1: not chased; it gets run on the fixed cc binary with the replay, reported on the PR.Something else the quarantine caught on the base binary
PERRY_GC_PROTECT_FROMSPACE=1 …_DEPTH=800with the seed faults much earlier, at safepoint 818, on a stale receiver inperry_closure_cli_2_1_112_js__14587— zod'sq.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.proggeramlug commentedon Sep 2, 2026
Replay on the fixed build (#9497 at
f1e9c370a):PERRY_GC_SCHEDULE_SEED=1 PERRY_GC_SCHEDULE_RATE=1now completes —done: seed=1 safepoints=6645 scheduled_collections=6645 copying_minors=6645 moved_objects=960167, answering node'sNot logged in · Please run /login. The base build dies at 4266.PERRY_CONSERVATIVE_STACK_SCAN=1segfaults 3/3 on the base build and runs correctly on the fixed one — the same trampoline frame, not a false positive.…_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 awaitedvaluehanded tojs_async_step_chainnaming 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.
fix(runtime): make the all-f64 call trampoline unwindable while its c…