My own #9191 (lazy stack-map index) left a path into the native root scan that does not build the index, and #9182's fail-closed assert catches it:
thread '<unnamed>' panicked at crates/perry-runtime/src/gc/roots/stack_maps.rs:1221:5:
perry: the native root scan ran before the stack-map index was built. An unbuilt index is
indistinguishable from an image with no native roots, so the collector would find no roots
on this frame and free live objects silently. This means a path into the root scan does not
pass through the point that builds the index.
Repro
Any allocating program under a tight heap with forced evacuation — no throw, no packed loop, nothing exotic:
const arr: number[] = [];
for (let i = 0; i < 1024; i++) arr.push(i * 2);
const sink: object[] = [];
function churn(): number { let s = 0; for (let i = 0; i < arr.length; i++) s += arr[i]; return s; }
let out = 0;
for (let k = 0; k < 200; k++) { sink.push({ a: k, b: "x" + k }); if (sink.length > 50) sink.length = 0; out = churn(); }
console.log("s=" + out);
PERRY_GC_HEAP_LIMIT=8 PERRY_GC_FORCE_EVACUATE=1 ./prog
The program prints its correct result first and then panics, so the unguarded scan appears to be on a shutdown or final-collection path rather than a steady-state one — gc_collect_minor_with_trigger, gc_collect_forced_evacuating_minor and gc_collect_full_mark_sweep_with_trigger all got roots::ensure_stack_maps_built() in #9191, so it is likely a fourth entry point I missed. Does not reproduce without PERRY_GC_HEAP_LIMIT=8.
Severity
The assert is doing its job — this is a crash, not a wrong answer, and only under a stress configuration. But it means the guarantee #9182 was written to enforce does not hold on every path: in a build where that assert is compiled out, this same path would scan for roots against an unbuilt index, find none, and free live objects silently.
Found while GC-stressing an unrelated packed-loop change; it reproduces with that change disabled and on programs containing none of the constructs it touches, so it is independent of it.
My own #9191 (lazy stack-map index) left a path into the native root scan that does not build the index, and #9182's fail-closed assert catches it:
Repro
Any allocating program under a tight heap with forced evacuation — no
throw, no packed loop, nothing exotic:The program prints its correct result first and then panics, so the unguarded scan appears to be on a shutdown or final-collection path rather than a steady-state one —
gc_collect_minor_with_trigger,gc_collect_forced_evacuating_minorandgc_collect_full_mark_sweep_with_triggerall gotroots::ensure_stack_maps_built()in #9191, so it is likely a fourth entry point I missed. Does not reproduce withoutPERRY_GC_HEAP_LIMIT=8.Severity
The assert is doing its job — this is a crash, not a wrong answer, and only under a stress configuration. But it means the guarantee #9182 was written to enforce does not hold on every path: in a build where that assert is compiled out, this same path would scan for roots against an unbuilt index, find none, and free live objects silently.
Found while GC-stressing an unrelated packed-loop change; it reproduces with that change disabled and on programs containing none of the constructs it touches, so it is independent of it.