gc: pin latch aborts relocating a PINNED young Map on a preflight-skipped cycle (zod dep-corpus, exit 134) — and gc_pin_sites.py says OK #7990
Description
Found while re-running #7803's workload on bdfcba4a2 (v0.5.1499), now that the
zod dependency corpus links again (#7980).
What happens
test-files/gc-dep-corpus/main.ts under a rate-1 seeded schedule aborts with
exit 134 and the runtime's own latch report:
[gc-pin-latch] FATAL: copying minor is about to relocate a PINNED young object
on a preflight-skipped cycle. header=0x2db2f681350 obj_type=8 size=731 flags=0x37
The young-pin latch (gc/pin.rs) is incomplete: some site sets GC_FLAG_PINNED
without going through gc::pin_object. Find it with `python3 scripts/gc_pin_sites.py`
and route it through pin_object (#7645).
[gc-schedule] FAILURE (signal 6) under seed=15
[gc-schedule] safepoints=1870 scheduled_collections=1870
This is crates/perry-runtime/src/gc/copying.rs:691 — the latch #7645 added on
purpose — firing. So this is a detected fault, not a silent one, which is
the latch working as designed.
Reproduce
cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static
npm ci --ignore-scripts --no-audit --no-fund
PERRY_RUNTIME_DIR=target/release PERRY_NO_AUTO_OPTIMIZE=1 PERRY_DISABLE_BUILD_CACHE=1 \
target/release/perry test-files/gc-dep-corpus/main.ts -o /tmp/w
RATE=1 TIMEOUT=1800 PERRY_GC_PROTECT_FROMSPACE=0 PERRY_GC_DIAG=1 \
./scripts/gc_schedule_fuzz.sh /tmp/w 16
It is intermittent. Seed 15 aborted once in a 16-seed sweep and then passed
3/3 on re-run with the identical command, so do not treat a passing seed 15 as a
refutation — the seeded schedule does not replay exactly on this workload
(safepoints drifts ~4% run to run at a fixed seed). Budget a sweep, not a run.
What the header says
obj_type=8=GC_TYPE_MAP. The corpus keeps its registry inMaps
(SCHEMAS/CALLBACKSintest-files/gc-dep-corpus/shared.ts), walked by
parseRegistered().size=731.flags=0x37=MARKED | ARENA | PINNED | INTERNED | TENURED. Worth a second
look on its own:TENUREDis set on an object the copying minor is treating
as young.
The FATAL's own remediation does not apply
The message says to find a site that sets GC_FLAG_PINNED without going through
gc::pin_object. On this tree that tool disagrees:
$ python3 scripts/gc_pin_sites.py
gc_pin_sites: OK — every pin originates in gc::pin_object
(2 allowlisted exception(s), 56 GC_FLAG_PINNED tokens scanned).
So either one of the two allowlisted exceptions is responsible, or the pin is
legitimate and the defect is in the preflight-skip decision
(Self::preflight_walks_decided, copying.rs:1111) rather than in pin
bookkeeping. Either way the printed remediation currently sends the reader at a
hypothesis its own tool refutes, and should be reworded once the cause is known.
#7645 is closed, hence a new issue rather than a reopen.
Scope note
This is not the #7154-class rooting failure that the same sweep also
produces on other seeds (TypeError: value is not a function,
Cannot read properties of undefined) — those surface cycles later with nothing
for the collector to find, whereas this one is caught at the instant of the
move. Triage of the whole sweep is in gc-handoff/ZOD-NOTES.md (#7989).
Activity
docs(gc-handoff): final sweep counts and the #7990 cross-reference
docs(gc-handoff): triage #7803 on a corpus that links again (#7989)
proggeramlug commentedon Aug 13, 2026
The FATAL text is rewritten in PR #7999. The underlying fault is still open — this makes
the abort point at the right investigation, and the header itself says which one.
The message's own remediation is refuted, and #7999 stops it asserting a cause
gc_pin_sites.pyreports OK, and both of its allowlisted exceptions are test-only(
gc/malloc.rs'spush_test_object, and the latch sabotage test ingc/tests/copying/latch.rs), so neither can be reached from a user program. The onlyproduction writers of
GC_FLAG_PINNEDarepin_objectandpin_object_non_young. Thestated cause is therefore refuted for any real run, exactly as this issue says.
Two things in the reported header decode differently than assumed
TENUREDon a "young" object is not an anomaly.gc/types.rsonGC_FLAG_TENURED:"Non-moving generational: tenured objects stay physically in nursery (no copying /
forwarding-pointer machinery), but the trace pretends they're old-gen." TENURED +
nursery-resident is the ordinary state, so that half of the issue's "worth a second look"
resolves to "expected".
INTERNEDon aGC_TYPE_MAPis a contradiction, and that is the useful signal.GC_FLAG_INTERNEDis written in exactly one file —string/intern.rs, two sites — andonly on strings. Every other occurrence in the tree reads it, or preserves it across a
move (
copying.rs:748,oldgen.rs:1932). Nothing sets it on a Map.So the reported header is not a coherent live Map. It reads like memory that once held
an interned string. That points at the #7154 unrooted-slot class — the same class the rest
of the sweep produces on other seeds — reaching the collector rather than surfacing later in
JS, and not at pin bookkeeping. It also fits the ~1-in-16 rate: an unrooted register only
goes bad when a collection lands in its window, so it is intermittent, whereas an unrooted
cache would be reproducible.
Worth noting alongside it: the latch check in
move_youngruns before the size /plausibility guard a few lines below it, so a garbage header is attributed to the pin latch
before anything asks whether it is a plausible object at all.
What changed
gc/pin.rsgrowsheader_incoherence()andpinned_young_move_report(). The abort now:0x37againstgc/types.rs;only moment that evidence exists;
why it used to lead;
PERRY_GC_ZEAL=1 PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=800, because the default depth of 4 misses a windowhundreds of collections wide.
Six unit tests in
pin.rspin it, including one built from this issue's exact header bytesand one proving the verdict can come out consistent (a verdict that can only say one thing
is decoration).
Not done
The underlying fault. Also deliberately no CI gate: at ~6% of runs a gate would go red on
a healthy tree often enough to teach people to ignore it — the same reasoning that declined
to gate #7803's 19%. The next step is a sweep under the quarantine at raised depth, sized so
6% → 0% is distinguishable from luck.
fix(link): refuse a link whose ext wrappers bundle a different tokio (#…
docs(changelog): record the PerryTS#7990 regression
test(gc): correct the PerryTS#7990 assertion diagnostic
test(gc): pin the #7990 Map comparison regression (#8021)