Skip to content

fix(cli): restore the default SIGPIPE disposition - #377

Merged
domenkozar merged 1 commit into
cachix:mainfrom
frdminc:fix/sigpipe-default-disposition
Aug 19, 2026
Merged

fix(cli): restore the default SIGPIPE disposition#377
domenkozar merged 1 commit into
cachix:mainfrom
frdminc:fix/sigpipe-default-disposition

Conversation

@djbclark

Copy link
Copy Markdown
Contributor

Follow-up to point 3 of your review on #373.

Rust's runtime ignores SIGPIPE, so a closed output pipe comes back as an
EPIPE write error instead of terminating the process. secretspec then
fails where every other Unix tool exits quietly:

$ secretspec export --format dotenv | head
Error:   × Failed to export secrets
  ├─▶ IO error: Broken pipe (os error 32)
  ╰─▶ Broken pipe (os error 32)
$ echo $?
1

$ secretspec check --json | head
thread 'main' panicked at library/std/src/io/stdio.rs:1166:9:
failed printing to stdout: Broken pipe (os error 32)
$ echo $?
101

check --json is the worse of the two: it panics out of println! rather
than returning an error.

The fix

Reset the disposition to SIG_DFL at the top of the binary entry point. Doing
it there fixes every stdout-writing command at once, instead of teaching each
call site to special-case EPIPE — which is why this is one small commit
rather than three.

After the change, both commands terminate on signal 13 with empty stderr,
and unpiped output is byte-for-byte unchanged.

Tests

secretspec/tests/sigpipe.rs covers both paths: it spawns the real binary,
reads a few bytes, drops the read end, and asserts termination by SIGPIPE
with no stderr. The secret count is sized so output exceeds the ~64KiB pipe
buffer — below that the write fits in the buffer and the pipe never breaks.

I checked both tests fail without the fix, with exactly the errors quoted
above, so they're genuinely pinning the behavior rather than passing
vacuously.

Note on libc

New direct dependency, declared only under [target.'cfg(unix)'.dependencies].
It was already in Cargo.lock transitively, so nothing new is vendored. Happy
to drop it for a hand-rolled extern "C" declaration if you'd rather not add
the direct dep.

Rust's runtime ignores SIGPIPE, so a closed output pipe surfaces as an
EPIPE write error rather than terminating the process. `secretspec` then
fails where every other Unix tool exits quietly:

    $ secretspec export --format dotenv | head
    Error:   x Failed to export secrets
      |-> IO error: Broken pipe (os error 32)
      `-> Broken pipe (os error 32)          # exit 1

    $ secretspec check --json | head
    thread 'main' panicked at library/std/src/io/stdio.rs:1166:9:
    failed printing to stdout: Broken pipe (os error 32)   # exit 101

Resetting the disposition to SIG_DFL in the binary entry point fixes
every stdout-writing command at once, rather than teaching each call
site to special-case EPIPE. Both commands above now terminate on signal
13 with empty stderr; unpiped output is unchanged.

This is the follow-up promised in cachix#373, where the same broken-pipe
behavior came up on the `check` path.

libc is a new direct dependency, declared only under cfg(unix). It was
already in the lock file transitively, so nothing new is vendored.
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.

2 participants