Skip to content

gc: PERRY_GC_ZEAL + PERRY_GC_VERIFY_EVACUATION reports a stale forwarded pointer in shadow-stack roots (10/10, pre-existing, no CI arm sets the pair) #7254

Description

@proggeramlug

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:

  1. 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;
  2. 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.

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