Repository navigation
GC: #7154's residual is NOT fixed — the loop-polls config is red 0/30, and stock zod alone fails 5/40 #7280
Description
#7154 was closed on a sfw-registry --help 30/30 result. That result could not be reproduced here, and the configuration a revert would ship is red.
The gate result
perry 4e9b1b7ea (origin/main, includes #7275):
| arm | result |
|---|---|
| default (no compile env, no run env) | clean 30/30 |
PERRY_GC_MOVING_LOOP_POLLS=1 compiled AND run — the config a revert ships |
red 0/30, TypeError: object is not a function |
POLLS=1 compiled, PERRY_GEN_GC=0 at run |
clean 10/10 |
| POLLS=1 compiled, polls not set at run | clean 10/10 |
| default binary, POLLS=1 at run only — the false-green trap | clean 10/10 |
evac_minor (default binary, HEAP_LIMIT=8 INCREMENTAL=0 CONSERVATIVE_STACK_SCAN=off) |
red 0/10, live (copied_objects=6084) |
Liveness asserted both ways: the revert arm ran 6 copying minors (copied_objects=50695…); the default arm ran 0. The result tracks collector mode exactly.
Note the fifth row. Setting POLLS=1 at run time against a default-compiled binary is clean 10/10 — because the poll codegen isn't in the binary. Anyone verifying that way gets a false green.
sfw-registry is not in this repo
Two independent sweeps found no source tree and no binary — it is a private checkout. So the 30/30 is neither confirmed nor refuted on that workload. It is refuted on a faithful substitute (86 modules, 8 endpoints × 12 cross-module defineApiCall(...) at module init, shaped after the frames in #7154's own disassembly) and on stock zod alone.
The library-only control is the important one — 20 lines, no scaffolding: import zod; build schema; safeParse ×96. 35/40 clean under the revert config; 40/40 on the shipped default. Failures print TypeError: value is not a function — #7154's exact string.
Instruction-level identification
Under #7196's quarantine (ZEAL=1 PROTECT_FROMSPACE=1 DEPTH=800):
FAULT: signal 10 ... RETIRED FROM-SPACE retired_by_minor=#18
last-known object: obj_type=2 size=72
2 perry_fn_node_modules_zod_src_v4_core_util_ts__clone + 1964
3 ...native_call_method::dispatch_handle
5 js_typed_feedback_native_call_method_by_id
#7154's final comment reports util_ts__clone + 1996, obj_type=2 size=72 — same function, same object type and size. The disassembly matches its prose exactly:
and x21, x8, #0xffffffffffff ; receiver -> bare register
bl _js_typed_feedback_observe_property_get ; observe() ALLOCATES
lsr x9, x21, #20 ; <-- reported pc
ldr x10, [x9, #0x10] ; STALE derefA comment that is too strong
policy.rs:1408 states the loop-polls route is "sound by construction" because it defers to a precise-root safepoint. That does not hold here. The stale value is a codegen-held bare register live across a runtime call that allocates — no safepoint's root set covers it. The comment should be corrected; it is the kind of claim that stops the next person looking.
Why CI did not catch it
gc-moving-witnesses on main for #7275 (run 30765414418) is green: loop_polls PASS 25/25, copy-minor 25/25 live. safepoint_minor — which the matrix describes as "what default becomes again when the stopgap lifts" — is in PR_ARMS and green.
So the gate ran, its subject was live, and it still cannot express this bug. The 25-file hand-written corpus is too small. A 20-line stock-zod program fails 5/40 while 25 curated files pass 25/25 — that gap is why the 30/30 number was believable. A dependency-scale witness is needed.
Asks
- Keep the stopgap. fix(gc): disable evacuating minor by default pending #7154 (use-after-free on dynamically-added fields) #7161 stays.
- Correct
policy.rs:1408. - Add a dependency-scale (real npm package) witness to the moving-GC corpus.
Activity
proggeramlug commentedon Aug 3, 2026
Sequencing warning before anyone starts this — see
docs/src/internals/rfc-rooting-by-construction.md("Interaction with the RSS goal", landed in #7292).Two findings change how this should be approached:
The −65% RSS (320 MB → 111 MB) does not come from the copying minor. It comes from the 16 MB nursery cap, which merely happens to be gated on the same flag:
effective_next_arena_triggergates it ongc_scavenge_enabled() || gc_moving_loop_polls_enabled(). A no-poll arm reaches the same 108 MB. So there is a route to the memory win that does not require the risky moving path at all.Do not measure the cost of collecting more often until the minor path actually works. The "~20× wall-clock" figure attached to the cap was measured while gc-matrix: four of the six PR-gating arms run ZERO copying minors corpus-wide (default/verify_evac/cons_scan_off at copy-minor 0/49; header still claims 12/22) #7255's defect was live — in the default configuration
gc_check_triggerfalls through to a direct minor behindManualGcScanGuard::force_full_scan(), i.e. a conservative full scan. Those were not minor collections. Minor-GC cost should scale with survivors, not with collection count, so a 20× penalty is a symptom, not a curve to sit on.⇒ Fix the minor path first, then re-derive these numbers. Measuring now would bake the full-scan cost into the record and make an RSS/speed tradeoff look inherent when it is a defect. That conclusion is why this is sequenced last in the plan, not because the RSS goal is deprioritised.
fix(gc): root optional parameters, and the instance in runtime-dispat…
fix(gc): root the spread-array accumulator across its elements (#7280)
fix(gc): root the namespace-import object across its member materiali…
fix(gc): root the parameter, the spread accumulator and the namespace…
proggeramlug commentedon Aug 3, 2026
Still red. #7161 cannot be reverted. Two independent efforts landed real fixes and the acceptance arm did not move:
POLLS=1compiled and runcopied_objects>0What was fixed (real, just not sufficient)
new(#7280) #7291, and independently fix(gc): root the parameter, the spread accumulator and the namespace object (#7280) #7299's dropped commit — verified equivalent by identical emitted prologue, samejs_shadow_frame_enter(4), same fourjs_shadow_slot_bind). zod'sclonehad frame size 2 withdef/paramsnot bound at all, so LLVM promotedparamsintod8— the exact register this issue's disassembly reports.Expr::ArraySpreadhad no rooting whatsoever.import * as nsas a value: 269 members for zod'score, zerojs_gc_temp_root_*beside 269 allocating stores.And the fault moved. After the parameter fix, the quarantine no longer faults at zod's
clone— it faults atjs_native_call_method + 580from a module-init body: a receiver stale in a runtime frame, which is #7249's blind spot (layer 3 indocs/engine-plan.md), not something codegen rooting can reach.Two findings that should redirect effort
1. 101 of 115 remaining violations are ONE unaddressed rule. #7284's checker fix took
--moving-onlyfrom 65 → 115 on the gate corpus. The distribution: 55 property-GET-helper windows, 36 ToPrimitive/js_number_coerce, 18js_closure_callN, 6 intra-corpus callees — but 101 of them are a single shape: a shadow-slot load cached in a register across a collection point. That is one measured change, not 101 fixes.2. The curated corpus is the wrong instrument. Running the same checker over the reproducer's own IR (86 modules, 65 MB) gives 77 fatal moving hits with a completely different distribution: 29
js_array_alloc → js_array_spread_append/concat, 14js_box_get_bits → js_closure_callN, 8js_object_alloc → js_object_set_field_by_name. The 25-file hand-written corpus does not represent real dependency code, which is the same gap this issue already records (25 curated files pass while 20 lines of stock zod fail).⇒ Point the gate at dependency-scale IR before fixing another instance.
fix(gc): measure the dependency-scale corpus, then close the rule tha…
GC rooting: measure the dependency-scale corpus, then close the rule …
fix(ci): the dependency-scale dominance corpus was vacuous too (#7460)
refactor(codegen): migrate lower_array_method.rs onto the Layer 1 roo…
refactor(codegen): migrate instance_misc1 + logical_collections + map…
refactor(codegen): migrate instance_misc1 + logical_collections + map…
refactor(codegen): migrate instance_misc1 + logical_collections + map…
refactor(codegen): migrate the object/array literal modules onto the …
fix(codegen): root Promise.try's argument accumulator and Array.fromA…
1 remaining item
refactor(codegen): Layer 1 rooting migration slice 6 — the multi-poin…
fix(gc): Layer 1 rooting slice 8 — the raw API becomes unreachable (#…
gc: Layer 1 rooting slice 8 — the raw API becomes unreachable (#7615) (…
proggeramlug commentedon Aug 9, 2026
Status on
8d27032c7: asks 1 and 2 are done, ask 3 is not, and the stock-zod control no longer reproducesAsk 2 is done
policy.rs:1780now reads:Ask 1 is honoured
PERRY_GC_MOVING_LOOP_POLLSis still compile-time and still default-off.Ask 3 is NOT done
gc-moving-witnesses.ymlstill drivesscripts/gc_repsel_matrix.sh --arms loop_polls --filter test_gap_gc_— the same 25-file hand-written corpus this issue says is too small, and the job assertsarms == [("loop_polls", "move")]so nothing else runs.A dependency-scale corpus does now exist —
scripts/gc_root_dominance_dep_corpus.sh(zod, ~90 native modules), wired atgc-root-dominance.yml:312. But that is a static IR corpus. There is still no dependency-scale RUNTIME witness on any moving-GC arm, which is what this ask was for.The stock-zod control does not fail any more
I could not use the original file (it is not in the repo), so I rebuilt it from this issue's description — "20 lines, no scaffolding: import zod; build schema; safeParse ×96" — and then, because the reported fault was in
zod/src/v4/core/util.ts::clone, built a second probe whose schema is made of the fluent modifiers that go throughclone(.optional(),.nullable(),.default()), since the first one never calls any of them.This issue measured 35/40 on that second row.
Liveness, because "40/40" on its own is exactly the false green this issue is about
PERRY_GC_ZEAL=1with the POLLS binary:So the poll codegen is in the binary (1139 back-edge polls fired), 1144 copying minors ran, and 43 799 objects actually moved — and the program is still correct. Under the pairing this repo names for #7154-class bugs,
PERRY_GC_ZEAL=1 PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=800, the plain probe is 5/5 clean with 1144 retired quarantine sets per run. The clone-heavy probe under the alloc-pressure route plus the quarantine is 0/5 faults with the quarantine live 5/5; under zeal it accumulated 38 915 retired sets before hitting my 900 s timeout, still with zero faults.What that does and does not mean
It means the specific stale deref this issue captured (
util_ts__clone + 1964,obj_type=2 size=72, viajs_typed_feedback_native_call_method_by_id) does not reproduce on a faithful reconstruction of its own control, under a configuration where the collector demonstrably moves tens of thousands of objects.It does not mean the revert is safe. My probes are reconstructions, and this issue's central argument — "a 20-line stock-zod program fails 5/40 while 25 curated files pass 25/25; that gap is why the 30/30 number was believable" — is untouched by two more probes that happen to pass. Ask 3 is the load-bearing one and it is still open, and it is now cheaper than when it was filed: the zod checkout the static dep-corpus script already depends on is exactly the input a runtime arm needs.
Separately, and relevant to how this issue is verified: the from-space quarantine's usual knob set now prints
retired_set=zero times on several workloads, because #7687 made allocation-point collections non-moving. Any arm added for ask 3 must assertretired_set=#per test, or it will report a clean run over a collector that never moved anything.fix(codegen): root a[i]++ / o.f++'s result across the write (#7628)
fix(codegen): root new's non-class ctor args and the computed-key win…
fix(codegen): the lowering-window cluster — push order, member update…
proggeramlug commentedon Aug 9, 2026
Closing — asks 1 and 2 are done and verified; ask 3 is refiled so the missing gate is tracked as work rather than as the tail of a mostly-closed ticket.
Done: the stopgap is kept, and the overclaiming comment is fixed.
Refiled: #7717 — the dependency-scale runtime witness for
gc-moving-witnesses.yml, which is still absent. That issue carries this one's key evidence (loop-polls red 0/30, stock zod alone failing 5/40) and, more importantly, the acceptance criteria that stop it shipping vacuous: it must prove the collector ran ([gc-zeal] forced_collections/copying_minors, exit 70 when either is zero), compile withPERRY_GC_MOVING_LOOP_POLLS=1, and use aPERRY_GC_PROTECT_FROMSPACE_DEPTHsized to real code rather than the default 4.That last point is not theoretical — today on #7528 I ran the from-space quarantine and it printed zero
retired_set=lines: no copying minor had run, so the instrument protected nothing while exiting clean. A witness that can do that is worse than no witness.fix(gc): close #7210's two remaining unrooted-alloca sites + #7640 A/…
fix(gc): close #7210's two remaining unrooted-alloca sites + #7640 A/…
fix(gc): close #7210's two remaining unrooted-alloca sites + #7640 A/…
fix(gc): close #7210's two remaining unrooted-alloca sites + #7640 A/…
gc: diagnostics, two rooting fixes, and the ungated corpus/lowering c…
fix(gc): address bot review followups from #7291, #7281, #7284 (#8257)