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.
claude --helpon the natively compiledcli_2.1.112.jsbundle segfaults deterministically on currentmain, with no GC knobs, no environment, and no flags.--versionworks;--helpand-pcrash.This is not #9261 (that needs
PERRY_GC_SCHEDULE_SEED/RATE;--helpwith no knobs is explicitly node-identical there). It is not #8770 (that was non-deterministic agent-loop init).Repro
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.--help6c880be77b+ #9272's content42d0f45685d20fb4fd25(maintip)So the regression is in
6c880be77b..42d0f45685, excluding #9272 (which arm A already carries), and it is still present onmaintip. 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:Fault address
0xfffffffffffc8250is exactly0 - 0x37db0, i.e. a NULL base with a large negative displacement — the shape of a thread-local access whose base pointer is null.btis#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:
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 acall setjmpand an indirectcall *%r12into runtime/generated code.Caveat, stated rather than glossed:
objdumpfinds six writers of0x38(%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 --helpis the corpus this repo's whole perf campaign measures against — #9272, #9216, #9177 and every A/B in that family gate on "--helpbyte-identical to node, 9,175 bytes". Onmaintoday that gate cannot run at all. Any perf work that rebases ontomainright 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.