Skip to content

gc: PERRY_GC_FROMSPACE_SCAN_ABORT=1 aborts deterministically on two corpus files — an Old-space slot points at an unevacuated NurseryEden object #7677

Closed

Description

@proggeramlug

Summary

PERRY_GC_FROMSPACE_SCAN_ABORT=1 aborts 100% reproducibly on two corpus files, reporting an Old-space slot that points into NurseryEden at a target that was never evacuated — while the slot is dirty and the owner is marked.

Found while investigating #7018 (which does not reproduce); this is a separate, unrelated finding.

Reproduction

export CARGO_TARGET_DIR=<your target dir>
cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static
./target/release/perry test-files/test_gap_repsel_scalar_replaced_locals.ts -o /tmp/srl
PERRY_GC_HEAP_LIMIT=8 PERRY_GC_FROMSPACE_SCAN_ABORT=1 /tmp/srl
  • test_gap_repsel_scalar_replaced_locals — aborts every run
  • test_gap_repsel_gc_stress — aborts every run
  • test_gap_repsel_ptr_shape_elements, test_gap_gc_pointer_publish, test_gap_repsel_element_shape_loop_clone — clean

It is SIGABRT, not SIGSEGV (the instrument's own panic, working as designed), and it is independent of PERRY_GC_TRACE, of the rest of the #7018 env (the knob alone suffices), and of PERRY_RS4GC — 0 and 1 both abort.

What the instrument reports

[gc-fromspace-scan abort] objects=97487 words=414928 fwd_owners_skipped=0 missing_rewrites=0
                          dangling=1 owners=1 | never_dirty=0 lost_dirty=0 dirty_but_missed=1
  owner=0x3656ab68010 type=3 space=Old +24 nanbox -> 0x3656a4d8934 (type=48 NurseryEden)
  DANGLING (target not evacuated)
  [slot dirty_now=true ever_dirty=true owner_flags=0x23 marked=true]

The interesting part is what is not wrong. missing_rewrites=0, so no evacuated object went un-rewritten. The slot is in the remembered set (ever_dirty=true), and its owner is marked. The target simply was never evacuated, which is why the rewrite pass had nothing to do — dirty_but_missed=1.

Why it looks like a table rather than a register

Per CLAUDE.md's own tell: "an unrooted register goes bad only when a collection lands in its window, so it is intermittent; an unrooted cache goes bad at collection #0 and stays bad, so a perfectly reproducible GC bug means a table, not a register." This is perfectly reproducible on two files and absent on three, which points at a runtime-side cache of a raw heap pointer — the class of changelog.d/7219-registry-gc-unrooted-caches.md and changelog.d/7239-gc-unrooted-runtime-caches.md — rather than a codegen rooting miss, which scripts/gc_root_dominance_check.py would see and which is currently allowlist-empty.

gc_register_mutable_root_scanner in gc/mod.rs (~55 entries) is where such a cache has to be registered, and the static checker is structurally blind to one that is not.

Why it matters beyond the knob

PERRY_GC_FROMSPACE_SCAN_ABORT is one of the #7154-family instruments, and its whole job is to turn a stale from-space reference into an immediate, precise report. If it fires deterministically on two corpus files today, then either

  • there is a live unrooted heap-pointer cache reachable from those two workloads, or
  • the instrument has a false-positive shape,

and the two are worth telling apart before the next rooting investigation trusts a clean run of it. It is not covered by #7254 (that is the different ZEAL + VERIFY_EVACUATION pairing).

Not investigated here

The owner's type=3 and the +24 slot offset are recorded but not decoded; whoever picks this up should start by identifying the owning object type and which side table (if any) holds that address, rather than by reading codegen output.

Activity

  1. proggeramlug commented on Aug 10, 2026

    @proggeramlug
    ContributorAuthor

    Closing — re-tested on current main (v0.5.1432) with the instrument verifiably ARMED, and it no longer aborts.

    The issue's literal repro, unchanged:

    PERRY_GC_HEAP_LIMIT=8 PERRY_GC_FROMSPACE_SCAN_ABORT=1 ./srl
    PERRY_GC_HEAP_LIMIT=8 PERRY_GC_FROMSPACE_SCAN_ABORT=1 ./gc_stress
    
    fixture exit [gc-fromspace-scan] lines eligible=false / conservative fallback objects scanned
    test_gap_repsel_scalar_replaced_locals 0 230 0 43,734
    test_gap_repsel_gc_stress 0 106 0 22,380

    Why the arming evidence is the whole point

    An earlier pass at this found the repro inert rather than clean: PERRY_GC_TRACE=1 PERRY_GC_DIAG=1 showed eligible=false fallback=conservative_stack on every collection and zero [gc-fromspace-scan] lines. The scan code never ran, so exit 0 meant nothing at all — a run with no copying minor scans nothing and aborts on nothing.

    That is now reversed: hundreds of scan lines, zero conservative fallbacks, tens of thousands of objects examined per run, no abort.

    What changed is #7721, which flipped PERRY_GC_MOVING_LOOP_POLLS to default ON. Reaching a copying minor by the alloc-point→safepoint route needed loop polls, and they were default-OFF from #7161 until #7721. So the configuration this issue reported was, for most of its life, one in which the instrument could not fire — and whatever the original abort was, the code path that produced it is now exercised heavily and comes back clean.

    Reopen if it returns, but please include the [gc-fromspace-scan] count and the eligible= state in the report — without those a clean exit and an inert run are indistinguishable, which is what made this issue hard to settle for so long.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions