Skip to content

fix(gc): the evacuation verifier released its malloc-registry borrow before validating malloc-backed parents (PERRY_GC_VERIFY_EVACUATION re-entered the RefCell) - #9965

Draft
proggeramlug wants to merge 5 commits into
PerryTS:mainfrom
proggeramlug:fix/verify-evacuation-borrow

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Diagnostic-only runtime fix, on origin/main 8b7dc33. Written by codex from the campaign's ATX run; gates not yet run locally (dev-box disk under the 12 GB floor when the task ran) — perrymaster's gate ladder and a 4-turn cc run under PERRY_GC_VERIFY_EVACUATION=1 are the acceptance.

Why

With PERRY_GC_VERIFY_EVACUATION=1, a copying minor on cc dies in its own verifier: gc/malloc.rs:527 RefCell already borrowed. The verifier held a shared borrow of the thread-local MALLOC_STATE while iterating s.objects; each malloc-backed parent's slot validation (verify_old_young_parent_slots_coveredvisit_gc_rewrite_slotsverify_old_young_slot_covered) can reach remembered_child_needs_trackinggc_malloc_header_is_tracked, whose ensure_set_built takes a mutable borrow of the same RefCell to rebuild the exact-lookup set. Same thread, nested borrow, panic — before the verifier has inspected anything. The campaign needed this arm to discriminate an intermittent cc TypeError seen under #9951 (2 of 7 runs) and the arm could not run.

Production callers of the exact-membership helper (barrier/mod.rs, young_log.rs, native_handle.rs, timer.rs, path.rs, symbol/get.rs, value/dyn_index.rs, json/stringify.rs) were audited: none holds a MALLOC_STATE borrow across the call. The re-entrancy is verifier-only.

What changes

  • gc/verify.rs: one helper snapshots the malloc header vector and releases the borrow before any verifier callback. Every verifier-owned malloc walk uses it: the old→young edge check, marked-child checks, array-slot enumeration, and the final evacuation heap walk. Validation semantics unchanged — no try_borrow fallback, no weakened pointer check.
  • changelog.d/verify-evacuation-malloc-borrow.md.

Test (named; sabotage stated)

gc::tests::copying::verify_malloc_borrow::test_copied_minor_verify_evacuation_releases_malloc_registry_before_validation — on a spawned worker thread: malloc-backed closure parent → malloc-backed child, registry made inactive with a non-empty side table, asserts the exact lookup's rebuild count advances by one during verify_old_to_young_edges_collect, then completes a copying minor with evacuation verification on (a nursery object copied; both evacuation_verify and old_young_edge_verify phases present). Sabotage: put the verifier loop back under MALLOC_STATE.with(...borrow()) — the child lookup's borrow_mut() panics the worker and join().expect(...) fails.

Gates

  • Local: rustfmt --check and git diff --check only (disk floor). Not run: the named test, cargo test -p perry-runtime --release --lib -- --test-threads=1, cargo build --release -p perry-runtime --features wasm-host.
  • perrymaster: relink cc on main's cache, run 4 turns with PERRY_GC_VERIFY_EVACUATION=1; the run must complete all four turns with verifier output present and no RefCell already borrowed/panic.

GC-adjacent: needs the run-extended-tests label.

VF2 (1ec9e0e): the parent names itself

On perrymaster the fixed verifier ran 4 cc turns on main without a panic and, on the #9951 runtime, caught a real fault in 1 of 3 runs: stale forwarded pointer in heap fields at the first minor after a budgeted sweep. The panic named only slot/old/forwarded_to. The second commit makes every stale-forwarding panic (heap rewrite descriptors, remembered dirty ranges, shadow-stack/stack-map/global roots, the named Rust and FFI root scanners, the runtime side-table visitor paths) one line with parent= parent_type= parent_space=(old_page|nursery_from|nursery_to|promoted_in_place_this_cycle|malloc|pinned) slot_index= visitor= child_type= child_space= remembered= young_logged= dirty_snapshot= minor= trigger= after_budgeted_step= surface=, all derived on the cold failure path only (the passing per-slot closure is unchanged). Under PERRY_GC_DIAG=1 a passing copied minor prints [gc-verify] minor=N evacuation_ok parents= slots= old_young_edges= so a clean run proves the verifier was live. Tests: stale_forwarded_reference_panic_names_parent_slot_and_coverage, evacuation_verifier_pass_line_counts_parents_and_slots (new module gc/tests/copying/verify_parent_context.rs). Local gates on the second commit are partial (disk): the focused verifier gate passed 16 before the final cleanup; the final rerun, full lib suite and archive build run on perrymaster's ladder (stage VF2: 6 four-turn cc runs each on main+fix and #9951+fix with the verifier on).

Measured (perrymaster VF2, 2026-09-07)

Gate on the box at 38229cc: runtime lib suite 3,250 passed / 0 failed one thread (42 verifier-named tests ok), archive feature set rc 0. Twelve 4-turn cc runs (6 on main + this, 6 on the #9951 runtime + this, interleaved) with PERRY_GC_VERIFY_EVACUATION=1 PERRY_GC_DIAG=1: 0 verifier failures, 0 panics, 0 non-diagnostic stderr lines, all 48 turns completed. Liveness on every copying minor (25–27 [gc-verify] minor=N evacuation_ok lines per run; e.g. minor 9: parents=735,640 slots=4,106,513 old_young_edges=55). The verifier's cost is ≈ +1.3 s per turn; RSS unchanged. The stale-forwarded-pointer fault first seen on the #9951 runtime is now 1 of 9 verifier runs there and 0 of 7 on main; it did not recur under the attributing verifier, so no parent line exists yet — the instrument stays armed on that family.

https://claude.ai/code/session_011dhBmdn4vGgNibjo3oZqTo

Snapshot malloc-backed headers before running verifier callbacks so exact
child validation can lazily rebuild the malloc registry without re-entering
its RefCell borrow. Add a worker-thread copying-minor regression fixture.

Claude-Session: https://claude.ai/code/session_011dhBmdn4vGgNibjo3oZqTo
Record the re-entrancy path, structural fix, disk-gated validation status,
and the requested perrymaster campaign handoff.

Claude-Session: https://claude.ai/code/session_011dhBmdn4vGgNibjo3oZqTo
@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug proggeramlug added the run-extended-tests Opt PR into compile-smoke/parity/doc-tests/drizzle-mysql-smoke label Sep 7, 2026
Ralph Küpper added 3 commits September 7, 2026 15:31
Name heap parents, layout slots, root scanners, and collection coverage
when evacuation verification finds a stale forwarding alias. Emit a compact
success witness with heap-walk and remembered-edge counts under GC diagnostics.

Add focused failure-attribution and success-line regression tests.

Claude-Session: https://claude.ai/code/session_011dhBmdn4vGgNibjo3oZqTo
Record VF2 field derivation, covered failure sites, passing-path cost,
focused test evidence, disk-limited gates, and the perrymaster campaign
request.

Claude-Session: https://claude.ai/code/session_011dhBmdn4vGgNibjo3oZqTo
Re-pin the PASS1_MARKED non-moving window after auditing the verifier
diagnostic plumbing, and classify its three counter-only TLS holders.

Claude-Session: https://claude.ai/code/session_011dhBmdn4vGgNibjo3oZqTo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-extended-tests Opt PR into compile-smoke/parity/doc-tests/drizzle-mysql-smoke

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant