Skip to content

release blocker: cc --help segfaults on main (NULL deref in run_microtasks) — clean at 6c880be77b, broken at 42d0f45685 and still broken at tip #9305

Description

@proggeramlug

claude --help on the natively compiled cli_2.1.112.js bundle segfaults deterministically on current main, with no GC knobs, no environment, and no flags. --version works; --help and -p crash.

This is not #9261 (that needs PERRY_GC_SCHEDULE_SEED/RATE; --help with no knobs is explicitly node-identical there). It is not #8770 (that was non-deterministic agent-loop init).

Repro

PERRY_NO_CACHE=1 PERRY_RUNTIME_DIR=<...>/target/release \
  perry compile --no-auto-optimize --enable-wasm-runtime cli_2.1.112.js -o app
HOME=/tmp/cc_home ./app --help
rc=139 (SIGSEGV)
app[...]: segfault at fffffffffffc8250 ip ... error 5

PERRY_DEBUG_SYMBOLS=1, x86-64 Linux (perrymaster). The compile itself is clean — CC_RC=0, only the usual deferred-API warnings.

Good/bad, six binaries, one box, one toolchain

Every arm below is the same host, same nightly, same cli_2.1.112.js, same compile command. Three consecutive runs each.

binary source --help
A 6c880be77b + #9272's content rc=0, byte-identical to node (9,175 B)
B A + an unrelated runtime perf change rc=0, byte-identical
C A + the same change, final form rc=0, byte-identical
D pristine 42d0f45685 139 139 139
E D + that same unrelated change 139 139 139
F pristine d20fb4fd25 (main tip) 139 139 139

So the regression is in 6c880be77b..42d0f45685, excluding #9272 (which arm A already carries), and it is still present on main tip. The five non-test, non-CI commits in that window are #9282, #9257, #9254-phase-1, #9263, #9274, #9279.

Where it crashes

perry_runtime::promise::microtasks::run_microtasks, at the same file offset every run and across ASLR:

c6fc808:  mov    0x38(%rsp),%rax
c6fc814:  movzbl -0x37db0(%rax),%eax     <-- faults, %rax == 0
c6fc81b:  test   %eax,%eax
c6fc81d:  jne    ...

Fault address 0xfffffffffffc8250 is exactly 0 - 0x37db0, i.e. a NULL base with a large negative displacement — the shape of a thread-local access whose base pointer is null. bt is #0 run_microtasks #1 main.

The slot holds a live pointer, then holds NULL — same frame

gdb (ASLR off), breakpoints on the store and the reload of that spill slot:

STORE   rax=0x7ffff7e97bc0  rsp=0x7fffffffe780     (run_microtasks+0xa35)
RELOAD  slot=(nil)          rsp=0x7fffffffe780     (run_microtasks+0xc58)
Program received signal SIGSEGV

Same %rsp, so the same frame and the same slot: it held a valid pointer and reads back as NULL. Between the two there is a call setjmp and an indirect call *%r12 into runtime/generated code.

Caveat, stated rather than glossed: objdump finds six writers of 0x38(%rsp) in this (heavily LLVM-merged, ~30 KB) function, so "a callee scribbled on the caller's frame" and "a different control-flow path legitimately stored 0 and the reload does not check it" are both consistent with the evidence above. Distinguishing them is the next step, and a watchpoint on the slot should do it in one run.

Why this matters beyond the crash

cc --help is the corpus this repo's whole perf campaign measures against — #9272, #9216, #9177 and every A/B in that family gate on "--help byte-identical to node, 9,175 bytes". On main today that gate cannot run at all. Any perf work that rebases onto main right now gets a baseline that segfaults, which is indistinguishable from "my change broke it" until you build a control. It also means the bundle is not exercising the microtask pump at all, so nothing downstream of it is covered.

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