Repository navigation
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
Activity
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 in
gc/tests/copying/latch.rs), so neither can be reached from a user program. The only
production writers ofGC_FLAG_PINNEDarepin_objectandpin_object_non_young. The
stated cause is therefore refuted for any real run, exactly as this issue says.Two things in the reported header decode differently than assumed
obj_type=8 size=731 flags=0x37 (MARKED|ARENA|PINNED|INTERNED|TENURED)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 — and
only 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:- decodes the flags byte by name, so nobody hand-decodes
0x37againstgc/types.rs; - prints a coherence verdict computed from the header at the instant of the abort — the
only moment that evidence exists; - explains TENURED-on-young instead of leaving it looking suspicious;
- ranks five candidates by that evidence, with the pin-site scan last and a note saying
why it used to lead; - points the incoherent case at
PERRY_GC_ZEAL=1 PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=800, because the default depth of 4 misses a window
hundreds of collections wide.
Six unit tests in
pin.rspin it, including one built from this issue's exact header bytes
and 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.- decodes the flags byte by name, so nobody hand-decodes
- added a commit that references this issue
on Aug 13, 2026
Found while re-running #7803's workload on
bdfcba4a2(v0.5.1499), now that thezod dependency corpus links again (#7980).
What happens
test-files/gc-dep-corpus/main.tsunder a rate-1 seeded schedule aborts withexit 134 and the runtime's own latch report:
This is
crates/perry-runtime/src/gc/copying.rs:691— the latch #7645 added onpurpose — firing. So this is a detected fault, not a silent one, which is
the latch working as designed.
Reproduce
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
(
safepointsdrifts ~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 byparseRegistered().size=731.flags=0x37=MARKED | ARENA | PINNED | INTERNED | TENURED. Worth a secondlook on its own:
TENUREDis set on an object the copying minor is treatingas young.
The FATAL's own remediation does not apply
The message says to find a site that sets
GC_FLAG_PINNEDwithout going throughgc::pin_object. On this tree that tool disagrees: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 pinbookkeeping. 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 nothingfor 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).