Repository: uutils/coreutils (2025-07-12T13:42:34Z)

Issue:
cat: /dev/zero: EPIPE: Broken pipe when running fish shell tests
When I try to build [fish-git](https://aur.archlinux.org/packages/fish-git) and use [coreutils-hybrid](https://aur.archlinux.org/packages/coreutils-hybrid) which replaces some of the coreutils with stable uutils, the pipestatus.fish test fails due to a broken pipe. It works fine with regular coreutils though, so I think it's an issue with one of the uutils binaries

```
89/184 Test  #89: pipestatus.fish...........................***Failed    0.17 sec
Testing file checks/pipestatus.fish ... Failure:

  There were no remaining checks left to match stderr:9:
    cat: /dev/zero: EPIPE: Broken pipe

  Context:
    [...] from line 196 (stderr:6):
    warning: Path '/not' does not exist
    warning: An error occurred while redirecting file '/not/a/valid/path'
    warning: Path '/not' does not exist
    cat: /dev/zero: EPIPE: Broken pipe <= no more checks

  when running command:
    ../test/root/bin/fish checks/pipestatus.fish
```


Discussion:
Possible related to some of these? https://github.com/uutils/coreutils/issues?q=is%3Aissue%20state%3Aopen%20pipe
Unsure if that's the same issue but gzip fails to rebuild on Ubuntu with rust-coreutils
https://launchpadlibrarian.net/798206696/buildlog_ubuntu-questing-amd64.gzip_1.13-1ubuntu3_BUILDING.txt.gz

```
$ echo a | gzip -c > f.gz
$ perl -e '
      use POSIX qw(dup2);
      $SIG{PIPE} = "DEFAULT";
      pipe my ($read_end, $write_end);
      dup2 fileno $write_end, 1;
      close $read_end;
      exec "cat", "f.gz", "f.gz";
  '

cat: f.gz: EPIPE: Broken pipe
```



Patch:
diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs
index 74da35caccd..b1c5f9aac63 100644
--- a/src/uu/cat/src/cat.rs
+++ b/src/uu/cat/src/cat.rs
@@ -8,8 +8,10 @@
 mod platform;
 
 use crate::platform::is_unsafe_overwrite;
+use clap::{Arg, ArgAction, Command};
+use memchr::memchr2;
 use std::fs::{File, metadata};
-use std::io::{self, BufWriter, IsTerminal, Read, Write};
+use std::io::{self, BufWriter, ErrorKind, IsTerminal, Read, Write};
 /// Unix domain socket support
 #[cfg(unix)]
 use std::net::Shutdown;
@@ -19,12 +21,11 @@ use std::os::fd::AsFd;
 use std::os::unix::fs::FileTypeExt;
 #[cfg(unix)]
 use std::os::unix::net::UnixStream;
-
-use clap::{Arg, ArgAction, Command};
-use memchr::memchr2;
 use thiserror::Error;
 use uucore::display::Quotable;
 use uucore::error::UResult;
+#[cfg(not(target_os = "windows"))]
+use uucore::libc;
 use uucore::locale::get_message;
 use uucore::{fast_inc::fast_inc_one, format_usage};
 
@@ -220,6 +221,15 @@ mod options {
 
 #[uucore::main]
 pub fn uumain(args: impl uucore::Args) -> UResult<()> {
+    // When we receive a SIGPIPE signal, we want to terminate the process so
+    // that we don't print any error messages to stderr. Rust ignores SIGPIPE
+    // (see https://github.com/rust-lang/rust/issues/62569), so we restore it's
+    // default action here.
+    #[cfg(not(target_os = "windows"))]
+    unsafe {
+        libc::signal(libc::SIGPIPE, libc::SIG_DFL);
+    }
+
     let matches = uu_app().try_get_matches_from(args)?;
 
     let number_mode = if matches.get_flag(options::NUMBER_NONBLANK) {
@@ -502,7 +512,9 @@ fn write_fast<R: FdReadable>(handle: &mut InputHandle<R>) -> CatResult<()> {
                 if n == 0 {
                     break;
                 }
-                stdout_lock.write_all(&buf[..n])?;
+                stdout_lock
+                    .write_all(&buf[..n])
+                    .inspect_err(handle_broken_pipe)?;
             }
             Err(e) => return Err(e.into()),
         }
@@ -513,7 +525,7 @@ fn write_fast<R: FdReadable>(handle: &mut InputHandle<R>) -> CatResult<()> {
     // that will succeed, data pushed through splice will be output before
     // the data buffered in stdout.lock. Therefore additional explicit flush
     // is required here.
-    stdout_lock.flush()?;
+    stdout_lock.flush().inspect_err(handle_broken_pipe)?;
     Ok(())
 }
 
@@ -584,7 +596,7 @@ fn write_lines<R: FdReadable>(
         // and not be buffered internally to the `cat` process.
         // Hence it's necessary to flush our buffer before every time we could potentially block
         // on a `std::io::Read::read` call.
-        writer.flush()?;
+        writer.flush().inspect_err(handle_broken_pipe)?;
     }
 
     Ok(())
@@ -704,11 +716,18 @@ fn write_end_of_line<W: Write>(
 ) -> CatResult<()> {
     writer.write_all(end_of_line)?;
     if is_interactive {
-        writer.flush()?;
+        writer.flush().inspect_err(handle_broken_pipe)?;
     }
     Ok(())
 }
 
+fn handle_broken_pipe(error: &io::Error) {
+    // SIGPIPE is not available on Windows.
+    if cfg!(target_os = "windows") && error.kind() == ErrorKind::BrokenPipe {
+        std::process::exit(13);
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use std::io::{BufWriter, stdout};


Test patch:
diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs
index 451d7f8f7f2..c3e25c6d010 100644
--- a/tests/by-util/test_cat.rs
+++ b/tests/by-util/test_cat.rs
@@ -119,6 +119,20 @@ fn test_closes_file_descriptors() {
         .succeeds();
 }
 
+#[test]
+#[cfg(unix)]
+fn test_broken_pipe() {
+    let mut cmd = new_ucmd!();
+    let mut child = cmd
+        .set_stdin(Stdio::from(File::open("/dev/zero").unwrap()))
+        .set_stdout(Stdio::piped())
+        .run_no_wait();
+    // Dropping the stdout should not lead to an error.
+    // The "Broken pipe" error should be silently ignored.
+    child.close_stdout();
+    child.wait().unwrap().fails_silently();
+}
+
 #[test]
 #[cfg(unix)]
 fn test_piped_to_regular_file() {
diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs
index ec4f46401d2..1e40a279146 100644
--- a/tests/by-util/test_tail.rs
+++ b/tests/by-util/test_tail.rs
@@ -4898,10 +4898,11 @@ fn test_when_piped_input_then_no_broken_pipe() {
 }
 
 #[test]
+#[cfg(unix)]
 fn test_when_output_closed_then_no_broken_pie() {
     let mut cmd = new_ucmd!();
     let mut child = cmd
-        .args(&[FOOBAR_TXT])
+        .args(&["-c", "100000", "/dev/zero"])
         .set_stdout(Stdio::piped())
         .run_no_wait();
     // Dropping the stdout should not lead to an error.