Skip to content

Stack-map assert fires under PERRY_GC_HEAP_LIMIT=8 + FORCE_EVACUATE: a root-scan path still skips ensure_built() (#9191 gap) #9234

Description

@proggeramlug

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.

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