#11817·biome

`biome check` aborts with SIGABRT (double panic) when its diagnostics stream is a closed pipe

Author: vksargsyanCreated Sep 16, 2026Updated Sep 17, 2026

Environment information

bash
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:

bash
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 core

The 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.