Summary
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-990 does assign_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 — before lower_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.
Refs #7202, #7154, #7161, #7207, #6968.
Summary
The
catch (e)parameter has a shadow slot reserved for it and never bound, so the exception object — almost always a heapError— has no root at all for the whole catch body.This is the sharpest single finding of the
#7202bare-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_slotfor the catch parameter (typedAny), sojs_shadow_frame_enter's count includes it.stmt/try_stmt.rs:114allocates the parameter slot withalloca_entryand never emitsjs_shadow_slot_bind, soactive[idx]stays false forever and the collector never dereferences it.stmt/try_stmt.rs:107callsjs_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(thecons_scan_offmatrix 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 throughexpr::scalar_slot_root::root_entry_alloca(generalized in #7207), whose contract also covers theundefinedseed.A reproducer:
try { throw new Error(...) } catch (e) { churn(); use(e.message) }in a loop, underPERRY_GC_MOVING_LOOP_POLLS=1withPERRY_CONSERVATIVE_STACK_SCAN=off.Refs #7202, #7154, #7161, #7207, #6968.