Repository navigation
fix(gc): CannotCollect helpers can collect when a GcRootRegistryGuard drop flushes a deferred GC request — enforce or remove #11523
Description
Problem
Several helpers are classified CannotCollect in crates/perry-codegen/src/gc_call_effects.rs (for example js_gc_note_slot_layout / _aware ~L140, the js_typed_feedback_* record/observe/guard helpers ~L150–182, and js_closure_set_capture_bits / _ptr ~L235), so lower_roots_for_rs4gc marks their calls "gc-leaf-function" and RS4GC relocates nothing across them.
But some of these take a GcRootRegistryGuard (crates/perry-runtime/src/gc/roots.rs ~L129–185). Its Drop calls exit_gc_root_lock(), and when the lock depth returns to 0, that calls flush_deferred_gc_request() (crates/perry-runtime/src/gc/policy.rs ~L1614). That runs a collection (gc_check_trigger, a direct minor, or a full collection) if a request was deferred while the lock was held. So "cannot collect" holds only if nothing inside the locked region can raise a deferred GC request, i.e. nothing allocates or triggers while holding the lock. That invariant isn't written down or enforced. A future edit adding an allocation inside such a region would silently turn a gc-leaf call into a collecting one, and the caller's unrelocated pointers would go stale.
Fix
Pick one, and document it next to the classification:
- Enforce the invariant: add a debug assertion, active in the
gcauditprofile (note thatdebug_assert!is compiled out of release and perry-dev), that no deferred GC request is raised while aGcRootRegistryGuardis held from within aCannotCollecthelper. Or assert atflush_deferred_gc_requesttime that the flush didn't come from such a helper. - Or have
CannotCollecthelpers never flush on guard drop: keep the request pending for the next real safepoint, e.g. a guard variant that doesn't flush. - Reclassify any helper where the flush can genuinely fire.
List exactly which CannotCollect / AllocNoReentry helpers take a GcRootRegistryGuard (or call something that does), with the audit result for each, in the PR.
Verify
- A test that plants an allocation inside the locked region of one such helper (test-only hook) and shows the chosen enforcement catches it (assertion fires, or the request is deferred, not flushed). Sabotage-check it.
- The GC suite (
RUST_TEST_THREADS=1 cargo test --release -p perry-runtime --lib gc::) andcargo test --release -p perry-codegenpass.
Rules for the PR (repo conventions)
- Code + tests + a
changelog.d/<PR>-<slug>.mdfragment. No version bump (don't touch[workspace.package].version, theCLAUDE.mdversion line, orCargo.lockversions). - Every new regression test must be sabotage-checked: revert the fix, confirm the test goes red, restore. Say so in the PR body.
perry-runtimetests must run withRUST_TEST_THREADS=1. Build-p perry -p perry-runtime-static -p perry-stdlib-statictogether (the.aarchives come from the-staticwrappers).- Run
scripts/run_lint_gates.sh(or at leastcargo fmt --all -- --check,scripts/check_file_size.sh,python3 scripts/raw_handle_debt.py,python3 scripts/addr_class_inventory.py,python3 scripts/gc_runtime_root_holders.py,python3 scripts/gc_rekeyed_key_tables.py). - Self-contained: no private bundle or special host needed. A normal Linux or macOS dev box is enough.
Filed from the 2026-09 side-table / RSS audit; line numbers are against main e379a7a and may drift.
Activity
fix(gc): CannotCollect helpers' registry lock never flushes a deferre…
fix(gc): CannotCollect helpers' registry lock never flushes a deferre…
proggeramlug commentedon Sep 27, 2026
Status (S1, #11565): this is not fixed at the source yet; codegen now classifies these helpers conservatively.
On Linux, macOS and Windows, the call graph shows 23 former
CannotCollecthelpers reachingflush_deferred_gc_requestthrough aGcRootRegistryGuarddrop:js_gc_note_slot_layout[_aware]js_gc_key_add_layout_unknownjs_gc_{init,declare}_typed_shape_layoutjs_typed_feedback_*record/observe/guard helpersjs_packed_arraylike_loop_revalidate_livejs_array_{clear_numeric_layout,note_numeric_write,declare_all_pointer_elements}js_closure_set_capture_bits/_set_box_capture_ptr/_set_capture_ptrThey are now
AllocOnlyin the generated table, which makes them statepoints under today's runtime. That is the sound choice.Cost, measured on benchmarks/suite plus 60 gap tests: these demotions add about 99k of 410k relocations;
js_gc_note_slot_layoutalone adds 74k. Option 2 of this issue (a guard drop only sets the pending flag, RFC S3 arm D) would let the next table regeneration return all of them toLeaf.fix(gc): make the #11523 non-collecting root guard a distinct type so…
gc(effects): regenerate tables at the new base; #11523 helpers are pr…
proggeramlug commentedon Sep 27, 2026
Update (#11565): the fix on main (
lock_gc_root_registry_noncollecting) is correct at runtime, but it chose the arm with a runtime flag. As a result, everyGcRootRegistryGuarddrop still contained the flushing branch, and the call-graph checker still proved all 23 helpers collecting.#11565 makes the non-collecting guard a distinct type,
NonCollectingRootRegistryGuard, whose drop has no path toflush_deferred_gc_request. With that change, 17 of the 23 helpers are provablyLeafin the generated table. The other 6 reach JS or a materializing allocator on their own paths. Thegc::runtime suite passes (1504 tests).fix(gc): make the #11523 non-collecting root guard a distinct type so…
gc(effects): regenerate tables at the new base; #11523 helpers are pr…
fix(gc): make the #11523 non-collecting root guard a distinct type so…
gc(effects): regenerate tables at the new base; #11523 helpers are pr…
fix(gc): make the #11523 non-collecting root guard a distinct type so…
gc(effects): regenerate tables at the new base; #11523 helpers are pr…
fix(gc): make the #11523 non-collecting root guard a distinct type so…
gc(effects): regenerate tables at the new base; #11523 helpers are pr…
fix(gc): make the #11523 non-collecting root guard a distinct type so…
gc(effects): regenerate tables at the new base; #11523 helpers are pr…
fix(gc): make the #11523 non-collecting root guard a distinct type so…
gc(effects): regenerate tables at the new base; #11523 helpers are pr…