Found while fixing #6951. Distinct defect, same symptom class.
With the conservative native-stack scan disabled, a heap value stored into a scalar-replaced object or array local is swept out from under the alloca that holds it. The alloca has no shadow-slot binding, so it is not a precise root.
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 -- the failure is a use-after-free, so the freed block has to be
// recycled before it shows. A single statement passes.
{ const o = { a: fresh(0), b: churn(N) }; console.log(o.a, o.b); }
{ const o = { a: fresh(1), b: churn(N) }; console.log(o.a, o.b); }
{ const o = { a: fresh(2), b: churn(N) }; console.log(o.a, o.b); }
{ const o = { a: fresh(3), b: churn(N) }; console.log(o.a, o.b); }
{ const o = { a: fresh(4), b: churn(N) }; console.log(o.a, o.b); }
{ const o = { a: fresh(5), b: churn(N) }; console.log(o.a, o.b); }
$ PERRY_CONSERVATIVE_STACK_SCAN=off PERRY_GC_HEAP_LIMIT=8 ./repro
f0-0 417894
f1-7 421991
f2-14 417894
421991 <- o.a is empty
417894
421991
The array-literal form is identical: const a = [fresh(k), churn(N)]; console.log(a[0], a[1]);.
Why
There is no object. The literal is scalar-replaced, so o.a / o.b are plain entry-block allocas:
%r13 = call double @..._fresh__spec_i32(i32 0)
store double %r13, ptr %r10 ; <-- o.a, a bare alloca
%r16 = call double @..._churn(double %r15) ; collects; %r10's string is swept
store double %r16, ptr %r11 ; <-- o.b
%r10 holds a heap string and is bound to no shadow slot, so the shadow-stack root walk never sees it. #6951's object-literal fix does not apply — that path roots the object handle, and here there is no handle.
Scope
Any scalar-replaced object/array local whose replaced slot can hold a heap value (string, object, closure) and is live across an allocating call. Numeric-only scalar replacement is unaffected.
Likely fix: the scalar-replacement pass must reserve a shadow slot (and emit js_shadow_slot_bind) for each replaced slot that is not proven non-pointer — the same treatment an ordinary pointer-typed local already gets.
Detection
Invisible today: gc_check_trigger forces a conservative native-stack scan on both automatic arms, which finds the alloca. Production resolves conservative_stack_scan_mode() to Auto -> SkipDisabled. The matrix's cons_scan_off arm (scripts/gc_repsel_matrix.sh) is the configuration that sees it, and after #6951 that arm is green, so a corpus member with this shape would now catch it.
Related: #6951, #6950, #6942.
Found while fixing #6951. Distinct defect, same symptom class.
With the conservative native-stack scan disabled, a heap value stored into a scalar-replaced object or array local is swept out from under the alloca that holds it. The alloca has no shadow-slot binding, so it is not a precise root.
Repro
The array-literal form is identical:
const a = [fresh(k), churn(N)]; console.log(a[0], a[1]);.Why
There is no object. The literal is scalar-replaced, so
o.a/o.bare plain entry-block allocas:%r10holds a heap string and is bound to no shadow slot, so the shadow-stack root walk never sees it. #6951's object-literal fix does not apply — that path roots the object handle, and here there is no handle.Scope
Any scalar-replaced object/array local whose replaced slot can hold a heap value (string, object, closure) and is live across an allocating call. Numeric-only scalar replacement is unaffected.
Likely fix: the scalar-replacement pass must reserve a shadow slot (and emit
js_shadow_slot_bind) for each replaced slot that is not proven non-pointer — the same treatment an ordinary pointer-typed local already gets.Detection
Invisible today:
gc_check_triggerforces a conservative native-stack scan on both automatic arms, which finds the alloca. Production resolvesconservative_stack_scan_mode()toAuto -> SkipDisabled. The matrix'scons_scan_offarm (scripts/gc_repsel_matrix.sh) is the configuration that sees it, and after #6951 that arm is green, so a corpus member with this shape would now catch it.Related: #6951, #6950, #6942.