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
perry-ui-* callback tables hold NaN-boxed JS closures with no GC scanner #8701
Every perry-ui-* crate caches JS callbacks as raw NaN-boxed f64 in thread-local side tables, and no UI crate registers a gc_register_mutable_root_scanner. The ledger currently carries 466 perry-ui-* frontier entries of this shape.
A stored callback is a heap pointer held across arbitrary time, including across collections. Two independent failure modes:
Not marked — the collector can reclaim a listener that is still reachable only from the UI table, so a later invoke_listener calls freed memory.
Not rewritten — after an evacuating minor the cached pointer still points into from-space. Per docs/src/internals/gc-rooting-invariant.md this surfaces cycles later as TypeError: value is not a function, far from the cause.
This is the class CLAUDE.md describes as invisible to the static checker: scripts/gc_root_dominance_check.py reads emitted LLVM IR, so a side table holding a heap pointer is structurally out of its reach. The runtime instruments (PERRY_GC_PROTECT_FROMSPACE, PERRY_GC_SCHEDULE_SEED) are the only detectors.
Note the reproducibility tell from the same doc: an unrooted table goes bad at collection #0 and stays bad, unlike an unrooted register which is intermittent.
Suggested shape
Register one scanner per UI crate covering its callback tables, in the manner of gc/roots.rs's existing gc_register_mutable_root_scanner users, then move the corresponding frontier entries to holders with a real verdict. The frontier entries are the work list.
Activity
fix(ui): root persistent JavaScript callbacks; complete the C-ABI FFI…