Skip to content

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

@proggeramlug

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 in Maps
    (SCHEMAS / CALLBACKS in test-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: TENURED is 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

  1. added 2 commits that reference this issue on Aug 12, 2026
  2. proggeramlug commented on Aug 13, 2026

    @proggeramlug
    ContributorAuthor

    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.py reports OK, and both of its allowlisted exceptions are test-only
    (gc/malloc.rs's push_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 of GC_FLAG_PINNED are pin_object and pin_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)
    

    TENURED on a "young" object is not an anomaly. gc/types.rs on GC_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".

    INTERNED on a GC_TYPE_MAP is a contradiction, and that is the useful signal.
    GC_FLAG_INTERNED is 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_young runs 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.rs grows header_incoherence() and pinned_young_move_report(). The abort now:

    • decodes the flags byte by name, so nobody hand-decodes 0x37 against gc/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.rs pin 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.

  3. added a commit that references this issue on Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions