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: the catch (e) parameter slot is reserved but never bound — the exception has no root across the catch body #7209
The catch (e) parameter has a shadow slot reserved for it and never bound, so the exception object — almost always a heap Error — has no root at all for the whole catch body.
This is the sharpest single finding of the #7202 bare-alloca enumeration (PR #7207), because the frame is already sized for it: the fix is a bind, not a reservation.
Evidence
collectors/pointer_locals.rs:983-990doesassign_slot for the catch parameter (typed Any), so js_shadow_frame_enter's count includes it.
stmt/try_stmt.rs:114 allocates the parameter slot with alloca_entry and never emits js_shadow_slot_bind, so active[idx] stays false forever and the collector never dereferences it.
stmt/try_stmt.rs:107 calls js_clear_exception() — dropping the runtime's own reference — beforelower_stmts(&clause.body) runs arbitrary allocating user code.
So between the clear and the first re-read, the only thing referring to the exception is an alloca nothing scans. Under a non-moving collector the conservative native-stack scan finds it; with PERRY_CONSERVATIVE_STACK_SCAN=off (the cons_scan_off matrix arm, and the direction the collector is moving) it is a live use-after-free, and under an evacuating minor it is a stale address.
Fix shape
Bind the existing reserved index — js_shadow_slot_bind(idx, catch_slot) right after the store — or route it through expr::scalar_slot_root::root_entry_alloca (generalized in #7207), whose contract also covers the undefined seed.
A reproducer: try { throw new Error(...) } catch (e) { churn(); use(e.message) } in a loop, under PERRY_GC_MOVING_LOOP_POLLS=1 with PERRY_CONSERVATIVE_STACK_SCAN=off.
Red at origin/main1679e22b4, compiled and run with PERRY_GC_MOVING_LOOP_POLLS=1: bad churn 0 message 8 field 8 of 400 iterations, deterministic 3/3 — both e.message and a field stored on the error read wrong after an allocating call in the catch body. Green 5/5 with the fix, 3/3 on the evacuating arm, 3/3 under the shipped default, byte-exact against Node 26.5.1.
The fix is emit_shadow_slot_bind_for_local, which binds the index collect_pointer_typed_locals had already reserved — so the frame does not grow; it simply stops being sized for a root that did not exist. Emitted after the store rather than hoisted to entry setup, because on the non-throwing path the alloca is never written and an entry-hoisted bind would make the slot active over uninitialized stack bytes.
Fixed by #7216 (dd83c7e92) with emit_shadow_slot_bind_for_local, which reuses the already-reserved index rather than growing the frame, emitted after the store rather than hoisted to entry setup (on the non-throwing path the alloca is never written, so an entry-hoisted bind would make the slot active over uninitialized stack bytes).
Your framing was right and is worth preserving: this was sharper than an ordinary missing root because js_clear_exception() drops the runtime's own reference two lines earlier, so the exception was reachable only through unrooted stack memory while arbitrary user code ran — a precise-roots collection could sweep it, not merely relocate it.
Evidence
test-files/test_gap_gc_catch_param_rooting.ts (shipped with #7216), compiled with no compile-time GC env, run on the allocation-point arm:
bad churn 0 message 0 field 0, 10/10, byte-exact vs node 26.5.1
Movement confirmed: 6 copying minors, 1 614 objects copied. Note this is the one witness that can also fail under a non-moving precise-roots collection (the sweep case), which is why it belongs on cons_scan_off and not only on the requires=move arms.
Activity
proggeramlug commentedon Aug 1, 2026
Reproduced and fixed in #7216.
Red at
origin/main1679e22b4, compiled and run withPERRY_GC_MOVING_LOOP_POLLS=1:bad churn 0 message 8 field 8of 400 iterations, deterministic 3/3 — bothe.messageand a field stored on the error read wrong after an allocating call in the catch body. Green 5/5 with the fix, 3/3 on the evacuating arm, 3/3 under the shipped default, byte-exact against Node 26.5.1.The fix is
emit_shadow_slot_bind_for_local, which binds the indexcollect_pointer_typed_localshad already reserved — so the frame does not grow; it simply stops being sized for a root that did not exist. Emitted after the store rather than hoisted to entry setup, because on the non-throwing path the alloca is never written and an entry-hoisted bind would make the slot active over uninitialized stack bytes.fix(gc): bind the catch parameter and the closure this/new.target slo…
ci(gc): make the moving-GC stale-root witnesses able to fail (+ #7205) (
proggeramlug commentedon Aug 2, 2026
Re-verified on
main=d4ac65f21. Fixed — closing.Fixed by #7216 (
dd83c7e92) withemit_shadow_slot_bind_for_local, which reuses the already-reserved index rather than growing the frame, emitted after the store rather than hoisted to entry setup (on the non-throwing path the alloca is never written, so an entry-hoisted bind would make the slot active over uninitialized stack bytes).Your framing was right and is worth preserving: this was sharper than an ordinary missing root because
js_clear_exception()drops the runtime's own reference two lines earlier, so the exception was reachable only through unrooted stack memory while arbitrary user code ran — a precise-roots collection could sweep it, not merely relocate it.Evidence
test-files/test_gap_gc_catch_param_rooting.ts(shipped with #7216), compiled with no compile-time GC env, run on the allocation-point arm:1679e22b4message 8 field 8of 400, 3/3 deterministicd4ac65f21bad churn 0 message 0 field 0, 10/10, byte-exact vs node 26.5.1Movement confirmed: 6 copying minors, 1 614 objects copied. Note this is the one witness that can also fail under a non-moving precise-roots collection (the sweep case), which is why it belongs on
cons_scan_offand not only on therequires=movearms.Part of the sweep in #7262.