Skip to content

Copying minor loses ObjectHeader::keys_array on large/infrequent cycles — pre-existing, unmasked by #7432; blocks the public baseline #7472

Description

@proggeramlug

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.

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