Skip to content

GC: explicit gc() (full collect, default auto stack-scan) reclaims live top-level locals — string fields read back as garbage #4977

Description

@proggeramlug

Repro

const keep = { nested: { deep: "leaf-string-4916" } };
gc();
console.log(keep.nested.deep.length); // garbage, e.g. 3833593936
console.log(keep.nested.deep);        // [invalid utf8]

Same corruption with a class instance (class Widget { x = 1; constructor(public name: string) {} }) reading w.name after gc(). Reproduces in both auto-optimize and PERRY_NO_AUTO_OPTIMIZE=1 builds (v0.5.1155, macOS arm64).

Bisection

  • PERRY_CONSERVATIVE_STACK_SCAN=fullcorrect (prints 16 / the string).
  • PERRY_GEN_GC=0 (legacy full mark-sweep) → correct.
  • Default (auto scan mode) → live objects reclaimed; later field reads return dangling-pointer garbage.

So in the default auto mode (gc/roots.rs::conservative_stack_scan_decision_for maps AutoSkipDisabled), a full collection skips the native stack scan, and module-init/top-level locals like keep are evidently not covered by the precise shadow-stack roots or the module-var scanners at the gc() callsite — the whole object graph is treated as dead. The auto skip was motivated by copied-minor eligibility ("copied-minor eligibility only depends on exact mutable roots"), but it also applies to explicit full collections where these locals are live.

Impact

Any program calling the documented explicit gc() (or any API that forces a full collection) with live state held in top-level locals can have that state reclaimed and read back as garbage — silent memory corruption, no crash.

v8.getHeapSnapshot()/writeHeapSnapshot() (#4916) force a full collection like Node does; they sidestep this by setting the per-thread set_conservative_stack_scan_override(Full) around their collect. Explicit gc() likely wants the same treatment (or shadow-stack coverage for module-init locals) — the override mechanism already exists for the GC unit tests.

Found while validating the real heap-snapshot graph for #4916.

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