Found while fixing #6951. Same bug class (an evaluated argument held in an SSA register across a later argument's collection), different lowering path — the native-method dispatch, not the variadic accumulator #6951 fixed.
This one aborts (exit 134), so it is the loudest of the group.
Repro
let sink: unknown[] = [];
function churn(n: number): number {
let acc = 0;
for (let i = 0; i < n; i++) {
sink.push({ i: i, s: "x" + (i & 255), a: [i, i + 1] });
if (sink.length > 4096) { acc = (acc + sink.length) | 0; sink = []; }
}
return acc | 0;
}
function fresh(k: number): string { return "f" + k + "-" + (k * 7); }
const N = 420000;
// Repeat 6x: use-after-free, needs the freed block recycled.
{ const m = new Map<unknown, unknown>(); m.set(fresh(0), churn(N)); console.log([...m.keys()][0], [...m.values()][0]); }
{ const m = new Map<unknown, unknown>(); m.set(fresh(1), churn(N)); console.log([...m.keys()][0], [...m.values()][0]); }
{ const m = new Map<unknown, unknown>(); m.set(fresh(2), churn(N)); console.log([...m.keys()][0], [...m.values()][0]); }
{ const m = new Map<unknown, unknown>(); m.set(fresh(3), churn(N)); console.log([...m.keys()][0], [...m.values()][0]); }
{ const m = new Map<unknown, unknown>(); m.set(fresh(4), churn(N)); console.log([...m.keys()][0], [...m.values()][0]); }
{ const m = new Map<unknown, unknown>(); m.set(fresh(5), churn(N)); console.log([...m.keys()][0], [...m.values()][0]); }
$ PERRY_CONSERVATIVE_STACK_SCAN=off PERRY_GC_HEAP_LIMIT=8 ./repro
... ; exit 134 (abort)
Byte-exact vs node 26.5.0 under the default configuration, so this is purely a rooting gap that conservative stack scanning is currently hiding (gc_check_trigger forces the scan on both automatic arms; production resolves Auto -> SkipDisabled).
Fix shape
The map.set(key, value) lowering evaluates the receiver and each argument into SSA registers and holds them across the remaining argument evaluations and the runtime call. crates/perry-codegen/src/expr/temp_root.rs (added by #6951) has the primitive — lower_exprs_rooted / lower_operand_pair_rooted root each already-evaluated value across the later ones and return re-read values plus a guard for temp_root_release.
Related: #6951, #6968, #6969, #6971, #6950.
Found while fixing #6951. Same bug class (an evaluated argument held in an SSA register across a later argument's collection), different lowering path — the native-method dispatch, not the variadic accumulator #6951 fixed.
This one aborts (exit 134), so it is the loudest of the group.
Repro
Byte-exact vs node 26.5.0 under the default configuration, so this is purely a rooting gap that conservative stack scanning is currently hiding (
gc_check_triggerforces the scan on both automatic arms; production resolvesAuto -> SkipDisabled).Fix shape
The
map.set(key, value)lowering evaluates the receiver and each argument into SSA registers and holds them across the remaining argument evaluations and the runtime call.crates/perry-codegen/src/expr/temp_root.rs(added by #6951) has the primitive —lower_exprs_rooted/lower_operand_pair_rootedroot each already-evaluated value across the later ones and return re-read values plus a guard fortemp_root_release.Related: #6951, #6968, #6969, #6971, #6950.