PERRY_GC_ZEAL=1 and PERRY_GC_VERIFY_EVACUATION=1 are both shipped knobs, and no CI arm sets them together over the representation corpus. Setting them together finds something on the first file that was tried.
Repro
test_gap_repsel_p4a3_ptr_numarray, compiled and run with back-edge safepoint polls, at origin/main = 64c1f56fb:
PERRY_GC_MOVING_LOOP_POLLS=1 perry test-files/test_gap_repsel_p4a3_ptr_numarray.ts -o bin
PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off \
PERRY_GC_MOVING_LOOP_POLLS=1 PERRY_GC_ZEAL=1 PERRY_GC_VERIFY_EVACUATION=1 ./bin
thread '<unnamed>' panicked at crates/perry-runtime/src/gc/verify.rs:59:5:
gc evacuation verification failed: stale forwarded pointer in shadow stack roots:
slot=0x16d72ea48 old=0x7ffd0437f5914878 forwarded_to=0x7ffd0437f59148c8
10/10, deterministic. old is a POINTER_TAG NaN-box (0x7ffd); the object moved 0x50 bytes and a shadow-stack slot still holds the pre-move address after the rewrite pass ran.
Adding PERRY_GC_PROTECT_FROMSPACE=1 instead of the verifier gives a hard fault at the use rather than at the check, also 10/10:
[gc-fromspace-protect] FAULT: signal 10 at 0x33a1e140001
block=0x33a1e140000 +1 retired_bytes=144 retired_by_minor=#14072
last-known object: user_ptr=0x33a1e140008 obj_type=1 size=144
Why this is probably a dead slot, and why it still matters
Three measurements say the mutator is not reading it:
- the same binary under the same zeal without the verifier is byte-exact vs node 26.5.1, 10/10, across 14 373 evacuating minors and 9 485 copied objects. A live stale read would not survive that;
[gc-fromspace-scan clean] … missing_rewrites=0 dangling=0 owners=0 on every cycle — PERRY_GC_FROMSPACE_SCAN_ABORT=1 never fires;
- the failure surfaces only when the verifier or the from-space quarantine looks, never in program output.
So the most likely reading is a shadow-stack slot whose value is no longer live but was not rewritten — the verifier is stricter than the collector needs to be. That is still worth resolving, in one of two directions, because the current state makes an instrument unusable:
- if the slot is genuinely dead, the verifier needs to know that (or the frame needs to clear it), otherwise
PERRY_GC_VERIFY_EVACUATION=1 cannot be combined with zeal on real code — and zeal is precisely the configuration you reach for when hunting a rooting bug;
- if it is not dead, this is a missed rewrite on the shadow stack, which is the highest-severity class this campaign has.
Note the adjacent, opposite-signed finding in #7035 (the verifier reporting clean on a demonstrable missing rewrite because it only self-checks the rewrite pass's own enumeration). A verifier with both a false-negative mode and a false-positive mode is not yet a trustworthy oracle.
Scope, honestly
Suggested first step: sweep the remaining 45 corpus files under this arm to size the population before deciding between (1) and (2) — the ratio of clean to dirty files is what says whether this is a verifier-strictness bug or a rewrite bug.
PERRY_GC_ZEAL=1andPERRY_GC_VERIFY_EVACUATION=1are both shipped knobs, and no CI arm sets them together over the representation corpus. Setting them together finds something on the first file that was tried.Repro
test_gap_repsel_p4a3_ptr_numarray, compiled and run with back-edge safepoint polls, atorigin/main=64c1f56fb:10/10, deterministic.
oldis aPOINTER_TAGNaN-box (0x7ffd); the object moved 0x50 bytes and a shadow-stack slot still holds the pre-move address after the rewrite pass ran.Adding
PERRY_GC_PROTECT_FROMSPACE=1instead of the verifier gives a hard fault at the use rather than at the check, also 10/10:Why this is probably a dead slot, and why it still matters
Three measurements say the mutator is not reading it:
[gc-fromspace-scan clean] … missing_rewrites=0 dangling=0 owners=0on every cycle —PERRY_GC_FROMSPACE_SCAN_ABORT=1never fires;So the most likely reading is a shadow-stack slot whose value is no longer live but was not rewritten — the verifier is stricter than the collector needs to be. That is still worth resolving, in one of two directions, because the current state makes an instrument unusable:
PERRY_GC_VERIFY_EVACUATION=1cannot be combined with zeal on real code — and zeal is precisely the configuration you reach for when hunting a rooting bug;Note the adjacent, opposite-signed finding in #7035 (the verifier reporting clean on a demonstrable missing rewrite because it only self-checks the rewrite pass's own enumeration). A verifier with both a false-negative mode and a false-positive mode is not yet a trustworthy oracle.
Scope, honestly
c9cd73ba5(before fix(gc): root the ten unrooted runtime-side caches, and the scanner that walked 1 of 3 sibling slots (#7231) #7239/fix(codegen): remove the unproven i64 function specialization (#7238) #7242/fix(gc): a Symbol-typed local is a GC-heap reference, and gets a shadow slot (#7236) #7243/fix(gc): run the globalThis bootstrap in a no-move window (#7217) #7249/perf(gc): stop maintaining a remembered set nothing has read yet (#7187 Phase A) #7250/fix(gc): root every argument of a cross-module direct call (#7240, by @jdalton) #7252). It is not what matrix: test_gap_repsel_p4a3_ptr_numarray red on main across ALL requires=move arms, untriaged — blocks clean gating of every barrier/GC PR (#7016 neighbourhood) #7194 was about — that was an output mismatch fixed by fix(gc): run the globalThis bootstrap in a no-move window (#7217) #7249, and this survives the fix.test_gap_repsel_p4a3_ptr_numarrayfails 10/10;test_gap_repsel_canonical_i32,test_gap_repsel_p4a3_numarray_growthandtest_gap_repsel_ptr_shape_localsare clean 0/3. Nobody has swept the other 45.scripts/gc_instrument_smoke.shexercises zeal against its own synthetic fixture;gc_repsel_matrix.shhas no zeal arm at all. Under CLAUDE.md's GC knob kill-policy that is a configuration nobody has verified — which is how a 10/10 deterministic reproducer sat unseen in a corpus file that the matrix runs twenty-one different ways.Suggested first step: sweep the remaining 45 corpus files under this arm to size the population before deciding between (1) and (2) — the ratio of clean to dirty files is what says whether this is a verifier-strictness bug or a rewrite bug.