Found while fixing #6951. #6951 rooted the string-concat operand pair and the n-way concat chain; the string-method receiver and its arguments are a separate path in lower_string_method.rs and are still unrooted.
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.
{ console.log(fresh(0).concat("|" + churn(N))); }
{ console.log(fresh(1).concat("|" + churn(N))); }
{ console.log(fresh(2).concat("|" + churn(N))); }
{ console.log(fresh(3).concat("|" + churn(N))); }
{ console.log(fresh(4).concat("|" + churn(N))); }
{ console.log(fresh(5).concat("|" + churn(N))); }
PERRY_CONSERVATIVE_STACK_SCAN=off PERRY_GC_HEAP_LIMIT=8 ./repro prints wrong output. Before #6951 the same shape segfaulted; the concat fix removed the crash but the receiver is still dropped.
Why
The receiver string is lowered to an SSA register (often already unboxed to a raw StringHeader* handle), then the argument expression is lowered — and it allocates. Neither the boxed receiver nor the unboxed handle is a GC root. Note the unboxed-handle form is the more dangerous one: a raw handle is a bare address, which only the gc::root_words bare form covers, and only if something roots it.
Same hazard exists for every s.method(arg) whose argument can collect: slice, replace, split, padStart, indexOf, concat, …
Fix shape
crates/perry-codegen/src/expr/temp_root.rs (added by #6951) has both primitives:
lower_exprs_rooted for the receiver + argument list, and
temp_root_push_i64 / temp_root_get_i64 for a bare unboxed handle held across a call (the pattern already used for the intermediate js_jsvalue_to_string handle in lower_string_coerce_concat).
Related: #6951, #6968, #6969, #6970, #6950.
Found while fixing #6951. #6951 rooted the string-concat operand pair and the n-way concat chain; the string-method receiver and its arguments are a separate path in
lower_string_method.rsand are still unrooted.Repro
PERRY_CONSERVATIVE_STACK_SCAN=off PERRY_GC_HEAP_LIMIT=8 ./reproprints wrong output. Before #6951 the same shape segfaulted; the concat fix removed the crash but the receiver is still dropped.Why
The receiver string is lowered to an SSA register (often already unboxed to a raw
StringHeader*handle), then the argument expression is lowered — and it allocates. Neither the boxed receiver nor the unboxed handle is a GC root. Note the unboxed-handle form is the more dangerous one: a raw handle is a bare address, which only thegc::root_wordsbare form covers, and only if something roots it.Same hazard exists for every
s.method(arg)whose argument can collect:slice,replace,split,padStart,indexOf,concat, …Fix shape
crates/perry-codegen/src/expr/temp_root.rs(added by #6951) has both primitives:lower_exprs_rootedfor the receiver + argument list, andtemp_root_push_i64/temp_root_get_i64for a bare unboxed handle held across a call (the pattern already used for the intermediatejs_jsvalue_to_stringhandle inlower_string_coerce_concat).Related: #6951, #6968, #6969, #6970, #6950.