Summary
readable.tee() + for await over a branch SIGSEGVs once stream-family ids pass STREAM_ID_BAND_START + ~512 — i.e. after ~256 prior stream+reader allocations. This is the same failure class as the two sites fixed in #6599 (numeric handle id with k >= 512 → low-48-of-double taken as a pointer → deref below the macOS ~2TB heap floor), at a third, still-unfixed site reached through the tee path.
Found while validating #6602 (id recycling) — it reproduces identically on a pristine main build, and is masked by #6602's recycling when the quarantine is small (ids stay below the fuse), which is how it surfaced: the same binary crashes with PERRY_STREAM_ID_QUARANTINE=64 or default but passes with =8.
Repro (deterministic)
async function churnReaders(n: number): Promise<number> {
let total = 0;
for (let i = 0; i < n; i++) {
const rs = new ReadableStream({
start(c) { c.enqueue("x"); c.close(); },
});
const reader = rs.getReader();
const first = await reader.read();
if (!first.done) total++;
await reader.read();
reader.releaseLock();
}
return total;
}
async function main() {
console.log("readers:", await churnReaders(260)); // 250 → OK, 260 → SIGSEGV
const src = new ReadableStream({
start(c) { c.enqueue(1); c.enqueue(2); c.close(); },
});
const [a, b] = src.tee();
let sa = 0, sb = 0;
for await (const v of a) sa += v;
for await (const v of b) sb += v;
console.log("tee:", sa, sb);
}
main().then(() => console.log("OK"));
churnReaders(250) (max id offset k≈501): prints tee: 3 3 / OK.
churnReaders(260) (k≈521): crashes before printing tee:. Node prints tee: 3 3 / OK at any n.
- Threshold sits exactly at the k≥512 fuse: each churn iteration allocates 2 ids (stream + reader).
Crash
EXC_BAD_ACCESS (code=1, address=0x1969fffffff8)
-> ldurb w9, [x8, #-0x8] ; header sniff at ptr-8
0x1969fffffff8 + 8 = 0x196A00000000 — the low 48 bits of the f64 bit pattern of an in-band numeric stream handle (≈1.15M, inside [0x100000, 0x200000)), i.e. somewhere in the tee/branch-iteration path a raw numeric handle is treated as a NaN-boxed pointer and its double-bits' low-48 get dereferenced, exactly like the two #6599 sites (ungated URLSearchParams probe, #597 fold's 48-bit codegen mask).
Filing separately per the granular-issue convention; #6602's fix reduces exposure under steady-state serving (recycled ids stay low) but does not remove the underlying misclassification.
Summary
readable.tee()+for awaitover a branch SIGSEGVs once stream-family ids passSTREAM_ID_BAND_START + ~512— i.e. after ~256 prior stream+reader allocations. This is the same failure class as the two sites fixed in #6599 (numeric handle id withk >= 512→ low-48-of-double taken as a pointer → deref below the macOS ~2TB heap floor), at a third, still-unfixed site reached through the tee path.Found while validating #6602 (id recycling) — it reproduces identically on a pristine
mainbuild, and is masked by #6602's recycling when the quarantine is small (ids stay below the fuse), which is how it surfaced: the same binary crashes withPERRY_STREAM_ID_QUARANTINE=64or default but passes with=8.Repro (deterministic)
churnReaders(250)(max id offset k≈501): printstee: 3 3/OK.churnReaders(260)(k≈521): crashes before printingtee:. Node printstee: 3 3/OKat any n.Crash
0x1969fffffff8 + 8 = 0x196A00000000— the low 48 bits of the f64 bit pattern of an in-band numeric stream handle (≈1.15M, inside[0x100000, 0x200000)), i.e. somewhere in the tee/branch-iteration path a raw numeric handle is treated as a NaN-boxed pointer and its double-bits' low-48 get dereferenced, exactly like the two #6599 sites (ungated URLSearchParams probe, #597 fold's 48-bit codegen mask).Filing separately per the granular-issue convention; #6602's fix reduces exposure under steady-state serving (recycled ids stay low) but does not remove the underlying misclassification.