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
Scope is perry-runtime only.perry-stdlib and every perry-ext-* crate are outside it.
It counts bare reads out of a RuntimeHandle — i.e. it measures debt in code that has already adopted the rooting API and then degraded a use. Code that never roots at all has no get_raw_*_ptr to count and scores zero, the ratchet's best possible result.
So a file holding four unrooted heap pointers with no RuntimeHandleScope anywhere is indistinguishable, to this instrument, from a perfectly rooted one. Its own docstring is careful to call it "a DEBT COUNTER, not a soundness proof" — this issue is not that it overclaims, it is that its denominator excludes the surface where the bugs are being found.
Scale
Files under crates/perry-stdlib/src and crates/perry-ext-*/src that call an allocator (js_string_from_bytes, js_array_alloc, js_object_alloc, js_closure_alloc, alloc_string, alloc_buffer, js_array_push*) or invoke JS (js_closure_call*):
files that allocate or call JS: 177
of those, with NO rooting-API usage at all: 161 (90%)
Top density, zero rooting: streams.rs (72), crypto/sign.rs (66), lodash.rs (57), mysql2/result.rs (51), ioredis.rs (50), perry-ext-fetch/lib.rs (50), perry-ext-events/lib.rs (40).
This is an exposure surface, not a bug count — most of those sites allocate and return without holding anything across a second collection point, which is fine. The number says how much of the surface no instrument is watching, not how much is broken.
Two independent confirmations that the shape is really there
events/events_on.rs — worse, because it persists the corruption:
let state = js_array_alloc(6);let buffer = js_array_alloc(0);// may move `state`let pending = js_array_alloc(0);// may move `state` and `buffer`let _ = js_array_push_f64(state,js_nanbox_pointer(buffer asi64));// stale `state`, and stores stale `buffer`
The push writes a pre-move address into the heap, so the damage outlives the frame rather than being a transient misread.
Why the other instruments don't cover it either
scripts/gc_root_dominance_check.py reads emitted LLVM IR — structurally blind to Rust runtime locals (CLAUDE.md says so directly).
scripts/gc_runtime_root_holders.py enumerates static declarations — these are stack locals, and its scope is runtime+stdlib only (ext crates are named in its "cannot see" list).
Not "widen SRC" — these files have no get_raw_*_ptr to count, so widening the scope adds zero. It needs an instrument that detects the shape: a local bound from an allocator return (or a raw pointer extracted from a NaN-box) that is still used after a subsequent call which can allocate or run JS. A line-order heuristic over Rust source would catch all seven sites above; it will need an allowlist and a --self-test like the repo's other ratchets, and it should start as a report before it becomes a gate (CLAUDE.md: a new gate has never been green).
Filing rather than building because the scale — potentially hundreds of sites across two crate families — makes it a campaign that wants an owner's priority call, not a drive-by.
Found while filing #8217. That issue is four instances; this is the reason nothing was going to catch them, or the next four.
The gap
scripts/raw_handle_debt.pyis the repo's instrument for "a raw heap pointer copy held across a collection point". It has two properties that compound:perry-runtimeonly.perry-stdliband everyperry-ext-*crate are outside it.RuntimeHandle— i.e. it measures debt in code that has already adopted the rooting API and then degraded a use. Code that never roots at all has noget_raw_*_ptrto count and scores zero, the ratchet's best possible result.So a file holding four unrooted heap pointers with no
RuntimeHandleScopeanywhere is indistinguishable, to this instrument, from a perfectly rooted one. Its own docstring is careful to call it "a DEBT COUNTER, not a soundness proof" — this issue is not that it overclaims, it is that its denominator excludes the surface where the bugs are being found.Scale
Files under
crates/perry-stdlib/srcandcrates/perry-ext-*/srcthat call an allocator (js_string_from_bytes,js_array_alloc,js_object_alloc,js_closure_alloc,alloc_string,alloc_buffer,js_array_push*) or invoke JS (js_closure_call*):Top density, zero rooting:
streams.rs(72),crypto/sign.rs(66),lodash.rs(57),mysql2/result.rs(51),ioredis.rs(50),perry-ext-fetch/lib.rs(50),perry-ext-events/lib.rs(40).This is an exposure surface, not a bug count — most of those sites allocate and return without holding anything across a second collection point, which is fine. The number says how much of the surface no instrument is watching, not how much is broken.
Two independent confirmations that the shape is really there
Sampled two files, both hit:
fetch/headers.rs(gc: js_headers_for_each/keys/values/entries hold heap pointers across allocations and a JS callback #8217) — four sites, including a bare*const ClosureHeaderreused acrossjs_closure_call2in a loop.events/events_on.rs— worse, because it persists the corruption:The push writes a pre-move address into the heap, so the damage outlives the frame rather than being a transient misread.
Why the other instruments don't cover it either
scripts/gc_root_dominance_check.pyreads emitted LLVM IR — structurally blind to Rust runtime locals (CLAUDE.md says so directly).scripts/gc_runtime_root_holders.pyenumeratesstaticdeclarations — these are stack locals, and its scope is runtime+stdlib only (ext crates are named in its "cannot see" list).What would close this
Not "widen
SRC" — these files have noget_raw_*_ptrto count, so widening the scope adds zero. It needs an instrument that detects the shape: a local bound from an allocator return (or a raw pointer extracted from a NaN-box) that is still used after a subsequent call which can allocate or run JS. A line-order heuristic over Rust source would catch all seven sites above; it will need an allowlist and a--self-testlike the repo's other ratchets, and it should start as a report before it becomes a gate (CLAUDE.md: a new gate has never been green).Filing rather than building because the scale — potentially hundreds of sites across two crate families — makes it a campaign that wants an owner's priority call, not a drive-by.