Pádraig Brady <[email protected]> writes:
>> -write_wait (int fd, void const *buffer, size_t size)
>> +write_wait (int fd, void const *buffer, ssize_t size)
>
> It would be best to keep the same types as write()
Oh, right. For some reason I thought it used ssize_t. I guess I mixed it
up with the return type.
>> {
>> unsigned char const *buf = buffer;
>> - while (true)
>> + while (0 < size)
>
> I wouldn't avoid the write() with a zero size,
> as that can be used to probe an fd.
> (I know we don't currently, but best to leave that possibility).
Good point.
>> - return true;
>> -
>> - if (! wait_for_nonblocking_write (fd))
>> - return false;
>> -
>> + if (written <= 0)
>> + {
>> + /* Continue if FD becomes writable. */
>> + if (wait_for_nonblocking_write (fd))
>> + continue;
>> + return false;
>> + }
>> buf += written;
>> + size -= written;
>> }
>> +
>> + return true;
>> }
>> Is it possible/common for write to return zero bytes when the given
>> size
>> is greater than zero?
> Potentially yes.
My concern is the case where write repeatedly returns 0 and size is
always a possible as a result, leading to an infinite loop.
I could be overthinking it, since I don't think POSIX allows write to
return 0 when given a positive size. But as Paul mentioned, there are
quirky platforms like Solaris (and an unknown number of driver bugs that
could cause this). It would be nice not to cause infinite loops there.
How about treating this case as ENOSPC? This is done in src/dd.c and
Gnulib's lib/full-write.c, which quotes an old Linux bug. I've attached
a proposed patch that does that, along with adding a test using strace
to inject a write error.
Collin
>From adcc69b247c4cbc60d9602df7837d589e8385c13 Mon Sep 17 00:00:00 2001
Message-ID: <adcc69b247c4cbc60d9602df7837d589e8385c13.1779072260.git.collin.fu...@gmail.com>
From: Collin Funk <[email protected]>
Date: Sun, 17 May 2026 17:10:35 -0700
Subject: [PATCH] tee: fix an infinite loop when write sets errno to EAGAIN
* NEWS: Mention the bug fix.
* src/iopoll.c (close_wait): Remove function.
(write_wait): Handle errors more robustly.
* src/iopoll.h (close_wait): Remove declaration.
* src/tee.c (tee_files): Use close instead of close_wait.
* tests/tee/write-eagain.sh: New file to test for the bug.
* tests/local.mk: Add the new test.
Fixes https://bugs.gnu.org/81060
---
NEWS | 4 +++
src/iopoll.c | 59 ++++++++++++++++++++++++---------------
src/iopoll.h | 1 -
src/tee.c | 2 +-
tests/local.mk | 1 +
tests/tee/write-eagain.sh | 31 ++++++++++++++++++++
6 files changed, 73 insertions(+), 25 deletions(-)
create mode 100755 tests/tee/write-eagain.sh
diff --git a/NEWS b/NEWS
index f8c43d5ed..731d307b5 100644
--- a/NEWS
+++ b/NEWS
@@ -15,6 +15,10 @@ GNU coreutils NEWS -*- outline -*-
'shred' no longer blocks when opening a FIFO that has no readers.
[This bug was present in "the beginning".]
+ 'tee' no longer loops infinitely after writing all output if a write call sets
+ errno to EAGAIN.
+ [bug introduced in coreutils-9.11]
+
'unexpand -t' no longer overflows a heap buffer, for tab values > SIZE_MAX/16.
[bug introduced in coreutils-9.11]
diff --git a/src/iopoll.c b/src/iopoll.c
index de20bc8d9..0bd2d6bf6 100644
--- a/src/iopoll.c
+++ b/src/iopoll.c
@@ -194,17 +194,6 @@ wait_for_nonblocking_write (int fd)
return true;
}
-/* wrapper for close() that also waits for FD if non blocking. */
-
-extern bool
-close_wait (int fd)
-{
- while (wait_for_nonblocking_write (fd))
- ;
- return close (fd) == 0;
-}
-
-
/* wrapper for write() that also waits for FD if non blocking. */
extern bool
@@ -212,19 +201,43 @@ write_wait (int fd, void const *buffer, size_t size)
{
unsigned char const *buf = buffer;
- while (true)
+ do
{
- ssize_t written = write (fd, buf, size);
- if (written < 0)
- written = 0;
-
- size -= written;
- if (size <= 0) /* everything written */
- return true;
-
- if (! wait_for_nonblocking_write (fd))
- return false;
+ const ssize_t written = write (fd, buf, size);
+ /* POSIX says that calling write with SIZE of zero may detect and
+ return errors. If no error occurs, or write makes no attempt
+ to detect errors, then write returns zero with no other
+ results. write_fail will return successfully in this case. */
+ if (written == 0)
+ {
+ if (size == 0)
+ return true;
+ else
+ {
+ /* If SIZE is greater than zero and write returns zero,
+ treat it as an error. Some buggy drivers behave this
+ way. See src/dd.c and Gnulib's lib/full-write.c for
+ more details. */
+ errno = ENOSPC;
+ return false;
+ }
+ }
- buf += written;
+ if (written < 0)
+ {
+ /* Return an error if write detected one with a SIZE of zero.
+ Otherwise, if SIZE is greater than zero, fail if it does
+ not become writable. */
+ if (size == 0 || ! wait_for_nonblocking_write (fd))
+ return false;
+ }
+ else
+ {
+ buf += written;
+ size -= written;
+ }
}
+ while (0 < size);
+
+ return true;
}
diff --git a/src/iopoll.h b/src/iopoll.h
index 1711fdab9..a1561b9ff 100644
--- a/src/iopoll.h
+++ b/src/iopoll.h
@@ -5,5 +5,4 @@ int iopoll (int fdin, int fdout, bool block);
bool iopoll_input_ok (int fdin);
bool iopoll_output_ok (int fdout);
-bool close_wait (int fd);
bool write_wait (int fd, void const *buffer, size_t size);
diff --git a/src/tee.c b/src/tee.c
index 32a18e340..fba6ae089 100644
--- a/src/tee.c
+++ b/src/tee.c
@@ -329,7 +329,7 @@ tee_files (int nfiles, char **files, bool pipe_check)
/* Close the files, but not standard output. */
for (int i = 1; i <= nfiles; i++)
- if (0 <= descriptors[i] && ! close_wait (descriptors[i]))
+ if (0 <= descriptors[i] && close (descriptors[i]) < 0)
{
error (0, errno, "%s", quotef (files[i]));
ok = false;
diff --git a/tests/local.mk b/tests/local.mk
index fd96d7f3a..6d9a8c511 100644
--- a/tests/local.mk
+++ b/tests/local.mk
@@ -491,6 +491,7 @@ all_tests = \
tests/tail/tail.pl \
tests/tee/append.sh \
tests/tee/tee.sh \
+ tests/tee/write-eagain.sh \
tests/test/test-N.sh \
tests/test/test-diag.pl \
tests/test/test-file.sh \
diff --git a/tests/tee/write-eagain.sh b/tests/tee/write-eagain.sh
new file mode 100755
index 000000000..f434ee62b
--- /dev/null
+++ b/tests/tee/write-eagain.sh
@@ -0,0 +1,31 @@
+#!/bin/sh
+# Test 'tee' when a write fails with errno set to EAGAIN.
+
+# Copyright (C) 2026 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_ tee
+require_strace_ write
+
+# In coreutils-9.11 the following test would infinite loop.
+echo a >exp || framework_failure_
+timeout 10 strace -qqq -o /dev/null -e trace-fds=3 \
+ -e inject=write:error=EAGAIN:when=1 tee file1 <exp >out 2>err || fail=1
+compare exp file1 || fail=1
+compare exp out || fail=1
+compare /dev/null err || fail=1
+
+Exit $fail
--
2.54.0