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.
Summary
On clean
main(v0.5.1191), objects pushed to a module-level array from inside a function are wrongly collected under allocation churn + explicitgc(), 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)
perry run(or compile + run) → SIGSEGV (exit 139), 5/5 runs. Somestrong[]elements have been freed; reading them faults.Isolation / narrowing
(function setup(){ for (...) strong.push({id:n}); })();with a module-scopefor (let n=0;n<50;n++) strong.push({id:n});→ survives 50/50, no crash. That one wrapper is the only delta.PERRY_GEN_GC=0(full mark-sweep) and withPERRY_WRITE_BARRIERS=0.churn()(just agc()loop) → 50/50. The churn profile matters: rapid small-array alloc + discard triggers it; building one large array per round does not.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.