Skip to content

gc: the raw-handle ratchet cannot see unrooted locals in perry-stdlib / perry-ext-* — 90% of that surface never adopted the rooting API #8233

Description

@proggeramlug

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.py is the repo's instrument for "a raw heap pointer copy held across a collection point". It has two properties that compound:

SRC = ROOT / "crates" / "perry-runtime" / "src"
PAT = re.compile(r"\.get_raw_(?:mut|const)_ptr\b")
  1. Scope is perry-runtime only. perry-stdlib and every perry-ext-* crate are outside it.
  2. 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

Sampled two files, both hit:

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 as i64)); // 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).
  • The Next App Route fixture's cold-start loop runs zero copying minors in normal mode (measured, test(next): add the warm-process soak arm #8163's residual needs #8215), so end-to-end coverage would not surface it either.

What would close this

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions