You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
gc: ASYNC_STEP_GUARD.last_closure is a write-only stale closure address that is still rekeyed #8193
ASYNC_STEP_GUARD.last_closure (crates/perry-runtime/src/promise/) is a raw closure address rekeyed in two places — scan_promise_roots_mut (scanners.rs:89) and its budgeted twin scan_async_step_guard_step (scanners.rs:500) — both via visit_metadata_usize_slot, i.e. rewritten if the closure moved, deliberately not marked.
It is written only at microtasks.rs:749 (last_closure: step_closure as usize) and zeroed on any non-error step (microtasks.rs:754-757). The same-closure check that used to read it was deleted (microtasks.rs:710-717), so on the error path it is now a write-only stale closure address that survives arbitrarily many collections with nothing marking it and nothing pruning it.
That is the #8040 shape: the arena recycles the address, the recycled bytes are read as a GcHeader, and a coincidental GC_FLAG_FORWARDED byte makes the rewrite pass follow a garbage forwarding pointer. #8174 narrowed what can be followed out of such a header; it did not remove the dead key.
Fix: either drop the field (nothing reads it any more — check first) or clear it when the closure is dead, wired into DEAD_KEY_PRUNES in crates/perry-runtime/src/gc/dead_owner.rs with the GC_TYPE_CLOSURE-narrowed predicate the closure tables already use.
Declared as open_gap in scripts/gc_rekeyed_key_tables.json (two sites); closing this must delete both entries, or the gate fails on the stale exemptions.
Fixed in #8196 — by deleting the field, not by adding a prune.
AsyncStepGuard::last_closure held the address of the closure that took the last erroring async step, for a same-closure check that was deleted when #712/#921/#922 showed a runaway loop alternates between two closures (promise/microtasks.rs: "Drop the same-closure check"). Nothing has read it since.
It was not inert, though: it was a raw heap address that scan_promise_roots_mut and its budgeted twin rekeyed via visit_metadata_usize_slot — rewritten if the closure moved, deliberately not marked — with nothing pruning it when the closure died. Writing a prune to maintain state nobody reads would be its own dead code, so the field goes, and with it the PROMISE_SCAN_ASYNC_STEP_GUARD budgeted scan phase whose only slot it was.
Both of its scripts/gc_rekeyed_key_tables.json entries are deleted with it; the gate's MAX_OPEN_GAPS is 0.
Activity
fix(gc): give the six unpruned rekeyed tables a death story (#8190-#8195
proggeramlug commentedon Aug 16, 2026
Fixed in #8196 — by deleting the field, not by adding a prune.
AsyncStepGuard::last_closureheld the address of the closure that took the last erroring async step, for a same-closure check that was deleted when #712/#921/#922 showed a runaway loop alternates between two closures (promise/microtasks.rs: "Drop the same-closure check"). Nothing has read it since.It was not inert, though: it was a raw heap address that
scan_promise_roots_mutand its budgeted twin rekeyed viavisit_metadata_usize_slot— rewritten if the closure moved, deliberately not marked — with nothing pruning it when the closure died. Writing a prune to maintain state nobody reads would be its own dead code, so the field goes, and with it thePROMISE_SCAN_ASYNC_STEP_GUARDbudgeted scan phase whose only slot it was.Both of its
scripts/gc_rekeyed_key_tables.jsonentries are deleted with it; the gate'sMAX_OPEN_GAPSis 0.fix(gc): give the six unpruned rekeyed tables a death story (#8190-#8195
fix(gc): refuse a forwarding walk out of, or into, a non-object, and …