fix(global-cli): restore SIGPIPE default to silence broken-pipe crashes - #2784
Dev-next-gen wants to merge 2 commits into
Conversation
When `vp`'s stdout is piped to a reader that exits early (`vp --version | head -n 1`), Rust's default SIGPIPE-ignore disposition causes `println!` to hit EPIPE, which panics the standard library. Because the release profile sets `panic = "abort"`, the process terminates with SIGABRT (exit 134) instead of the quiet SIGPIPE exit (141) that standard Unix tools produce. Each abort leaves a systemd coredump and can trigger crash-notification loops that re-invoke `vp`. The root cause is that Rust ignores SIGPIPE at startup so async I/O does not race with signal delivery, but this trades one footgun for another when writing to a closed pipe. Restoring `SIG_DFL` once at process entry — before any I/O or thread spawning — is the established fix used by ripgrep, bat, fd, and other Rust CLI tools. Adds `restore_sigpipe_default()` to `vp_shared::stdio` (alongside the existing `ensure_blocking_stdio`) and calls it at the very top of `async fn main()`. The `signal` feature is added to the `nix` workspace dependency that `vp_shared` already uses. Closes voidzero-dev#2661.
|
Could you add the reproduce case described in #2661 as a snapshot test to ensure the issue is resolved? |
Reproduces the case from voidzero-dev#2661: `vp --version | head -n 1` must exit with SIGPIPE (141), not SIGABRT (134). Uses bash PIPESTATUS to surface the vp process exit code through the pipeline.
|
Added in 26629b7. The new fixture The CI failure on "Test (Linux x64 musl)" is unrelated: Found by a defect-hunting pipeline I build and run (Dev-next-gen), using Claude Code with Anthropic's Claude Opus 5. |
naokihaba
left a comment
There was a problem hiding this comment.
Thanks for looking into this. I was actually already investigating #2661 since it was assigned to me, and put together an alternative fix over in #2785. Before we settle on an approach, though, I wanted to share a quick concern.
Restoring SIGPIPE to SIG_DFL affects signal behavior across the entire process, including pipe and socket writes unrelated to stdout. For example, Vite Task’s FIFO transport explicitly relies on Rust ignoring SIGPIPE so disconnected clients result in normal write errors (see https://github.com/voidzero-dev/vite-task/blob/3aac49e31fba6905bb0b3d0e29d7755493241e9c/crates/socket_ipc/src/unix.rs#L32-L38).
While vp_global_cli doesn't use that transport directly right now, introducing a process-wide change like this might become a constraint for future integration. On top of that, because the call is inside the #[tokio::main] body, it runs after the Tokio runtime is constructed. That means the comment about running before any threads are spawned might not be entirely accurate.
|
Your concern about the process-wide scope is fair, and the vite-task FIFO transport comment at I looked at your alternative in #2785. Scoping BrokenPipe handling to the specific output paths avoids both the IPC concern and the timing issue, which is a cleaner boundary for a CLI that might share a process with transport code later. The tradeoff is that every new output path needs the same treatment, but that seems manageable in practice. I'll defer to whoever owns the call on which approach to merge — both fix #2661, and #2785 has the narrower blast radius. Found by a defect-hunting pipeline I build and run (Dev-next-gen), using Claude Code with Anthropic's Claude Opus 5. |
|
Let's move to #2785 |
Summary
When
vp's stdout is piped to a reader that exits early, the process abortswith SIGABRT (exit 134) and leaves a systemd coredump instead of exiting
silently. This PR fixes that.
Root cause: Rust ignores SIGPIPE at startup. When
println!hits EPIPE,the standard library panics. Because the release profile sets
panic = "abort",the panic becomes SIGABRT. Stacktrace from the issue report:
Fix: restore
SIGPIPEtoSIG_DFLat process entry, before any I/O.This is the established pattern used by
ripgrep,bat,fd, and otherwell-behaved Rust CLI tools. After the fix, piping to
headexits withSIGPIPE (141) — the expected Unix behavior — with no coredump.
Verified locally:
Changes
vp_shared/src/stdio.rs: addrestore_sigpipe_default()alongside theexisting
ensure_blocking_stdio(). Usesnix::sys::signal(already adependency) with the new
signalfeature.vp_shared/Cargo.toml: addsignalto thenixfeature list.vp_shared/src/lib.rs: re-exportrestore_sigpipe_default.vp_global_cli/src/main.rs: callvp_shared::restore_sigpipe_default()at the very top of
main(), before any I/O or thread spawning.Closes #2661.
Found by a defect-hunting pipeline I build and run (Dev-next-gen), using Claude Code with Anthropic's Claude Opus 5.