Skip to content

GC use-after-free: objects pushed to a module-level array from inside a function are freed under allocation churn + gc() #5459

Description

@proggeramlug

Summary

On clean main (v0.5.1191), objects pushed to a module-level array from inside a function are wrongly collected under allocation churn + explicit gc(), causing a use-after-free / SIGSEGV. The same population done at module scope is fine. No weak references are involved — this was isolated while debugging WeakMap weakness and turns out to be a pre-existing, independent bug.

Minimal reproduction (deterministic)

declare function gc(): void;
function churn(n: number): void {
  let j: any[] = [];
  for (let i = 0; i < n; i++) { j.push({ i, p: "z".repeat(40) + i }); if (j.length > 128) j = []; }
}
const strong: any[] = [];
(function setup() { for (let n = 0; n < 50; n++) strong.push({ id: n }); })();  // populate from INSIDE a fn
for (let c = 0; c < 12; c++) { churn(80000); gc(); }
let alive = 0;
for (let n = 0; n < 50; n++) if (strong[n] && strong[n].id === n) alive++;
console.log("survived:", alive, "/ 50");

perry run (or compile + run) → SIGSEGV (exit 139), 5/5 runs. Some strong[] elements have been freed; reading them faults.

Isolation / narrowing

  • The IIFE is the trigger. Replacing (function setup(){ for (...) strong.push({id:n}); })(); with a module-scope for (let n=0;n<50;n++) strong.push({id:n}); → survives 50/50, no crash. That one wrapper is the only delta.
  • Not weak-collection-related. First seen with WeakMap/FinalizationRegistry/WeakRef, but the control above has no weak refs at all and still crashes. (FinalizationRegistry with live targets held in an array shows the same: live registered targets are lost / SIGSEGV.)
  • Not generational-GC-specific. Reproduces with PERRY_GEN_GC=0 (full mark-sweep) and with PERRY_WRITE_BARRIERS=0.
  • Requires allocation churn. No churn() (just a gc() loop) → 50/50. The churn profile matters: rapid small-array alloc + discard triggers it; building one large array per round does not.
  • Heap-layout sensitive (Heisenbug). Deterministic standalone, but runs clean under lldb (debugger heap layout masks the UAF), which has frustrated getting a faulting backtrace.

Suspected area

An object created in a nested function scope and stored only in a module-level array appears not to be reliably retained as a GC root — a sweep under allocation pressure frees it despite the live strong reference from the module-global array. Likely a root-scanning or codegen root-registration gap for module-level array elements written from within a callee frame. Needs an ASAN/sanitizer build or root-scan/codegen tracing to pin the exact site.

Impact

Silent memory corruption / crash for an ordinary pattern (populate a module-level collection from a helper function, then do allocation-heavy work with gc()). Also blocks making WeakMap/WeakSet genuinely weak (#2656), whose implementation exercises the same path.

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