shell: route FIFO redirect targets through the pollable IOWriter path
Repro
import { $ } from "bun";
import { execSync } from "node:child_process";
import * as fs from "node:fs";
const fifo = `/tmp/fifo-${process.pid}.fifo`;
execSync(`mkfifo ${fifo}`);
// a reader exists so open(O_WRONLY) succeeds, but it never drains
const rfd = fs.openSync(fifo, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK);
$.nothrow();
await $`echo ${"z".repeat(200 * 1024)} > ${fifo}`.quiet();
// before: rc=134 abort
// panic: internal error: entered unreachable code: drainBufferedData returning
// .pending in IOWriter.doFileWrite should not happen
Deterministic on Linux and macOS.
Cause
Builtin opens a >-redirect target via open_for_writing_impl, which fstats the fd, reports pollable = true for S_IFIFO/S_IFSOCK, and sets O_NONBLOCK. On POSIX the caller then discarded that result and passed a hardcoded pollable = false to IOWriter::init, so the writer took the synchronous file path (do_file_write). That path drives write(2) in a loop and treats WriteResult::Pending (EAGAIN) as unreachable!() on the assumption that a "file" never returns EAGAIN. A FIFO with a slow reader does: once the pipe buffer fills, the next write returns EAGAIN and the process aborts. Pipeline stdout (echo big | slow) already went through the pollable writer and handled the same fd shape correctly.
Fix
Builtin.rs: pass thepollablevalue computed byopen_for_writing_implto the redirectIOWriteron POSIX (Windows keeps its unconditional async writer). FIFO/socket redirect targets now take the pollable path and poll for writability under backpressure.IOWriter::do_file_write: replace theunreachable!()forPendingwith an in-place upgrade: record the partial write, flipflags.pollable/nonblock, and restart viawrite()so the remainder drains through the poll machinery. If the fd turns out to also be un-epollable, the EAGAIN is surfaced as the chunk's write error instead of aborting.
Verification
$ USE_SYSTEM_BUN=1 bun test test/js/bun/shell/file-io.test.ts -t FIFO
(fail) redirect to a FIFO whose reader drains slowly applies backpressure
exitCode: 134
stderr: panic: internal error: entered unreachable code: drainBufferedData
returning .pending in IOWriter.doFileWrite should not happen
$ bun bd test test/js/bun/shell/file-io.test.ts -t FIFO
(pass) redirect to a FIFO whose reader drains slowly applies backpressure
The full file-io.test.ts (27 tests) and bunshell.test.ts (412 tests) suites pass.
[review] gate passed · iteration 2 · 3 files touched
fails on main (without fix)
ASAN without fix: 1 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/shell/file-io.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (eaec13dc0)
test/js/bun/shell/file-io.test.ts:
(pass) IOWriter file output redirection > basic file redirection > simple echo to file [59.66ms]
(pass) IOWriter file output redirection > basic file redirection > empty output to file [7.65ms]
(pass) IOWriter file output redirection > basic file redirection > zero-length write should trigger onIOWriterChunk callback [6.36ms]
(pass) IOWriter file output redirection > drainBufferedData edge cases > large single write [17.95ms]
(pass) IOWriter file output redirection > drainBufferedData edge cases > write to subdirectory [8.22ms]
bun: Not a directory: /dev/null/invalid/path(pass) IOWriter file output redirection > file system error conditions > write to invalid path should fail [22.73ms]
... (truncated)
release without fix: all passed
bun test v1.4.0-canary.1 (85c00e9f8)
test/js/bun/shell/file-io.test.ts:
(pass) IOWriter file output redirection > basic file redirection > simple echo to file [1.35ms]
(pass) IOWriter file output redirection > basic file redirection > empty output to file [0.37ms]
(pass) IOWriter file output redirection > basic file redirection > zero-length write should trigger onIOWriterChunk callback [0.25ms]
(pass) IOWriter file output redirection > drainBufferedData edge cases > large single write [0.48ms]
(pass) IOWriter file output redirection > drainBufferedData edge cases > write to subdirectory [0.30ms]
bun: Not a directory: /dev/null/invalid/path(pass) IOWriter file output redirection > file system error conditions > write to invalid path should fail [0.25ms]
bun: No such file or directory: /nonexistent/file.txt(pass) IOWriter file output redirection > file system error conditions > write to non-existent directory should fail [0.11ms]
(pass) IOWriter file output redirection > special file types > write to /dev/null [0.07ms]
(pass) IOWriter file output redirection > special file types > redirect to a FIFO whose reader drains slowly applies backpressure [32.02ms]
(pass) IO
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/shell/file-io.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (eaec13dc0)
test/js/bun/shell/file-io.test.ts:
(pass) IOWriter file output redirection > basic file redirection > simple echo to file [58.79ms]
(pass) IOWriter file output redirection > basic file redirection > empty output to file [7.43ms]
(pass) IOWriter file output redirection > basic file redirection > zero-length write should trigger onIOWriterChunk callback [6.06ms]
(pass) IOWriter file output redirection > drainBufferedData edge cases > large single write [5.85ms]
(pass) IOWriter file output redirection > drainBufferedData edge cases > write to subdirectory [22.60ms]
bun: Not a directory: /dev/null/invalid/path(pass) IOWriter file output redirection > file system error conditions > write to invalid path should fail [16.45ms]
... (truncated)
release with fix: all passed
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 707ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/6] gen generated_host_exports.rs
generated_host_exports.rs: 91 exports (host=3, lazy=10, generic=78, rust=0); 244 extern-C blocks audited
[1/6] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: component rust-std is up to date
nightly-2026-05-06-x86_64-unknown-linux-gnu unchanged - rustc 1.97.0-nightly (e95e73209 2026-05-05)
info: checking for self-update (current version: 1.29.0)
[1m[92m Compiling[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
[1m[92m Compiling[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
[1m
... (truncated)
diff hotspot
src/runtime/shell/Builtin.rs | 13 ++-----
src/runtime/shell/IOWriter.rs | 38 ++++++++++++++-----
test/js/bun/shell/file-io.test.ts | 80 ++++++++++++++++++++++++++++++++++++++-
3 files changed, 111 insertions(+), 20 deletions(-)
gate history · 2 passed · 0 rejected · iteration 2
evidence per changed file
file reads edits tests
src/runtime/shell/Builtin.rs 3 2 0
src/runtime/shell/IOWriter.rs 3 4 0
test/js/bun/shell/file-io.test.ts 1 6 0