Subject: bug#64540: [PATCH] od: exit out on failure to write to stdout

From: Pádraig Brady <[email-masked]>
Date: Sat, 15 Jul 2023 22:08:38 +0100

On 09/07/2023 23:54, Pádraig Brady wrote:
> On 09/07/2023 20:11, Paul Eggert wrote:
>> On 2023-07-09 07:11, Pádraig Brady wrote:
>>
>>> Note the patch looks wrong as it would close the input always.
>>> We can fix that up easily enough anyway.
>>
>> If it's easy and doesn't hurt performance in the usual case, that's of
>> course fine.
>>
>>
>>> For my reference a short list of utils to check (that might inf loop) are:
>>>      cat comm cp cut dd dirname expand factor fmt fold head join ls nl numfmt
>>>      od paste pr printf ptx readlink realpath seq shuf sort stat tac tail
>>>      tee tr tsort unexpand uniq wc yes
>>
>> There's no point to complicating programs that have bounded input for
>> other reasons, as they'll report errors soon enough for those other
>> reasons. The main problem comes from programs like 'cat' and 'od' that
>> can read from "infinite" input like /dev/urandom and can write indefinitely.
>>
>> So I think we needn't worry about dirname, ptx, readlink, realpath,
>> sort, stat, tac, tsort, wc. For example, we needn't worry about 'sort'
>> because it needs to read all its input before outputting anything, and
>> it cannot read all of /dev/urandom. As long as these programs eventually
>> check for write errors, which I expect they all do, that should be good
>> enough.
>>
>> Even for programs like 'od' there is no need to check every output
>> function; just check occasionally, when it's convenient.
> 
> Yes completely agreed.
> The shortlist above is for my reference for commands to review,
> not for commands to adjust.

The attached patch-set addresses two classes of issue:
1. Doubled error messages upon write errors
2. Continued processing upon write errors  (the orig problem reported).

For class 1, we include fixes for:
   expand, factor, paste, shuf, tr, unexpand

For class 2, we include fixes for:
   od, uniq, cut, comm, join
For class 2, we do _not_ include fixes for these utils
which are more awkward / less important in this context:
   fmt, fold, nl, numfmt, pr

I'll update NEWS with this info before pushing.

cheers,
Pádraig
From b09caad2f11dd6b4ee434c70e07c22bb834679ee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C3=A1draig=20Brady?= <[email-masked]>
Date: Tue, 11 Jul 2023 13:28:53 +0100
Subject: [PATCH 1/7] tests: ensure utilties exit promptly upon write error

* tests/local.mk: Reference the new test.
* tests/misc/write-errors.sh: A new test to ensure utilities
exit promptly upon writing to /dev/full.
---
 tests/local.mk             |  1 +
 tests/misc/write-errors.sh | 70 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 71 insertions(+)
 create mode 100755 tests/misc/write-errors.sh

diff --git a/tests/local.mk b/tests/local.mk
index cc65fe656..f35728229 100644
--- a/tests/local.mk
+++ b/tests/local.mk
@@ -174,6 +174,7 @@ all_tests =					\
   tests/cp/no-ctx.sh				\
   tests/tty/tty-eof.pl				\
   tests/misc/read-errors.sh			\
+  tests/misc/write-errors.sh			\
   tests/tail/inotify-hash-abuse.sh		\
   tests/tail/inotify-hash-abuse2.sh		\
   tests/tail/F-vs-missing.sh			\
diff --git a/tests/misc/write-errors.sh b/tests/misc/write-errors.sh
new file mode 100755
index 000000000..f3b2f6c75
--- /dev/null
+++ b/tests/misc/write-errors.sh
@@ -0,0 +1,70 @@
+#!/bin/sh
+# Make sure all of these programs promptly diagnose write errors.
+
+# Copyright (C) 2023 Free Software Foundation, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
+print_ver_ timeout
+
+if ! test -w /dev/full || ! test -c /dev/full; then
+  skip_ 'Need /dev/full'
+fi
+
+# Writers that may output data indefinitely
+# First word in command line is checked against built programs
+echo "\
+cat /dev/zero
+# TODO: comm -z /dev/zero /dev/zero
+# TODO: cut -z -c1- /dev/zero
+dd if=/dev/zero
+expand /dev/zero
+# TODO: avoid double error from expand
+factor --version; yes 1 | factor
+# TODO: avoid double error from factor
+# TODO: fmt /dev/zero
+# TODO: fold -b /dev/zero
+head -z -n-1 /dev/zero
+# TODO: join -a 1 -z /dev/zero /dev/null
+# TODO: nl --version; yes | nl
+# TODO: numfmt --version; yes 1 | numfmt
+# TODO: od -v /dev/zero
+paste /dev/zero
+# TODO: avoid double error from paste
+# TODO: pr /dev/zero
+seq inf
+# TODO: avoid double error from shuf
+tail -n+1 -z /dev/zero
+tee < /dev/zero
+tr . . < /dev/zero
+# TODO: avoid double error from tr
+unexpand /dev/zero
+# TODO: avoid double error from unexpand
+# TODO: uniq -z -D /dev/zero
+yes
+" |
+sort -k 1b,1 > all_writers || framework_failure_
+
+printf '%s\n' $built_programs |
+sort -k 1b,1 > built_programs || framework_failure_
+
+join all_writers built_programs > built_writers || framework_failure_
+
+while read writer; do
+  timeout 10 $SHELL -c "$writer > /dev/full"
+  test $? = 124 && { fail=1; echo "$writer: failed to exit" >&2; }
+done < built_writers
+
+Exit $fail
-- 
2.41.0

From 8004dcd7adec9938de2dcd9fccbd269dcdd926b1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C3=A1draig=20Brady?= <[email-masked]>
Date: Sat, 15 Jul 2023 20:41:44 +0100
Subject: [PATCH 2/7] all: avoid repeated diagnostic upon write error

* cfg.mk (sc_some_programs_must_avoid_exit_failure): Adjust to
avoid false positive.
(sc_prohibit_exit_write_error): A new syntax check to prohibit
open coding error(..., "write error"); instead directiing to use...
* src/system.h (write_error): ... a new function to clear stdout errors
before we explicitly diagnose a write error and exit.
* src/basenc.c: Use write_error() to ensure no repeated diagnostics.
* src/cat.c: Likewise.
* src/expand.c: Likewise.
* src/factor.c: Likewise.
* src/paste.c: Likewise.
* src/seq.c: Likewise.
* src/shuf.c: Likewise.
* src/split.c: Likewise.
* src/tail.c: Likewise.
* src/tr.c: Likewise.
* src/unexpand.c: Likewise.
* tests/misc/write-errors.sh: Remove TODOs for the fixed utilities:
expand, factor, paste, shuf, tr, unexpand.
---
 cfg.mk                     |  9 ++++++++-
 src/basenc.c               | 10 +++++-----
 src/cat.c                  |  8 ++++----
 src/expand.c               |  4 ++--
 src/factor.c               |  2 +-
 src/paste.c                |  8 --------
 src/seq.c                  | 18 +++++-------------
 src/shuf.c                 |  2 +-
 src/split.c                |  7 +++----
 src/system.h               |  8 ++++++++
 src/tail.c                 |  6 +++---
 src/tr.c                   |  6 +++---
 src/unexpand.c             |  4 ++--
 tests/misc/write-errors.sh |  6 ------
 14 files changed, 45 insertions(+), 53 deletions(-)

diff --git a/cfg.mk b/cfg.mk
index e7b0519c4..2d6856e15 100644
--- a/cfg.mk
+++ b/cfg.mk
@@ -503,6 +503,12 @@ sc_prohibit_man_see_also_period:
 	  { echo '$(ME): do not end "SEE ALSO" section with a period'	\
 	      1>&2; exit 1; } || :
 
+sc_prohibit_exit_write_error:
+	@prohibit='error.*EXIT_FAILURE.*write error' \
+	in_vc_files='\.c$$' \
+	halt='Use write_error() instead' \
+	  $(_sc_search_regexp)
+
 # Don't use "indent-tabs-mode: nil" anymore.  No longer needed.
 sc_prohibit_emacs__indent_tabs_mode__setting:
 	@prohibit='^( *[*#] *)?indent-tabs-mode:'			\
@@ -620,7 +626,8 @@ sc_prohibit_test_empty:
 sc_some_programs_must_avoid_exit_failure:
 	@cd $(srcdir)							\
 	&& grep -nw EXIT_FAILURE					\
-	    $$(git grep -El '[^T]_FAILURE|EXIT_CANCELED' $(srcdir)/src)	\
+	    $$(git grep -El '[^T]_FAILURE|EXIT_CANCELED' src/)		\
+	  | grep -v '^src/system\.h:'					\
 	  | grep -vE '= EXIT_FAILURE|return .* \?' | grep .		\
 	    && { echo '$(ME): do not use EXIT_FAILURE in the above'	\
 		  1>&2; exit 1; } || :
diff --git a/src/basenc.c b/src/basenc.c
index 9152ad0de..be7a61641 100644
--- a/src/basenc.c
+++ b/src/basenc.c
@@ -924,7 +924,7 @@ wrap_write (char const *buffer, idx_t len,
     {
       /* Simple write. */
       if (fwrite (buffer, 1, len, stdout) < len)
-        error (EXIT_FAILURE, errno, _("write error"));
+        write_error ();
     }
   else
     for (idx_t written = 0; written < len; )
@@ -934,13 +934,13 @@ wrap_write (char const *buffer, idx_t len,
         if (to_write == 0)
           {
             if (fputc ('\n', out) == EOF)
-              error (EXIT_FAILURE, errno, _("write error"));
+              write_error ();
             *current_column = 0;
           }
         else
           {
             if (fwrite (buffer + written, 1, to_write, stdout) < to_write)
-              error (EXIT_FAILURE, errno, _("write error"));
+              write_error ();
             *current_column += to_write;
             written += to_write;
           }
@@ -997,7 +997,7 @@ do_encode (FILE *in, char const *infile, FILE *out, idx_t wrap_column)
 
   /* When wrapping, terminate last line. */
   if (wrap_column && current_column > 0 && fputc ('\n', out) == EOF)
-    error (EXIT_FAILURE, errno, _("write error"));
+    write_error ();
 
   if (ferror (in))
     error (EXIT_FAILURE, errno, _("read error"));
@@ -1060,7 +1060,7 @@ do_decode (FILE *in, char const *infile, FILE *out, bool ignore_garbage)
           ok = base_decode_ctx (&ctx, inbuf, (k == 0 ? sum : 0), outbuf, &n);
 
           if (fwrite (outbuf, 1, n, out) < n)
-            error (EXIT_FAILURE, errno, _("write error"));
+            write_error ();
 
           if (!ok)
             error (EXIT_FAILURE, 0, _("invalid input"));
diff --git a/src/cat.c b/src/cat.c
index 880fcf2e7..b109998bf 100644
--- a/src/cat.c
+++ b/src/cat.c
@@ -178,7 +178,7 @@ simple_cat (char *buf, idx_t bufsize)
       /* Write this block out.  */
 
       if (full_write (STDOUT_FILENO, buf, n_read) != n_read)
-        error (EXIT_FAILURE, errno, _("write error"));
+        write_error ();
     }
 }
 
@@ -193,7 +193,7 @@ write_pending (char *outbuf, char **bpout)
   if (0 < n_write)
     {
       if (full_write (STDOUT_FILENO, outbuf, n_write) != n_write)
-        error (EXIT_FAILURE, errno, _("write error"));
+        write_error ();
       *bpout = outbuf;
     }
 }
@@ -257,7 +257,7 @@ cat (char *inbuf, idx_t insize, char *outbuf, idx_t outsize,
               do
                 {
                   if (full_write (STDOUT_FILENO, wp, outsize) != outsize)
-                    error (EXIT_FAILURE, errno, _("write error"));
+                    write_error ();
                   wp += outsize;
                   remaining_bytes = bpout - wp;
                 }
@@ -794,7 +794,7 @@ main (int argc, char **argv)
   if (pending_cr)
     {
       if (full_write (STDOUT_FILENO, "\r", 1) != 1)
-        error (EXIT_FAILURE, errno, _("write error"));
+        write_error ();
     }
 
   if (have_read_stdin && close (STDIN_FILENO) < 0)
diff --git a/src/expand.c b/src/expand.c
index adad9d9d8..0e74d0cf6 100644
--- a/src/expand.c
+++ b/src/expand.c
@@ -144,7 +144,7 @@ expand (void)
 
                   while (++column < next_tab_column)
                     if (putchar (' ') < 0)
-                      error (EXIT_FAILURE, errno, _("write error"));
+                      write_error ();
 
                   c = ' ';
                 }
@@ -169,7 +169,7 @@ expand (void)
             return;
 
           if (putchar (c) < 0)
-            error (EXIT_FAILURE, errno, _("write error"));
+            write_error ();
         }
       while (c != '\n');
     }
diff --git a/src/factor.c b/src/factor.c
index 8ea00a13e..e0a98152c 100644
--- a/src/factor.c
+++ b/src/factor.c
@@ -2361,7 +2361,7 @@ lbuf_flush (void)
 {
   size_t size = lbuf.end - lbuf.buf;
   if (full_write (STDOUT_FILENO, lbuf.buf, size) != size)
-    error (EXIT_FAILURE, errno, "%s", _("write error"));
+    write_error ();
   lbuf.end = lbuf.buf;
 }
 
diff --git a/src/paste.c b/src/paste.c
index 73ddd1de6..68f8a36ef 100644
--- a/src/paste.c
+++ b/src/paste.c
@@ -152,14 +152,6 @@ collapse_escapes (char const *strptr)
   return backslash_at_end ? 1 : 0;
 }
 
-/* Report a write error and exit.  */
-
-static void
-write_error (void)
-{
-  error (EXIT_FAILURE, errno, _("write error"));
-}
-
 /* Output a single byte, reporting any write errors.  */
 
 static inline void
diff --git a/src/seq.c b/src/seq.c
index 633189c40..977702bb5 100644
--- a/src/seq.c
+++ b/src/seq.c
@@ -285,14 +285,6 @@ long_double_format (char const *fmt, struct layout *layout)
       }
 }
 
-static void
-io_error (void)
-{
-  /* FIXME: consider option to silently ignore errno=EPIPE */
-  clearerr (stdout);
-  error (EXIT_FAILURE, errno, _("write error"));
-}
-
 /* Actually print the sequence of numbers in the specified range, with the
    given or default stepping and format.  */
 
@@ -311,7 +303,7 @@ print_numbers (char const *fmt, struct layout layout,
         {
           long double x0 = x;
           if (printf (fmt, x) < 0)
-            io_error ();
+            write_error ();
           if (out_of_range)
             break;
           x = first + i * step;
@@ -358,11 +350,11 @@ print_numbers (char const *fmt, struct layout layout,
             }
 
           if (fputs (separator, stdout) == EOF)
-            io_error ();
+            write_error ();
         }
 
       if (fputs (terminator, stdout) == EOF)
-        io_error ();
+        write_error ();
     }
 }
 
@@ -539,7 +531,7 @@ seq_fast (char const *a, char const *b, uintmax_t step)
           if (buf_end - (p_len + 1) < bufp)
             {
               if (fwrite (buf, bufp - buf, 1, stdout) != 1)
-                io_error ();
+                write_error ();
               bufp = buf;
             }
         }
@@ -547,7 +539,7 @@ seq_fast (char const *a, char const *b, uintmax_t step)
       /* Write any remaining buffered output, and the terminator.  */
       *bufp++ = *terminator;
       if (fwrite (buf, bufp - buf, 1, stdout) != 1)
-        io_error ();
+        write_error ();
     }
 
   if (ok)
diff --git a/src/shuf.c b/src/shuf.c
index 436f327e1..be07c4a47 100644
--- a/src/shuf.c
+++ b/src/shuf.c
@@ -597,7 +597,7 @@ main (int argc, char **argv)
     }
 
   if (i != 0)
-    error (EXIT_FAILURE, errno, _("write error"));
+    write_error ();
 
   main_exit (EXIT_SUCCESS);
 }
diff --git a/src/split.c b/src/split.c
index a3ab8d88b..bfe391728 100644
--- a/src/split.c
+++ b/src/split.c
@@ -957,7 +957,7 @@ lines_chunk_split (intmax_t k, intmax_t n, char *buf, idx_t bufsize,
                  large chunks from an existing file, so it's more efficient
                  to write out directly.  */
               if (full_write (STDOUT_FILENO, bp, to_write) != to_write)
-                error (EXIT_FAILURE, errno, "%s", _("write error"));
+                write_error ();
             }
           else if (! k)
             cwrite (new_file_flag, bp, to_write);
@@ -1214,12 +1214,11 @@ lines_rr (intmax_t k, intmax_t n, char *buf, idx_t bufsize, of_t **filesp)
               if (line_no == k && unbuffered)
                 {
                   if (full_write (STDOUT_FILENO, bp, to_write) != to_write)
-                    error (EXIT_FAILURE, errno, "%s", _("write error"));
+                    write_error ();
                 }
               else if (line_no == k && fwrite (bp, to_write, 1, stdout) != 1)
                 {
-                  clearerr (stdout); /* To silence close_stdout().  */
-                  error (EXIT_FAILURE, errno, "%s", _("write error"));
+                  write_error ();
                 }
               if (next)
                 line_no = (line_no == n) ? 1 : line_no + 1;
diff --git a/src/system.h b/src/system.h
index c8c8d52f2..6c0883f4d 100644
--- a/src/system.h
+++ b/src/system.h
@@ -762,6 +762,14 @@ The following directory is part of the cycle:\n  %s\n"), \
     }					\
   while (0)
 
+static inline void
+write_error (void)
+{
+  fflush (stdout);    /* Ensure nothing buffered that might induce an error. */
+  clearerr (stdout);  /* To avoid extraneous diagnostic from close_stdout.  */
+  error (EXIT_FAILURE, errno, _("write error"));
+}
+
 /* Like stpncpy, but do ensure that the result is NUL-terminated,
    and do not NUL-pad out to LEN.  I.e., when strnlen (src, len) == len,
    this function writes a NUL byte into dest[len].  Thus, the length
diff --git a/src/tail.c b/src/tail.c
index db0913652..a7e681f8e 100644
--- a/src/tail.c
+++ b/src/tail.c
@@ -1263,7 +1263,7 @@ tail_forever (struct File_spec *f, size_t n_files, double sleep_interval)
         }
 
       if ((!any_input || blocking) && fflush (stdout) != 0)
-        error (EXIT_FAILURE, errno, _("write error"));
+        write_error ();
 
       check_output_alive ();
 
@@ -1417,7 +1417,7 @@ check_fspec (struct File_spec *fspec, struct File_spec **prev_fspec)
     {
       *prev_fspec = fspec;
       if (fflush (stdout) != 0)
-        error (EXIT_FAILURE, errno, _("write error"));
+        write_error ();
     }
 }
 
@@ -2454,7 +2454,7 @@ main (int argc, char **argv)
                  tail_forever_inotify flushes only after writing,
                  not before reading.  */
               if (fflush (stdout) != 0)
-                error (EXIT_FAILURE, errno, _("write error"));
+                write_error ();
 
               Hash_table *ht;
               tail_forever_inotify (wd, F, n_files, sleep_interval, &ht);
diff --git a/src/tr.c b/src/tr.c
index af47021dc..799e1609e 100644
--- a/src/tr.c
+++ b/src/tr.c
@@ -1571,7 +1571,7 @@ squeeze_filter (char *buf, size_t size, size_t (*reader) (char *, size_t))
             }
           if (out_len > 0
               && fwrite (&buf[begin], 1, out_len, stdout) != out_len)
-            error (EXIT_FAILURE, errno, _("write error"));
+            write_error ();
         }
 
       if (char_to_squeeze != NOT_A_CHAR)
@@ -1797,7 +1797,7 @@ main (int argc, char **argv)
           if (nr == 0)
             break;
           if (fwrite (io_buf, 1, nr, stdout) != nr)
-            error (EXIT_FAILURE, errno, _("write error"));
+            write_error ();
         }
     }
   else if (squeeze_repeats && delete && non_option_args == 2)
@@ -1889,7 +1889,7 @@ main (int argc, char **argv)
               if (bytes_read == 0)
                 break;
               if (fwrite (io_buf, 1, bytes_read, stdout) != bytes_read)
-                error (EXIT_FAILURE, errno, _("write error"));
+                write_error ();
             }
         }
     }
diff --git a/src/unexpand.c b/src/unexpand.c
index 8505a0761..5a2283fdd 100644
--- a/src/unexpand.c
+++ b/src/unexpand.c
@@ -228,7 +228,7 @@ unexpand (void)
                   if (pending > 1 && one_blank_before_tab_stop)
                     pending_blank[0] = '\t';
                   if (fwrite (pending_blank, 1, pending, stdout) != pending)
-                    error (EXIT_FAILURE, errno, _("write error"));
+                    write_error ();
                   pending = 0;
                   one_blank_before_tab_stop = false;
                 }
@@ -244,7 +244,7 @@ unexpand (void)
             }
 
           if (putchar (c) < 0)
-            error (EXIT_FAILURE, errno, _("write error"));
+            write_error ();
         }
       while (c != '\n');
     }
diff --git a/tests/misc/write-errors.sh b/tests/misc/write-errors.sh
index f3b2f6c75..32051508b 100755
--- a/tests/misc/write-errors.sh
+++ b/tests/misc/write-errors.sh
@@ -31,9 +31,7 @@ cat /dev/zero
 # TODO: cut -z -c1- /dev/zero
 dd if=/dev/zero
 expand /dev/zero
-# TODO: avoid double error from expand
 factor --version; yes 1 | factor
-# TODO: avoid double error from factor
 # TODO: fmt /dev/zero
 # TODO: fold -b /dev/zero
 head -z -n-1 /dev/zero
@@ -42,16 +40,12 @@ head -z -n-1 /dev/zero
 # TODO: numfmt --version; yes 1 | numfmt
 # TODO: od -v /dev/zero
 paste /dev/zero
-# TODO: avoid double error from paste
 # TODO: pr /dev/zero
 seq inf
-# TODO: avoid double error from shuf
 tail -n+1 -z /dev/zero
 tee < /dev/zero
 tr . . < /dev/zero
-# TODO: avoid double error from tr
 unexpand /dev/zero
-# TODO: avoid double error from unexpand
 # TODO: uniq -z -D /dev/zero
 yes
 " |
-- 
2.41.0

From e4337d9f3dd43fdccd853846357eac57837d0334 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C3=A1draig=20Brady?= <[email-masked]>
Date: Tue, 11 Jul 2023 12:46:50 +0100
Subject: [PATCH 3/7] od: promptly diagnose write errors

* src/od.c (dump): Check for write errors after each block written,
to exit early even with large / unbounded inputs.
* tests/misc/write-errors.sh: enable od check.
* NEWS: Mention the improvement.
Fixes https://bugs.gnu.org/64540
---
 NEWS                       | 3 +++
 src/od.c                   | 8 ++++++--
 tests/misc/write-errors.sh | 2 +-
 3 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/NEWS b/NEWS
index 35ae02a8e..892db27b8 100644
--- a/NEWS
+++ b/NEWS
@@ -62,6 +62,9 @@ GNU coreutils NEWS                                    -*- outline -*-
   irrespective of which kernel version coreutils is built against,
   reinstating that behaviour from coreutils-9.0.
 
+  od will now exit immediately upon receiving a write error, which is
+  significant when reading large / unbounded inputs.
+
   split now uses more tuned access patterns for its potentially large input.
   This was seen to improve throughput by 5% when reading from SSD.
 
diff --git a/src/od.c b/src/od.c
index 6b66ceb4f..cde483c98 100644
--- a/src/od.c
+++ b/src/od.c
@@ -1380,7 +1380,7 @@ dump (void)
 
   if (limit_bytes_to_format)
     {
-      while (true)
+      while (ok)
         {
           size_t n_needed;
           if (current_offset >= end_offset)
@@ -1396,13 +1396,15 @@ dump (void)
           affirm (n_bytes_read == bytes_per_block);
           write_block (current_offset, n_bytes_read,
                        block[!idx], block[idx]);
+          if (ferror (stdout))
+            ok = false;
           current_offset += n_bytes_read;
           idx = !idx;
         }
     }
   else
     {
-      while (true)
+      while (ok)
         {
           ok &= read_block (bytes_per_block, block[idx], &n_bytes_read);
           if (n_bytes_read < bytes_per_block)
@@ -1410,6 +1412,8 @@ dump (void)
           affirm (n_bytes_read == bytes_per_block);
           write_block (current_offset, n_bytes_read,
                        block[!idx], block[idx]);
+          if (ferror (stdout))
+            ok = false;
           current_offset += n_bytes_read;
           idx = !idx;
         }
diff --git a/tests/misc/write-errors.sh b/tests/misc/write-errors.sh
index 32051508b..8cf9db88d 100755
--- a/tests/misc/write-errors.sh
+++ b/tests/misc/write-errors.sh
@@ -38,7 +38,7 @@ head -z -n-1 /dev/zero
 # TODO: join -a 1 -z /dev/zero /dev/null
 # TODO: nl --version; yes | nl
 # TODO: numfmt --version; yes 1 | numfmt
-# TODO: od -v /dev/zero
+od -v /dev/zero
 paste /dev/zero
 # TODO: pr /dev/zero
 seq inf
-- 
2.41.0

From 886fa18153977ef34ec06a811f9a517def6d40db Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C3=A1draig=20Brady?= <[email-masked]>
Date: Sat, 15 Jul 2023 21:10:38 +0100
Subject: [PATCH 4/7] uniq: promptly diagnose write errors

* src/uniq.c (write_line): Check the output from fwrite() immediately.
(check_file): Likewise.
* tests/misc/write-errors.sh: Enable the test case.
---
 src/uniq.c                 | 9 ++++++---
 tests/misc/write-errors.sh | 2 +-
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/src/uniq.c b/src/uniq.c
index 15611540b..2dbec9fcd 100644
--- a/src/uniq.c
+++ b/src/uniq.c
@@ -309,7 +309,9 @@ writeline (struct linebuffer const *line,
   if (countmode == count_occurrences)
     printf ("%7" PRIuMAX " ", linecount + 1);
 
-  fwrite (line->buffer, sizeof (char), line->length, stdout);
+  if (fwrite (line->buffer, sizeof (char), line->length, stdout)
+      != line->length)
+    write_error ();
 }
 
 /* Process input file INFILE with output to OUTFILE.
@@ -378,8 +380,9 @@ check_file (char const *infile, char const *outfile, char delimiter)
 
           if (new_group || grouping != GM_NONE)
             {
-              fwrite (thisline->buffer, sizeof (char),
-                      thisline->length, stdout);
+              if (fwrite (thisline->buffer, sizeof (char), thisline->length,
+                  stdout) != thisline->length)
+                write_error ();
 
               SWAP_LINES (prevline, thisline);
               prevfield = thisfield;
diff --git a/tests/misc/write-errors.sh b/tests/misc/write-errors.sh
index 8cf9db88d..60817bd3d 100755
--- a/tests/misc/write-errors.sh
+++ b/tests/misc/write-errors.sh
@@ -46,7 +46,7 @@ tail -n+1 -z /dev/zero
 tee < /dev/zero
 tr . . < /dev/zero
 unexpand /dev/zero
-# TODO: uniq -z -D /dev/zero
+uniq -z -D /dev/zero
 yes
 " |
 sort -k 1b,1 > all_writers || framework_failure_
-- 
2.41.0

From d1bac50e2702095813efe324b04890aab1837fdf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C3=A1draig=20Brady?= <[email-masked]>
Date: Sat, 15 Jul 2023 21:23:55 +0100
Subject: [PATCH 5/7] cut: promptly diagnose write errors

* src/cut.c (cut_bytes): Diagnose errors from fwrite() and putchar().
(cut_fields): Likewise.
* tests/misc/write-errors.sh: Enable the test for cut,
and augment to cover both cut_bytes() and cut_fields().
---
 src/cut.c                  | 32 ++++++++++++++++++++++----------
 tests/misc/write-errors.sh |  3 ++-
 2 files changed, 24 insertions(+), 11 deletions(-)

diff --git a/src/cut.c b/src/cut.c
index 12dfd2299..476df0943 100644
--- a/src/cut.c
+++ b/src/cut.c
@@ -232,7 +232,8 @@ cut_bytes (FILE *stream)
 
       if (c == line_delim)
         {
-          putchar (c);
+          if (putchar (c) < 0)
+            write_error ();
           byte_idx = 0;
           print_delimiter = false;
           current_rp = frp;
@@ -252,13 +253,16 @@ cut_bytes (FILE *stream)
                 {
                   if (print_delimiter && is_range_start_index (byte_idx))
                     {
-                      fwrite (output_delimiter_string, sizeof (char),
-                              output_delimiter_length, stdout);
+                      if (fwrite (output_delimiter_string, sizeof (char),
+                                  output_delimiter_length, stdout)
+                          != output_delimiter_length)
+                        write_error ();
                     }
                   print_delimiter = true;
                 }
 
-              putchar (c);
+              if (putchar (c) < 0)
+                write_error ();
             }
         }
     }
@@ -325,7 +329,9 @@ cut_fields (FILE *stream)
                 }
               else
                 {
-                  fwrite (field_1_buffer, sizeof (char), n_bytes, stdout);
+                  if (fwrite (field_1_buffer, sizeof (char), n_bytes, stdout)
+                      != n_bytes)
+                    write_error ();
                   /* Make sure the output line is newline terminated.  */
                   if (field_1_buffer[n_bytes - 1] != line_delim)
                     putchar (line_delim);
@@ -336,7 +342,9 @@ cut_fields (FILE *stream)
           if (print_kth (1))
             {
               /* Print the field, but not the trailing delimiter.  */
-              fwrite (field_1_buffer, sizeof (char), n_bytes - 1, stdout);
+              if (fwrite (field_1_buffer, sizeof (char), n_bytes - 1, stdout)
+                  != n_bytes - 1)
+                write_error ();
 
               /* With -d$'\n' don't treat the last '\n' as a delimiter.  */
               if (delim == line_delim)
@@ -360,14 +368,17 @@ cut_fields (FILE *stream)
         {
           if (found_any_selected_field)
             {
-              fwrite (output_delimiter_string, sizeof (char),
-                      output_delimiter_length, stdout);
+              if (fwrite (output_delimiter_string, sizeof (char),
+                          output_delimiter_length, stdout)
+                  != output_delimiter_length)
+                write_error ();
             }
           found_any_selected_field = true;
 
           while ((c = getc (stream)) != delim && c != line_delim && c != EOF)
             {
-              putchar (c);
+              if (putchar (c) < 0)
+                write_error ();
               prev_c = c;
             }
         }
@@ -398,7 +409,8 @@ cut_fields (FILE *stream)
             {
               if (c == line_delim || prev_c != line_delim
                   || delim == line_delim)
-                putchar (line_delim);
+                if (putchar (line_delim) < 0)
+                  write_error ();
             }
           if (c == EOF)
             break;
diff --git a/tests/misc/write-errors.sh b/tests/misc/write-errors.sh
index 60817bd3d..d27970e2c 100755
--- a/tests/misc/write-errors.sh
+++ b/tests/misc/write-errors.sh
@@ -28,7 +28,8 @@ fi
 echo "\
 cat /dev/zero
 # TODO: comm -z /dev/zero /dev/zero
-# TODO: cut -z -c1- /dev/zero
+cut -z -c1- /dev/zero
+cut -z -f1- /dev/zero
 dd if=/dev/zero
 expand /dev/zero
 factor --version; yes 1 | factor
-- 
2.41.0

From f67a6770b0c0907efdf5b582adb8c1b2f7c0824a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C3=A1draig=20Brady?= <[email-masked]>
Date: Sat, 15 Jul 2023 21:43:07 +0100
Subject: [PATCH 6/7] comm: promptly diagnose write errors

* src/comm.c (write_line): Call write_error() upon ferror().
* tests/misc/write-errors.sh: Enable comm test.
---
 src/comm.c                 | 21 ++++++++++++---------
 tests/misc/write-errors.sh |  2 +-
 2 files changed, 13 insertions(+), 10 deletions(-)

diff --git a/src/comm.c b/src/comm.c
index 12fa9c9ea..5cb2410e5 100644
--- a/src/comm.c
+++ b/src/comm.c
@@ -163,13 +163,13 @@ Examples:\n\
   exit (status);
 }
 
-/* Output the line in linebuffer LINE to stream STREAM
+/* Output the line in linebuffer LINE to stdout
    provided the switches say it should be output.
    CLASS is 1 for a line found only in file 1,
    2 for a line only in file 2, 3 for a line in both. */
 
 static void
-writeline (struct linebuffer const *line, FILE *stream, int class)
+writeline (struct linebuffer const *line, int class)
 {
   switch (class)
     {
@@ -182,20 +182,23 @@ writeline (struct linebuffer const *line, FILE *stream, int class)
       if (!only_file_2)
         return;
       if (only_file_1)
-        fwrite (col_sep, 1, col_sep_len, stream);
+        fwrite (col_sep, 1, col_sep_len, stdout);
       break;
 
     case 3:
       if (!both)
         return;
       if (only_file_1)
-        fwrite (col_sep, 1, col_sep_len, stream);
+        fwrite (col_sep, 1, col_sep_len, stdout);
       if (only_file_2)
-        fwrite (col_sep, 1, col_sep_len, stream);
+        fwrite (col_sep, 1, col_sep_len, stdout);
       break;
     }
 
-  fwrite (line->buffer, sizeof (char), line->length, stream);
+  fwrite (line->buffer, sizeof (char), line->length, stdout);
+
+  if (ferror (stdout))
+    write_error ();
 }
 
 /* Check that successive input lines PREV and CURRENT from input file
@@ -329,7 +332,7 @@ compare_files (char **infiles)
         {
           /* Line is seen in both files.  */
           total[2]++;
-          writeline (thisline[1], stdout, 3);
+          writeline (thisline[1], 3);
         }
       else
         {
@@ -338,13 +341,13 @@ compare_files (char **infiles)
             {
               /* Line is seen in file 1 only.  */
               total[0]++;
-              writeline (thisline[0], stdout, 1);
+              writeline (thisline[0], 1);
             }
           else
             {
               /* Line is seen in file 2 only.  */
               total[1]++;
-              writeline (thisline[1], stdout, 2);
+              writeline (thisline[1], 2);
             }
         }
 
diff --git a/tests/misc/write-errors.sh b/tests/misc/write-errors.sh
index d27970e2c..65c2cec2b 100755
--- a/tests/misc/write-errors.sh
+++ b/tests/misc/write-errors.sh
@@ -27,7 +27,7 @@ fi
 # First word in command line is checked against built programs
 echo "\
 cat /dev/zero
-# TODO: comm -z /dev/zero /dev/zero
+comm -z /dev/zero /dev/zero
 cut -z -c1- /dev/zero
 cut -z -f1- /dev/zero
 dd if=/dev/zero
-- 
2.41.0

From df1c1e4d863bc02bbe9d3588424b12a0e2fc6efe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C3=A1draig=20Brady?= <[email-masked]>
Date: Sat, 15 Jul 2023 21:55:23 +0100
Subject: [PATCH 7/7] join: promptly diagnose write errors

* src/join.c (prjoin): Check for write errors after each line.
* tests/misc/write-errors.sh: enable the test for join.
---
 src/join.c                 | 3 +++
 tests/misc/write-errors.sh | 2 +-
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/join.c b/src/join.c
index 5b58ed4d5..afbf0307e 100644
--- a/src/join.c
+++ b/src/join.c
@@ -650,6 +650,9 @@ prjoin (struct line const *line1, struct line const *line2)
 
       putchar (eolchar);
     }
+
+  if (ferror (stdout))
+    write_error ();
 }
 
 /* Print the join of the files in FP1 and FP2.  */
diff --git a/tests/misc/write-errors.sh b/tests/misc/write-errors.sh
index 65c2cec2b..b5cd24053 100755
--- a/tests/misc/write-errors.sh
+++ b/tests/misc/write-errors.sh
@@ -36,7 +36,7 @@ factor --version; yes 1 | factor
 # TODO: fmt /dev/zero
 # TODO: fold -b /dev/zero
 head -z -n-1 /dev/zero
-# TODO: join -a 1 -z /dev/zero /dev/null
+join -a 1 -z /dev/zero /dev/null
 # TODO: nl --version; yes | nl
 # TODO: numfmt --version; yes 1 | numfmt
 od -v /dev/zero
-- 
2.41.0