Skip to content

gc: console.log argument temporaries are not precise roots — a precise-roots-only collection drops string-literal args (minimal repro, no evacuation needed) #6951

Description

@proggeramlug

With the conservative native-stack scan disabled — i.e. with precise (shadow-stack) roots only — a collection that lands during argument evaluation drops console.log's string-literal argument. The literal is simply gone from the output; no crash, no diagnostic.

This is the same signature #6942 recorded in item 3 ("the console.log string literal arguments are gone too — the labels vanish"), but reduced to a case that needs no evacuation and no manual minor(): a plain allocation-driven, non-moving full mark-sweep is enough. That makes it easier to fix and easier to regression-test than the #6942 form.

Repro

let sink: unknown[] = [];
function churn(n: number): number {
  let acc = 0;
  for (let i = 0; i < n; i++) {
    sink.push({ i: i, s: "x" + (i & 255), a: [i, i + 1] });
    if (sink.length > 4096) { acc = (acc + sink.length) | 0; sink = []; }
  }
  return acc | 0;
}
console.log("alpha", churn(420000));
console.log("bravo", churn(420000));
console.log("charlie", churn(420000));
console.log("delta", churn(420000));
console.log("echo", churn(420000));
$ perry repro.ts -o repro && ./repro                       # and node --experimental-strip-types
alpha 417894
bravo 421991
charlie 417894
delta 421991
echo 417894

$ PERRY_CONSERVATIVE_STACK_SCAN=off PERRY_GC_HEAP_LIMIT=8 ./repro
alpha 417894
bravo 421991
charlie 417894
421991          <- "delta" gone
417894          <- "echo" gone

Deterministic across runs. Measured on origin/main @ 83a6767ff, macOS arm64, --release, pinned Node 26.5.0. PERRY_GC_TRACE=1 confirms real collections occur (3 cycles).

It is not a representation-selection defect

Reproduced identically with all representation gates off (PERRY_CANONICAL_I32_LOCALS=0 PERRY_CANONICAL_STR_LOCALS=0 PERRY_PTR_SHAPE_LOCALS=0 PERRY_PTR_NUMARRAY_LOCALS=0 PERRY_SPECIALIZED_ABI=0 PERRY_INT_VALUED_LOCALS=0), on test-files/test_gap_repsel_gc_stress.ts — byte-identical corrupted output with the representations on and off. The evaluated-but-not-yet-consumed argument temporary is not a precise root, independently of how the surrounding locals are represented.

Why it is invisible today, and why that is the problem

Production is not currently exposed: every automatic collection takes ManualGcScanGuard::force_full_scan() (gc/policy.rs::gc_check_trigger, both the OldReclaim and the nursery arms), so the conservative scan pins the literal. But that means the conservative scan is doing load-bearing correctness work, not acting as a safety net — and gc/roots.rs's nominal production default is Auto → SkipDisabled. Any change that narrows the forced scan (which is what #6942 asks for so the #6655/#6935 rooting bug class becomes testable at all) turns this into a live miscompile.

It also blocks the one GC arm with real detection power: PERRY_CONSERVATIVE_STACK_SCAN=off is the only configuration that can observe a missing shadow-slot binding on a representation-selected pointer local. Until argument temporaries are precise roots, that arm is red for an unrelated reason and cannot discriminate. It is carried as a triaged expected-red in test-parity/gc_repsel_triage.txt against this issue.

Related: #6942, #6946, #6655, #6935.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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