Skip to content

fix(global-cli): restore SIGPIPE default to silence broken-pipe crashes - #2784

Closed
Dev-next-gen wants to merge 2 commits into
voidzero-dev:mainfrom
Dev-next-gen:fix/sigpipe-abort-on-broken-pipe
Closed

Dev-next-gen wants to merge 2 commits into
voidzero-dev:mainfrom
Dev-next-gen:fix/sigpipe-abort-on-broken-pipe

Conversation

@Dev-next-gen

Copy link
Copy Markdown

Summary

When vp's stdout is piped to a reader that exits early, the process aborts
with 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:

thread 'main' panicked at library/std/src/io/stdio.rs:1168:9:
failed printing to stdout: Broken pipe (os error 32)

Fix: restore SIGPIPE to SIG_DFL at process entry, before any I/O.
This is the established pattern used by ripgrep, bat, fd, and other
well-behaved Rust CLI tools. After the fix, piping to head exits with
SIGPIPE (141) — the expected Unix behavior — with no coredump.

Verified locally:

# Before (panic=abort binary without SIGPIPE fix)
$ ./sigpipe_test | head -2
line 0
line 1
thread 'main' panicked ... failed printing to stdout: Broken pipe (os error 32)
$ echo ${PIPESTATUS[0]}
134

# After (same binary with restore_sigpipe_default() added)
$ ./sigpipe_test | head -2
line 0
line 1
$ echo ${PIPESTATUS[0]}
141

Changes

  • vp_shared/src/stdio.rs: add restore_sigpipe_default() alongside the
    existing ensure_blocking_stdio(). Uses nix::sys::signal (already a
    dependency) with the new signal feature.
  • vp_shared/Cargo.toml: add signal to the nix feature list.
  • vp_shared/src/lib.rs: re-export restore_sigpipe_default.
  • vp_global_cli/src/main.rs: call vp_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.

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.
@fengmk2

fengmk2 commented Sep 22, 2026

Copy link
Copy Markdown
Member

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.
@Dev-next-gen

Copy link
Copy Markdown
Author

Added in 26629b7. The new fixture sigpipe_version_piped runs vp --version | head -n 1 through bash and surfaces the vp exit code via PIPESTATUS[0]. The snapshot asserts exit 141 (SIGPIPE) — before the fix it was 134 (SIGABRT). Skipped on Windows where SIGPIPE does not exist.

The CI failure on "Test (Linux x64 musl)" is unrelated: progress::tests::downloads_share_one_row_and_restore_the_elapsed_spinner is a pre-existing flaky assertion in progress.rs, none of the files this PR touches.

Found by a defect-hunting pipeline I build and run (Dev-next-gen), using Claude Code with Anthropic's Claude Opus 5.

@naokihaba naokihaba left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Dev-next-gen

Copy link
Copy Markdown
Author

Your concern about the process-wide scope is fair, and the vite-task FIFO transport comment at socket_ipc/src/unix.rs:32-38 makes the dependency on Rust's default SIGPIPE suppression concrete — I hadn't traced that path. The #[tokio::main] timing point is also right: by the time the body runs, Tokio's worker threads are already live, so my comment about running before any threads was inaccurate.

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.

@fengmk2

fengmk2 commented Sep 22, 2026

Copy link
Copy Markdown
Member

Let's move to #2785

@fengmk2 fengmk2 closed this Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

vp aborts with SIGABRT when stdout is piped to a reader that exits early (| head)

3 participants