perry's default configuration segfaults on benchmarks/json_polyglot/bench_field_access.ts. It is deterministic (3/3), on main since ~13:00 today, and it blocks regenerating the public benchmark artifact — run_public_baseline.sh completes three of four legs and then fails the cross-runtime checksum gate with "no checksum row for 'perry'", because the default perry (gen-gc + lazy tape) variant produced no successful runs.
Repro
./target/release/perry benchmarks/json_polyglot/bench_field_access.ts -o /tmp/fa
/tmp/fa # exit 139, no output
Bisect
git bisect over the 46 commits merged today, testing exit status:
first bad commit: 77fa4c5c4 gc: adaptive tenuring + young-scoped scavenge cap (#7432)
Confirmed by hand at both ends — parent 0402a05af exits 0 three times out of three; 77fa4c5c4 and today's tip both exit 139 three out of three.
It is NOT the tenuring threshold
The headline change is ruled out. Pinning tenuring_survivals() to the pre-#7432 constant 4 still crashes 3/3. Flooring the survival-rate lock at 2 instead of 1 also still crashes. So neither adaptive tenuring nor promote-on-first-copy is the mechanism.
What it is: cycle size
The remaining part of #7432 is the young-scoped scavenge cap, which changed the cadence from degenerate once-per-1 MB-block to once per ~16 MB of young-gen occupancy. The effect on this workload:
|
copying minors |
objects copied per cycle |
parent 0402a05af |
592 |
~123k |
tip 351742d30 |
4 |
228,425 (17.5 MB) in one cycle |
PERRY_GC_DIAG on the failing run shows the last two cycles as
copied_objects=228425 copied_bytes=17587656 tenuring_survivals=4
copied_objects=0 copied_bytes=0 tenuring_survivals=1
<segfault>
A cycle copying 228k objects, then one copying zero, then the fault.
Sweeping the cap confirms the size dependence — PERRY_GC_SCAVENGE_NURSERY_MB:
1 MB → 139 2 MB → 139 4 MB → 139 8 MB → 139 16 MB → 139 32 MB → 0
At 32 MB no minor fires in the vulnerable window and the program completes.
The fault
EXC_BAD_ACCESS (code=1, address=0x7ffc000000000014)
-> ldr w2, [x23, #0x4]
add x1, x23, #0x14
0x7FFC_0000_0000_0000 is the NaN-box immediate tag base; +0x14 is the header-payload offset. So a value that should be a heap object is an immediate (undefined/null/bool) and is being dereferenced as a StringHeader/ArrayHeader — length at +4, data at +0x14.
Both preconditions are required
default (gen-gc + lazy tape) → 139
PERRY_GEN_GC=0 → 0
PERRY_JSON_TAPE=0 → 0
both off → 0
Mark-sweep traces everything, so a missed old→young edge would not bite there — consistent with the copying minor being implicated. LazyArrayHeader holds four raw heap pointers (blob_str, materialized, materialized_elements, materialized_bitmap); they have a GcRewriteDescriptorKind::LazyArray rewrite descriptor in gc/layout.rs, and json_tape.rs registers no scanner of its own. That is where I would look first, at the scale the new cadence produces.
Note on the benchmark's checksum
Unrelated but worth recording: the field_access checksum is not stable run-to-run on the parent build (2552986845, 2552987326, 2552987326), while today's PERRY_JSON_TAPE=0 is stable at 2552986400. A cross-runtime checksum gate over a value that drifts is a weak gate; I did not chase this.
Impact
Default-configuration segfault on a JSON workload, and the public benchmark artifact cannot be regenerated until it is fixed — the gate correctly refuses to publish a comparison in which perry has no row.
perry's default configuration segfaults onbenchmarks/json_polyglot/bench_field_access.ts. It is deterministic (3/3), onmainsince ~13:00 today, and it blocks regenerating the public benchmark artifact —run_public_baseline.shcompletes three of four legs and then fails the cross-runtime checksum gate with "no checksum row for 'perry'", because the defaultperry (gen-gc + lazy tape)variant produced no successful runs.Repro
Bisect
git bisectover the 46 commits merged today, testing exit status:Confirmed by hand at both ends — parent
0402a05afexits 0 three times out of three;77fa4c5c4and today's tip both exit 139 three out of three.It is NOT the tenuring threshold
The headline change is ruled out. Pinning
tenuring_survivals()to the pre-#7432 constant4still crashes 3/3. Flooring the survival-rate lock at 2 instead of 1 also still crashes. So neither adaptive tenuring nor promote-on-first-copy is the mechanism.What it is: cycle size
The remaining part of #7432 is the young-scoped scavenge cap, which changed the cadence from degenerate once-per-1 MB-block to once per ~16 MB of young-gen occupancy. The effect on this workload:
0402a05af351742d30PERRY_GC_DIAGon the failing run shows the last two cycles asA cycle copying 228k objects, then one copying zero, then the fault.
Sweeping the cap confirms the size dependence —
PERRY_GC_SCAVENGE_NURSERY_MB:At 32 MB no minor fires in the vulnerable window and the program completes.
The fault
0x7FFC_0000_0000_0000is the NaN-box immediate tag base;+0x14is the header-payload offset. So a value that should be a heap object is an immediate (undefined/null/bool) and is being dereferenced as aStringHeader/ArrayHeader— length at+4, data at+0x14.Both preconditions are required
Mark-sweep traces everything, so a missed old→young edge would not bite there — consistent with the copying minor being implicated.
LazyArrayHeaderholds four raw heap pointers (blob_str,materialized,materialized_elements,materialized_bitmap); they have aGcRewriteDescriptorKind::LazyArrayrewrite descriptor ingc/layout.rs, andjson_tape.rsregisters no scanner of its own. That is where I would look first, at the scale the new cadence produces.Note on the benchmark's checksum
Unrelated but worth recording: the
field_accesschecksum is not stable run-to-run on the parent build (2552986845, 2552987326, 2552987326), while today'sPERRY_JSON_TAPE=0is stable at 2552986400. A cross-runtime checksum gate over a value that drifts is a weak gate; I did not chase this.Impact
Default-configuration segfault on a JSON workload, and the public benchmark artifact cannot be regenerated until it is fixed — the gate correctly refuses to publish a comparison in which perry has no row.