Repository navigation
for await (const chunk of process.stdin) intermittently stalls forever on piped input ≥ ~1 MiB (4 MiB+ hangs ~always) #10895
Description
Activity
Classification: a regression, not macOS-specific — and root-caused.
build host hangs published 0.5.1520 Linux x86_64 0 / 15 (64 KiB … 64 MiB × 3) main 841b605c97(v0.5.1632)Linux x86_64 1 / 350 at 16 MiB; 0 / 20 at 64 MiB; 0 / 90 at ≤ 4 MiB main 841b605c97(v0.5.1632)macOS arm64 3 / 6 at 1 MiB, 6 / 6 at 4 MiB, 5 / 8 at 8 MiB fed in 256-byte writes A stalled process has no reader thread at all —
sampleshows only the main thread, parked injs_wait_for_event. (The "reader thread inread()" in the issue text was a different, healthy process.)Root cause (
crates/perry-runtime/src/os_process_streams.rs,ensure_stdin_reader): the async iterator pauses its source after every delivered chunk and resumes it on the next pull (node_stream/async_iterator.rs,ns_readable_iter_on_data/ns_readable_iterator_next). Onprocess.stdin,pause()latchesSTDIN_DETACHED— the fd-0 reader exits when it sees the latch at the top of its loop — andresume()clears the latch and respawns the reader unlessSTDIN_READER_STARTEDis still true. The reader's stop decision and itsSTARTEDreset (a drop guard) were separate steps:reader: sees STDIN_DETACHED == true, decides to exit main: resume() → STDIN_DETACHED = false; CAS(STARTED false→true) FAILS — the dying reader has not cleared it yet reader: clears STARTED, gonefd 0 then has no reader, while
js_readline_has_activestill reports a started, un-paused, flowing stdin — so the loop stays alive and idles forever. One roll of that dice per delivered chunk: ~170 chunks/MiB through a 16 KiB macOS pipe fed in 512-byte writes, which is why 1 MiB is a coin flip there and 4 MiB is near-certain; a 64 KiB Linux pipe delivers ~16 chunks/MiB and the window is a handful of instructions, so it needs preemption to land (the one Linux hang was under load average ~30).Introduced by bb57392 (2026-09-04, "unify process.stdin reader surface"). Before it, a piped stdin was read by perry-stdlib readline's own reader, which never consulted the latch — that is why 0.5.1520 is clean.
Fix + tests coming in a PR. The "garbage frame header" noted at the bottom of this issue is a separate, deterministic bug —
process.stdout.write(bytes)UTF-8-decodes binary chunks — now #10903.Verdict on the two "also seen downstream" items at the bottom of this issue — neither is the stall, neither is GC:
- Garbage frame header (
4022190063) — deterministic:process.stdout.write(Uint8Array)UTF-8-decodes the chunk, soF7 FF 0F 00goes out asEF BF BD EF …. Tracked separately inprocess.stdout.write(bytes)is not binary-safe: a Buffer/Uint8Array chunk is UTF-8 decoded, every invalid byte is written as EF BF BD #10903. - 64 MiB SIGSEGV — deterministic: it is
Uint8Array<ArrayBuffer>-annotated receiver:indexOf/lastIndexOfsilently return -1 (plainUint8Array,anyandFloat32Array<ArrayBuffer>are correct) #10894. TheUint8Array<ArrayBuffer>annotation sendsmessage.indexOf(COMMA, from)to the plain-Arrayjs_array_indexOf_jsvalue, which reads the byte buffer as 8-byte slots and runs off the end (backtrace + a 6-line repro that crashes at 2 MiB are onUint8Array<ArrayBuffer>-annotated receiver:indexOf/lastIndexOfsilently return -1 (plainUint8Array,anyandFloat32Array<ArrayBuffer>are correct) #10894).
With the stall fixed (#10913) and the receiver annotated plain
Uint8Array, the host's read side is clean: 1 MiB … 64 MiB, 0 hangs / 0 crashes in 220 runs on macOS.- Garbage frame header (
- added a commit that references this issue
on Sep 22, 2026
Summary
for await (const chunk of process.stdin)on a pipe intermittently stops delivering chunks and never resumes. The process sits idle (0% CPU) with input still unread, the writer blocks, and nothing ever completes. It is a race: 1 MiB stalls about half the time, 4 MiB and up essentially always. 64 KiB is fine.Repro
perry 0.5.1618 (
b9ba951ff8, macOS arm64, 16 KiB pipe chunks), two runs per size:node 24 on the same driver:
done total 8388608 chunks 8261.When stalled, a
sampleshows the main thread inpthread_cond_waitand the stdin reader thread inread(). Logging per chunk shows delivery simply stops part-way (e.g. at 890,364 of 2,097,151 bytes), so it looks like a lost wakeup / pause-resume hand-off between the reader thread and the loop rather than EOF handling. Linux not tested yet.Also seen downstream, not yet isolated (may be separate bugs)
Same stdin path inside the Native Messaging host from https://github.com/guest271314/NativeMessagingHosts (each chunk copied into one large
Uint8Arraywith.set, then echoed withstdout.write):4022190063) — i.e. corrupted output, not just a stall.SIGSEGV(rc −11) after ~75 ms on one run, hang on another.Those need their own reduction once the stall is out of the way; noting them here so they are not lost.
Impact
Any CLI / filter / Native Messaging host that reads more than about a megabyte from a pipe. Browser→host Native Messaging messages go up to 64 MiB.
Related: #10873 and the
Uint8Array<ArrayBuffer>indexOfissue (same program). Context: jlucaso1/js-compiled#2