`biome check` aborts with SIGABRT (double panic) when its diagnostics stream is a closed pipe
Environment information
CLI:
Version: 2.5.12
Color support: true
Platform:
CPU Architecture: x86_64
OS: linux
Environment:
BIOME_DISTRIBUTION: unset
BIOME_LOG_FILE: unset
BIOME_LOG_PATH: unset
BIOME_LOG_PREFIX_NAME: unset
BIOME_LOG_LEVEL: unset
BIOME_LOG_KIND: unset
BIOME_CONFIG_PATH: unset
BIOME_THREADS: unset
BIOME_WATCHER_KIND: unset
BIOME_WATCHER_POLLING_INTERVAL: unset
BIOME_BINARY: unset
RUST_BACKTRACE: unset
NO_COLOR: unset
TERM: xterm-256color
JS_RUNTIME_VERSION: unset
JS_RUNTIME_NAME: unset
NODE_PACKAGE_MANAGER: unset
Biome Configuration:
Status: Loaded successfully.
Path: biome.json
Formatter enabled: true
Linter enabled: true
Assist enabled: true
VCS enabled: false
HTML full support enabled: unset
Workspace:
Open Documents: 0
Linux 7.2.3-arch1-3 x86_64. Biome installed from npm as @biomejs/cli-linux-x64.
What happened?
When the stream Biome writes diagnostics to is a pipe whose reader has already
exited, the BrokenPipe unwrap at crates/biome_console/src/lib.rs:173:14 panics.
The panic hook then writes its "Biome encountered an unexpected error" banner to
that same closed stream, which panics again. Panic-during-panic calls abort(),
so the process dies on SIGABRT and leaves a core dump, rather than exiting.
This is the same unwrap as #8125 and #3469; #8125 was closed asking for a new issue with a reproduction, so here is a deterministic one.
Reproduction
Needs only biome on PATH:
mkdir -p biome-epipe-abort && cd biome-epipe-abort
echo '{ "linter": { "enabled": true, "rules": { "preset": "recommended" } } }' > biome.json
for i in $(seq 1 50); do echo "var a$i = $i == '$i';"; done > messy.js
# A: the diagnostics stream (stderr) is the closed pipe
biome check messy.js 2>&1 | head -1 >/dev/null
echo "biome exit = ${PIPESTATUS[0]}"
# B: only stdout is the closed pipe
biome check --reporter=json messy.js 2>/dev/null | (exec true)
echo "biome exit = ${PIPESTATUS[0]}"Output on 2.5.12, identical on every run:
A: biome exit = 134 # 128+6 = SIGABRT, dumps core
B: biome exit = 101 # plain Rust panic, no coreThe A/B difference is the interesting part, and I think it points straight at the
cause: when stdout is the broken pipe (B), the panic hook still has a working
stderr, prints the banner, and the process exits 101 like a normal panic. When
stderr is the broken pipe (A), the hook's own write fails too, and the second
panic turns a recoverable EPIPE into an abort().
In case A the banner never reaches the user at all — the only artifact is a core
dump. Recovered from the core with strings:
Source Location: crates/biome_console/src/lib.rs:173:14
Thread Name: main
Message: called `Result::unwrap()` on an `Err` value: Os { code: 32, kind: BrokenPipe, message: "Broken pipe" }and coredumpctl shows the self-signal and the doubled frames of the nested panic:
Signal: 6 (ABRT) si_code: SI_TKILL
Command Line: biome check <files>
Stack trace of thread ...:
#2 abort (libc.so.6 + 0x25685)
#3 0x000055ca18d3ac0a n/a (biome + 0x39c7c0a)
...
#6 0x000055ca18d3b9d1 n/a (biome + 0x39c89d1) <-- panic #2
#7 0x000055ca18d3b989 n/a (biome + 0x39c8989)
#8 0x000055ca18d3bebc n/a (biome + 0x39c8ebc)
...
#13 0x000055ca18d3b9d1 n/a (biome + 0x39c89d1) <-- panic #1, same frames
#14 0x000055ca18d3b989 n/a (biome + 0x39c8989)
#15 0x000055ca18d3bebc n/a (biome + 0x39c8ebc)How I hit it in practice
Not through a contrived pipe. An editor/agent integration runs biome check <files>
on save and captures the output; when that subprocess is interrupted or times out,
the capture pipe closes and Biome aborts. It produced a 1.1 MB core dump and a
desktop "process crashed" notification for what should have been a no-op. head,
quitting less early, and a closed terminal all do the same thing.
biome check --write aborts identically (exit 134 on the same reproduction), so the
abort can land in the middle of a write run. I went looking for damage on a 40-file
--write run and found none: every file was either untouched or fully rewritten,
nothing truncated, everything still parsed afterwards. So this is a note about blast
radius, not a corruption report.
Expected result
A closed output pipe is normal, not a bug in the user's code. Biome should treat
ErrorKind::BrokenPipe from the console writer as end-of-output and exit quietly
(conventionally exit code 0, or 141 for SIGPIPE semantics) instead of panicking.
At minimum it should not abort(): the panic hook should not write to the stream
that just failed, so a single EPIPE can't escalate into a double panic and a core
dump.
Source: biomejs/biome