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
gc: zod dep-corpus under PERRY_GC_SCHEDULE_RATE=1 with the quarantine OFF dies with a rooting-shaped TypeError (seed 1, safepoint 3319) #7803
Found by #7793's witness during its merge audit, in the sabotage arm that turns the from-space quarantine OFF — i.e., this is a latent defect on main (v0.5.1458 runtime), not a defect of that PR, and the shipped witness configuration (quarantine ON) runs the same workload clean.
TypeError: Cannot read properties of undefined (reading 'toString')
at <anonymous>
[gc-schedule] done: seed=1 safepoints=3319 scheduled_collections=3319 polls_paced=23075 copying_minors=3319 moved_objects=385030 loop_polls=26394
exit 1, at safepoint 3,319 of the ~8,000 the full run performs. The workload is single-threaded, so the seeded schedule should replay exactly ((seed, counter) — one observation so far; the box was at load 20+ and a second confirmation run was cut off, so start by confirming determinism).
Why this smells like a #7154-class rooting bug and not an instrument bug
The same binary, same seed, same rate with PERRY_GC_PROTECT_FROMSPACE=1 (depth 800) passes: 8,022 copying minors, 852,411 objects moved, output byte-identical to the unscheduled run — verified twice.
The failure shape is the classic surfacing form: TypeError on an undefined that should be an object, cycles after the collection that lost it.
The quarantine changes recycling — retired from-space page-sets are held (and PROT_NONE'd) instead of being recycled into Eden. Two readings, both worth chasing:
the allocation/recycling dynamics under quarantine shift which safepoints land in the vulnerable window, so the bug's window simply never lines up in the protected arm (the protected arm also paced differently: 63,936 polls vs 26,394 at the failure point); or
less likely, a stale read that the quarantine should be catching is landing in memory the quarantine does not cover (same-cycle, pre-retirement).
If (1), PERRY_GC_SCHEDULE_SEED sweeps over the PROTECTED arm (scripts/gc_schedule_fuzz.sh) should eventually find a seed whose protected run faults precisely — that would hand us the exact stale address and retiring minor.
Context
The zod dependency corpus is exactly the population #7280 showed the curated fixtures don't reach (js_object_assign_one, js_new_function_construct). #7793's shipped witness gates the protected arm in CI; this issue tracks the unprotected finding it flushed out on its first run.
Still not reproducible — test-files/gc-dep-corpus/main.ts does not link. I confirmed the failure again on a build of origin/main (a769faf, PERRY_NO_AUTO_OPTIMIZE=1, PERRY_RUNTIME_DIR pinned): 45 distinct undefined _perry_fn_node_modules_zod_… symbols. The verdict on #7964 is genuine compiler gap (named re-export from an export * barrel emits a reference the barrel never defines), not a stale pin — details in that issue. So the blocker stands.
PR #7978 does not unblock it. That fix is runtime-side (Object.defineProperty's receiver / key / DescView rooting); this failure is in module lowering / codegen.
The candidate connection is now a little stronger, and still only a candidate. zod's descriptor traffic runs through both helpers in this family:
Object.defineProperty — the window fix(gc): root Object.defineProperty's receiver, key and descriptor fields across its own allocating calls #7978 fixes, where a pristine build faults on a retired from-space receiver (obj_type=2) under the quarantine, and where the stale receiver address is also the OWNER KEY of the per-property attribute/accessor side tables. A define filed under a dead address leaves the property invisible to every later read.
#7803's symptom — Cannot read properties of undefined (reading 'toString') — is exactly what either a stale key string or a stale receiver in those loops produces: the property is defined under a garbage name or a dead owner, and the later read misses.
Retest plan once #7964 lands: build the corpus, then run the two arms named in this issue (PERRY_GC_PROTECT_FROMSPACE=0 vs =1, seed 1, rate 1) against a build with #7978 in it and against one without. If #7978 closes it, the unprotected arm goes green; if not, the seed sweep over the protected arm (scripts/gc_schedule_fuzz.sh) is still the way to turn it into a precise fault.
Retested on bdfcba4a2 (v0.5.1499). Short version: the reproducer is stale, the subject is not. Please don't close.
The corpus was unrunnable all week (#7964); #7980 (fec46413d) fixed it and test-files/gc-dep-corpus/main.ts now compiles and links (exit 0, 29.9 MB), on
the corpus and zod@4.3.5 pin already in the tree — nothing regenerated or
re-pinned.
1. The filed reproducer no longer fails
PERRY_GC_SCHEDULE_SEED=1 PERRY_GC_SCHEDULE_RATE=1 PERRY_GC_PROTECT_FROMSPACE=0 PERRY_GC_DIAG=1
→ exit 0, 4 runs out of 4 (3 dedicated + seed 1 inside the sweep below), stdout byte-identical to the unscheduled run, with
the instrument live rather than assumed (6,627–6,909 copying minors, ~872k
objects moved, 63,936 loop polls per run).
2. …but the class is still live: 3 of 16 seeds fail
TypeError: Cannot read properties of undefined (reading 'issues')
626
Seed 16 is the same message shape as the one you filed. Seed 15 is a different
bug and is now #7990.
3. The seeded schedule does NOT replay — this changes how the issue should be read
You asked for determinism to be confirmed first. It does not hold: at a fixed
seed, loop_polls is stable (63,936) but safepoints / scheduled_collections
drift ~4% run to run (6627 → 6652 → 6909). Consequently every failure here is
intermittent:
seed
in the sweep
on re-run, same command
4
FAIL
pass, 2/2
15
FAIL
pass, 3/3
1 (this issue's)
pass
pass, 4/4
So §1 is a coin landing the other way, not a repair. The durable figure is the rate: ~19% of runs (3/16).
4. The candidate cause is refuted
gc-handoff/ROOTVEC-NOTES.md named #7962/#7978 (Object.defineProperties / Object.defineProperty rooting) as a candidate, explicitly unconfirmed. Sabotage
A/B — both fixes reverted underneath current main, archives verified to have
moved, relinked binary verified different:
Reverting them does not bring it back. They are not what closed the seed-1
window. (Tree restored and rebuilt afterwards, not git checkout-ed.)
The workload is not the variable either: test-files/gc-dep-corpus/ is untouched
since #7311 and the installed zod is 4.3.5 throughout, so the only delta between
v0.5.1458 and v0.5.1499 is compiler + runtime.
5. Your hypothesis (1) is confirmed on a second seed
Seed 4 fails unprotected at safepoint 738; the same seed with the quarantine on
at depth 800 passes, running 6,888 copying minors, with the instrument
saturated (sets_held=800/800 bytes_protected=2095054848, 6,888 retired sets).
Holding retired from-space pages out of Eden changes which addresses get
recycled and the window stops lining up. The protected arm is therefore not
evidence of health here, and scripts/gc_dep_scale_witness.sh (quarantine ON)
structurally cannot catch this window on this workload.
I ran your suggested way out — a seed sweep over the protected arm — and it
has not produced a protected-arm fault so far, so that route did not pay off in
this session.
Suggested disposition
Keep open; retitle away from "seed 1, safepoint 3319" and toward the rate, or
close in favour of a fresh issue quoting §2. Full audit trail, including the
things that did not work, is in gc-handoff/ZOD-NOTES.md (#7989).
Unrelated footgun found on the way: PERRY_GC_DIAG=0 enables diagnostics
(telemetry.rs:11 uses var_os(...).is_some()), which silently collapsed one of
my A/B arms. PERRY_GC_PROTECT_FROMSPACE does not have this bug, so the =0 in
this issue's repro really is off.
Closing with causal evidence, not because the old seed-1 reproducer merely stopped firing.
The surviving population was localized to comparison operands in the zod new-Function/dyn_eval path. #8011 (main commit 0543e87) roots the left comparison operand across evaluation of the allocating right operand. Its causal A/B moved from 3 failures in 16 saturated quarantine-off runs to 26/26 passes after the fix, each under PERRY_GC_SCHEDULE_RATE=1 with roughly 6,400 copying minors and ~855k moved objects—so the stress subject was live, not inferred.
#8021 (main commit 410dadd) then pinned the exact typed-Map comparison shape as strict_eq_rereads_a_map_operand_below_an_allocating_right_operand; current main a9a99d8 contains that emitted-IR regression and the registered corpus witnesses.
This also accounts for the incoherent Map header reported separately in #7990. The old fixed-seed wording remains historically stale because schedule counts drift, but the defect class is now causally fixed and guarded.
Activity
docs(gc-handoff): record the #7949 validation results and the #7803 f…
fix(runtime): root JS values retained in Rust containers across alloc…
docs(gc): record the #7963 acceptance corpus and the #7964/#7803 verd…
proggeramlug commentedon Aug 12, 2026
Status after working #7963 / #7964 (PR #7978):
Still not reproducible —
test-files/gc-dep-corpus/main.tsdoes not link. I confirmed the failure again on a build oforigin/main(a769faf,PERRY_NO_AUTO_OPTIMIZE=1,PERRY_RUNTIME_DIRpinned): 45 distinct undefined_perry_fn_node_modules_zod_…symbols. The verdict on #7964 is genuine compiler gap (named re-export from anexport *barrel emits a reference the barrel never defines), not a stale pin — details in that issue. So the blocker stands.PR #7978 does not unblock it. That fix is runtime-side (
Object.defineProperty's receiver / key /DescViewrooting); this failure is in module lowering / codegen.The candidate connection is now a little stronger, and still only a candidate. zod's descriptor traffic runs through both helpers in this family:
Object.definePropertiesatsrc/v4/core/util.ts:316andsrc/v4/classic/errors.ts:28— the helper runtime: root JSValues retained in Rust containers across allocations #7949/fix(runtime): root JS values retained in Rust containers across allocations (#7949) #7962 fixed;Object.defineProperty— the window fix(gc): root Object.defineProperty's receiver, key and descriptor fields across its own allocating calls #7978 fixes, where a pristine build faults on a retired from-space receiver (obj_type=2) under the quarantine, and where the stale receiver address is also the OWNER KEY of the per-property attribute/accessor side tables. A define filed under a dead address leaves the property invisible to every later read.#7803's symptom —
Cannot read properties of undefined (reading 'toString')— is exactly what either a stale key string or a stale receiver in those loops produces: the property is defined under a garbage name or a dead owner, and the later read misses.Retest plan once #7964 lands: build the corpus, then run the two arms named in this issue (
PERRY_GC_PROTECT_FROMSPACE=0vs=1, seed 1, rate 1) against a build with #7978 in it and against one without. If #7978 closes it, the unprotected arm goes green; if not, the seed sweep over the protected arm (scripts/gc_schedule_fuzz.sh) is still the way to turn it into a precise fault.fix(gc): root Object.defineProperty's receiver, key and descriptor fi…
proggeramlug commentedon Aug 12, 2026
Retested on
bdfcba4a2(v0.5.1499). Short version: the reproducer is stale, the subject is not. Please don't close.The corpus was unrunnable all week (#7964); #7980 (
fec46413d) fixed it andtest-files/gc-dep-corpus/main.tsnow compiles and links (exit 0, 29.9 MB), onthe corpus and
zod@4.3.5pin already in the tree — nothing regenerated orre-pinned.
1. The filed reproducer no longer fails
PERRY_GC_SCHEDULE_SEED=1 PERRY_GC_SCHEDULE_RATE=1 PERRY_GC_PROTECT_FROMSPACE=0 PERRY_GC_DIAG=1→ exit 0, 4 runs out of 4 (3 dedicated + seed 1 inside the sweep below), stdout byte-identical to the unscheduled run, with
the instrument live rather than assumed (6,627–6,909 copying minors, ~872k
objects moved, 63,936 loop polls per run).
2. …but the class is still live: 3 of 16 seeds fail
RATE=1 TIMEOUT=1800 PERRY_GC_PROTECT_FROMSPACE=0 PERRY_GC_DIAG=1 scripts/gc_schedule_fuzz.sh /tmp/w 16TypeError: value is not a function[gc-pin-latch] FATALTypeError: Cannot read properties of undefined (reading 'issues')Seed 16 is the same message shape as the one you filed. Seed 15 is a different
bug and is now #7990.
3. The seeded schedule does NOT replay — this changes how the issue should be read
You asked for determinism to be confirmed first. It does not hold: at a fixed
seed,
loop_pollsis stable (63,936) butsafepoints/scheduled_collectionsdrift ~4% run to run (6627 → 6652 → 6909). Consequently every failure here is
intermittent:
So §1 is a coin landing the other way, not a repair. The durable figure is the
rate: ~19% of runs (3/16).
4. The candidate cause is refuted
gc-handoff/ROOTVEC-NOTES.mdnamed #7962/#7978 (Object.defineProperties/Object.definePropertyrooting) as a candidate, explicitly unconfirmed. SabotageA/B — both fixes reverted underneath current
main, archives verified to havemoved, relinked binary verified different:
mainReverting them does not bring it back. They are not what closed the seed-1
window. (Tree restored and rebuilt afterwards, not
git checkout-ed.)The workload is not the variable either:
test-files/gc-dep-corpus/is untouchedsince #7311 and the installed zod is 4.3.5 throughout, so the only delta between
v0.5.1458 and v0.5.1499 is compiler + runtime.
5. Your hypothesis (1) is confirmed on a second seed
Seed 4 fails unprotected at safepoint 738; the same seed with the quarantine on
at depth 800 passes, running 6,888 copying minors, with the instrument
saturated (
sets_held=800/800 bytes_protected=2095054848, 6,888 retired sets).Holding retired from-space pages out of Eden changes which addresses get
recycled and the window stops lining up. The protected arm is therefore not
evidence of health here, and
scripts/gc_dep_scale_witness.sh(quarantine ON)structurally cannot catch this window on this workload.
I ran your suggested way out — a seed sweep over the protected arm — and it
has not produced a protected-arm fault so far, so that route did not pay off in
this session.
Suggested disposition
Keep open; retitle away from "seed 1, safepoint 3319" and toward the rate, or
close in favour of a fresh issue quoting §2. Full audit trail, including the
things that did not work, is in
gc-handoff/ZOD-NOTES.md(#7989).Unrelated footgun found on the way:
PERRY_GC_DIAG=0enables diagnostics(
telemetry.rs:11usesvar_os(...).is_some()), which silently collapsed one ofmy A/B arms.
PERRY_GC_PROTECT_FROMSPACEdoes not have this bug, so the=0inthis issue's repro really is off.
docs(gc-handoff): triage #7803 on a corpus that links again (#7989)
fix(gc): parse PERRY_GC_DIAG by value — `=0` no longer ENABLES diagno…
proggeramlug commentedon Aug 13, 2026
Closing with causal evidence, not because the old seed-1 reproducer merely stopped firing.
The surviving population was localized to comparison operands in the zod new-Function/dyn_eval path. #8011 (main commit 0543e87) roots the left comparison operand across evaluation of the allocating right operand. Its causal A/B moved from 3 failures in 16 saturated quarantine-off runs to 26/26 passes after the fix, each under PERRY_GC_SCHEDULE_RATE=1 with roughly 6,400 copying minors and ~855k moved objects—so the stress subject was live, not inferred.
#8021 (main commit 410dadd) then pinned the exact typed-Map comparison shape as
strict_eq_rereads_a_map_operand_below_an_allocating_right_operand; current main a9a99d8 contains that emitted-IR regression and the registered corpus witnesses.This also accounts for the incoherent Map header reported separately in #7990. The old fixed-seed wording remains historically stale because schedule counts drift, but the defect class is now causally fixed and guarded.
docs(gc-handoff): RATE=1 unpaced seed search cannot distinguish seeds (…
feat(gc): name the copying walk in the pin-latch abort (#7803)
docs(gc-handoff): seed 3 fails 2/2 under RATE=0.1 ALLOC_KB=0 (#7803)
docs(gc-handoff): seed 3 is the #7803 reproducer (3/3 fail)
8 remaining items
feat(gc): fromspace-scan abort dumps the owner object; drain-promotio…
fix(gc): fromspace scan skips array capacity slack; pin-latch identif…
fix(gc): carry RS4GC derived pointers through the compact map — gc_ma…
docs(gc-handoff): §39 — #7803 named and fixed: v3 map collapsed deriv…
feat(gc): PERRY_GC_THIS_SET_CHECK — trap the producer of an incoheren…
feat(gc): this-set trap grades both directions (slot vs cell corrupti…
feat(gc): PERRY_GC_NATIVE_SLOT_VERIFY — abort on the cycle that creat…
feat(gc): native-slot verifier reports cycle kind and target space (#…
feat(gc): native-slot verifier compares against the rewrite walk's st…
feat(gc): native-slot verifier prints the collector's own classificat…
feat(gc): native-slot verifier dumps the target's raw header (#7803)
feat(gc): native-slot verifier dumps every matched record and slot va…
fix(gc): unbreak cargo-test and clear the lint gates for #7803
gc: diagnostics, two rooting fixes, and the ungated corpus/lowering c…
fix(gc): a doc comment on a macro invocation fails -D warnings (#8181)