What happened
A plain TypeScript array of about 9 million or more elements that mixes heap strings and numbers reads back wrong values after it is filled with push. Millions of slots hold unrelated strings or empty strings. No error is raised: the program keeps running with corrupted data. Node returns the correct values for the same program at every size.
The wrong values include strings allocated after the array was built (for example "churn-string-that-is-long-2385" appears in slots that held "a-long-heap-string-number-…"). That suggests element slots whose strings were collected and whose memory was reused. This is a memory-safety bug in the runtime; no regex code is involved.
Measured on perry 0.5.1557 (main at 64f5249ac), release build from source, Linux x86_64 (perrymaster). Not bisected: it is not yet known when it started.
Reproduction
// bigarray.ts — pass "churn" as the last argument to add unrelated allocations
const churnArg = process.argv[process.argv.length - 1] === "churn";
for (const n of [9_000_000, 10_000_000]) {
const a: any[] = [];
for (let i = 0; i < n; i++) a.push(i % 3 === 0 ? "a-long-heap-string-number-" + (i % 1000) : i);
if (churnArg) { let churn = 0; for (let j = 0; j < 3000000; j++) churn += ("churn-string-that-is-long-" + j).length; }
let bad = 0, firstBad = -1, sample = "";
for (let i = 0; i < a.length; i++) {
const v = a[i];
const ok = i % 3 === 0 ? v === "a-long-heap-string-number-" + (i % 1000) : v === i;
if (!ok) { bad++; if (firstBad < 0) { firstBad = i; sample = String(typeof v) + ":" + String(v).slice(0, 30); } }
}
console.log(`n=${n} churn=${churnArg} bad=${bad} firstBad=${firstBad} sample=${sample}`);
}
perry compile bigarray.ts --no-auto-optimize -o bigarray && ./bigarray nochurn && ./bigarray churn
Results
| elements |
extra churn |
Perry bad slots |
first bad index |
first bad value |
Node bad slots |
| 8,000,000 |
yes |
0 |
— |
— |
0 |
| 8,500,000 |
yes |
0 |
— |
— |
0 |
| 9,000,000 |
no |
2,843,963 |
418,350 |
"a-long-heap-string-number-541" (wrong slot) |
0 |
| 9,000,000 |
yes |
2,976,690 |
20,166 |
"churn-string-that-is-long-2385" |
0 |
| 10,000,000 |
no |
2,177,942 |
3,366,645 |
"" |
0 |
| 10,000,000 |
yes |
3,202,154 |
0 |
"" |
0 |
Controls that are fine on the same build: arrays of 8–12 million elements holding only numbers and short strings ("s" + (i % 1000), which are stored inline, not as heap strings).
How it surfaced
String.prototype.replace over large inputs goes through a runtime list backed by a GC array of heap-string references and offsets. With the list's fixed entry cap removed by #10207 (part of #10164), a callback replace over 'ab12 cd345;'.repeat(n) completes at n = 600,000. At n = 800,000 it fails with an internal invalid-span error, surfaced as RangeError: Regular expression execution failed. At n = 1,000,000 it fails with an encoding error in the string it reads back, surfaced as RangeError: Invalid regular expression input storage. The failures start where that list passes about 2^23 entries. Before #10207, the cap made the same calls throw "Regular expression memory limit exceeded" before reaching this size.
Hypothesis (unconfirmed)
2^23 slots × 8 bytes = 64 MiB exactly, and the failures begin between 8.5 and 9 million elements. Element storage growing past a 64 MiB boundary may move onto a different path whose slots are not fully traced or relocated: large-object or malloc-tracked storage, card or remembered-set coverage, or a scan range limit. This has not been verified.
Expected
Every element reads back as written, as in Node.
Acceptance
Related: #10164, #10207.
What happened
A plain TypeScript array of about 9 million or more elements that mixes heap strings and numbers reads back wrong values after it is filled with
push. Millions of slots hold unrelated strings or empty strings. No error is raised: the program keeps running with corrupted data. Node returns the correct values for the same program at every size.The wrong values include strings allocated after the array was built (for example
"churn-string-that-is-long-2385"appears in slots that held"a-long-heap-string-number-…"). That suggests element slots whose strings were collected and whose memory was reused. This is a memory-safety bug in the runtime; no regex code is involved.Measured on perry
0.5.1557(mainat64f5249ac), release build from source, Linux x86_64 (perrymaster). Not bisected: it is not yet known when it started.Reproduction
perry compile bigarray.ts --no-auto-optimize -o bigarray && ./bigarray nochurn && ./bigarray churnResults
"a-long-heap-string-number-541"(wrong slot)"churn-string-that-is-long-2385"""""Controls that are fine on the same build: arrays of 8–12 million elements holding only numbers and short strings (
"s" + (i % 1000), which are stored inline, not as heap strings).How it surfaced
String.prototype.replaceover large inputs goes through a runtime list backed by a GC array of heap-string references and offsets. With the list's fixed entry cap removed by #10207 (part of #10164), a callbackreplaceover'ab12 cd345;'.repeat(n)completes at n = 600,000. At n = 800,000 it fails with an internal invalid-span error, surfaced asRangeError: Regular expression execution failed. At n = 1,000,000 it fails with an encoding error in the string it reads back, surfaced asRangeError: Invalid regular expression input storage. The failures start where that list passes about 2^23 entries. Before #10207, the cap made the same calls throw "Regular expression memory limit exceeded" before reaching this size.Hypothesis (unconfirmed)
2^23 slots × 8 bytes = 64 MiB exactly, and the failures begin between 8.5 and 9 million elements. Element storage growing past a 64 MiB boundary may move onto a different path whose slots are not fully traced or relocated: large-object or malloc-tracked storage, card or remembered-set coverage, or a scan range limit. This has not been verified.
Expected
Every element reads back as written, as in Node.
Acceptance
bad=0at 9, 10 and 16 million elements, with and without churn, including under forced collections.replacereproducer completes with Node's checksum.Related: #10164, #10207.