Skip to content

String literal operand is not GC-rooted across an allocating call in the same expression (stale handle after evacuation) #7114

Description

@proggeramlug

Summary

A string-literal handle is loaded before an allocating call in the same expression and reused after it. The handle global is a GC root that evacuation rewrites; the register holding the pre-call value is stale. Under enough allocation the concatenation silently produces a truncated or empty string.

This is exactly the hazard crates/perry-codegen/src/expr/temp_root.rs (#6951) was built to close — "an already-evaluated operand waiting for its sibling ... the slot is a mutable root, so an evacuating cycle rewrites it and the register pushed beforehand is stale" — with the string-literal-handle load not covered by it.

Silent wrong output, exit code 0. No crash, no diagnostic.

Repro

class Rec {
  id: number; score: number;
  constructor(id: number, score: number) { this.id = id; this.score = score; }
}
function make(i: number): Rec { const r = new Rec(i, 0); r.score = r.id * 1.5; return r; }
function run(n: number): number {
  let acc = 0;
  for (let i = 0; i < n; i++) { const r = make(i); acc = acc + r.score; }
  return acc;
}

console.log("acc:" + run(10000000));   // literal evaluated BEFORE the allocating call
const v = run(10000000);
console.log("hoisted:" + v);           // same concat, call hoisted out
$ node --experimental-strip-types repro.ts     # v26.5.1, the .node-version pin
acc:74999992500000
hoisted:74999992500000

$ perry compile repro.ts -o repro && ./repro
                                                <- EMPTY LINE
hoisted:74999992500000
$ echo $?
0

Only the first statement is wrong. Hoisting the call out of the concatenation fixes it, which localises the bug to operand evaluation order rather than to run, to js_string_concat_value, or to the arithmetic (the number is correct in both).

The corruption is allocation-count dependent and its manifestation varies, which is the signature of reading through a stale/reused heap slot rather than of a logic error:

iterations observed
1,000 acc:1249000 (correct)
100,000 acc:12499900000 (correct)
1,000,000 1249999000000 — prefix gone
10,000,000 S124999990000000 / r1249999000000 / empty — prefix replaced by garbage

Root cause, from the emitted IR

--trace llvm, @main, the two statements side by side:

; BROKEN — literal handle read first, used after the allocating call
%r1 = load double, ptr @minrepro2_ts_.str.2.handle            ; <-- read BEFORE
%r2 = call double @perry_fn_minrepro2_ts__run__spec_i32(i32 10000000)   ; 10M allocs -> evacuating GC
%r3 = bitcast double %r1 to i64                               ; <-- STALE
%r4 = and i64 %r3, 281474976710655
%r5 = call i64 @js_string_concat_value(i64 %r4, double %r2)

; CORRECT — call is a separate statement, so the handle is read after it
%r15 = call double @perry_fn_minrepro2_ts__run__spec_i32(i32 10000000)
store double %r15, ptr %r14
%r16 = load double, ptr @minrepro2_ts_.str.3.handle           ; <-- read AFTER

There is no js_gc_temp_root_push protecting %r1 across the call. The temp_root triple is emitted in this same function — but only later, around the console.log spread array (%r8/%r10), not around the literal operand.

Two possible fixes, both already-established patterns in the codebase:

  • push the loaded literal with temp_root_push_i64 before the sibling operand is evaluated and re-read it with temp_root_get_i64 afterwards (the documented contract in temp_root.rs), or
  • sink the load of the .handle global below the call so it is re-read after any collection point.

Scope

How it was found

Fell out of building a hot-path workload for the representation-selection A/B on #7107 (console.log("acc:" + run(N)) at N = 10⁷). Filing separately so it does not ride on a perf PR.

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