Skip to content

gc: string-method receiver and arguments are not precise roots across an allocating argument #6971

Description

@proggeramlug

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.

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