Skip to content

GC: #7154's residual is NOT fixed — the loop-polls config is red 0/30, and stock zod alone fails 5/40 #7280

Description

@proggeramlug

#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 deref

A 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

  1. Keep the stopgap. fix(gc): disable evacuating minor by default pending #7154 (use-after-free on dynamically-added fields) #7161 stays.
  2. Correct policy.rs:1408.
  3. Add a dependency-scale (real npm package) witness to the moving-GC corpus.

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