"Bernhard M. Wiedemann" <[email protected]> writes:
> I originally reported the issue on the openSUSE bugtracker.
>
> I tested both patches ontop of coreutils-9.11.
> Both avoided the infinite loop.
> https://debbugs.gnu.org/cgi/bugreport.cgi?bug=81060#14
> stopped writing to stdout in the middle.
>
> 0001-tee-fix-an-infinite-loop-when-write-sets-errno-to-EA.patch
> Behaves nicely.
> I just am not sure how well the test covers the actual issue.
Ah, okay. Looking over it again I think we, or at least I, were
misunderstanding the first issue regarding standard out no longer being
written to. I could reproduce it, but I'll just copy the behavior also
seen in the output Bernhard Voelker sent:
read(0, "de/qt6/QtDBus -I/usr/include/KF6"..., 8192) = 8192
write(1, "de/qt6/QtDBus -I/usr/include/KF6"..., 8192) = 1661
fcntl(1, F_GETFL) = 0x8802 (flags O_RDWR|O_NONBLOCK|O_LARGEFILE)
write(2, "tee: ", 5) = -1 EAGAIN (Resource temporarily unavailable)
write(2, "'standard output'", 17) = -1 EAGAIN (Resource temporarily
unavailable)
write(2, "\n", 1) = -1 EAGAIN (Resource temporarily unavailable)
write(3, "de/qt6/QtDBus -I/usr/include/KF6"..., 8192) = 8192
read(0, "p\", because it doesn't exist, fr"..., 8192) = 8192
write(3, "p\", because it doesn't exist, fr"..., 8192) = 8192
read(0, "L_CAST_FROM_STRING -DQT_OPENGL_L"..., 8192) = 8192
write(3, "L_CAST_FROM_STRING -DQT_OPENGL_L"..., 8192) = 8192
The reason that standard output is no longer written to is an issue with
the short write, not necessary EAGAIN (although, that gives a hint about
why the short write likely occurred). I accidentally called
wait_for_nonblocking_write when writes were successful but short. This
means if errno was not EAGAIN, likely zero in this case, it would be
treated as a write error. If it were EAGAIN from some earlier system
call, such as writing to standard error, we would mistakenly call poll.
I've pushed the patch with an additional test case using 'strace' to
inject short writes, which should hopefully address your good point that
the issue wasn't fully tested.
I was 50/50 on whether these were separate bugs that should be committed
separately. However, I'm not fully confident the infinite loop is solved
without fixing write_wait. E.g., you might have a short read after some
system call sets errno to EAGAIN, instead of before as in the original
report, which would call poll to be mistakenly called. Therefore, it
felt a bit better to commit them together.
Thanks again for the report and easy reproducer.
Collin
>From 0d6fcb99d691d920961938e61c43478566ef626e Mon Sep 17 00:00:00 2001
Message-ID: <0d6fcb99d691d920961938e61c43478566ef626e.1779163853.git.collin.fu...@gmail.com>
From: Collin Funk <[email protected]>
Date: Mon, 18 May 2026 20:40:28 -0700
Subject: [PATCH v2] tee: fix infinite loop when write returns EAGAIN and short
write errors
* NEWS: Mention the bug fixes.
* THANKS.in: Add Bernhard M. Wiedemann for reporting the bugs.
* src/iopoll.c (close_wait): Remove function.
(write_wait): Don't call wait_for_nonblocking_write if write is
successful. Handle errors more robustly.
* src/iopoll.h (close_wait): Remove declaration.
* src/tee.c (tee_files): Use close instead of close_wait.
* tests/tee/short-write.sh: New test for the bug.
* tests/tee/write-eagain.sh: Likewise.
* tests/local.mk (all_tests): Add the new tests.
Fixes https://bugs.gnu.org/81060
---
NEWS | 7 +++++
THANKS.in | 1 +
src/iopoll.c | 59 ++++++++++++++++++++++++---------------
src/iopoll.h | 1 -
src/tee.c | 2 +-
tests/local.mk | 2 ++
tests/tee/short-write.sh | 33 ++++++++++++++++++++++
tests/tee/write-eagain.sh | 31 ++++++++++++++++++++
8 files changed, 111 insertions(+), 25 deletions(-)
create mode 100755 tests/tee/short-write.sh
create mode 100755 tests/tee/write-eagain.sh
diff --git a/NEWS b/NEWS
index f8c43d5ed..d4963b275 100644
--- a/NEWS
+++ b/NEWS
@@ -15,6 +15,13 @@ 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]
+
+ 'tee' no longer treats short writes as errors.
+ [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/THANKS.in b/THANKS.in
index 5a2fd3501..b1453be30 100644
--- a/THANKS.in
+++ b/THANKS.in
@@ -86,6 +86,7 @@ Bernd Leibing [email protected]
Bernd Melchers [email protected]
Bernhard Baehr [email protected]
Bernhard Gabler [email protected]
+Bernhard M. Wiedemann [email protected]
Bernhard Rosenkraenzer [email protected]
Bert Deknuydt [email protected]
Bert Wesarg [email protected]
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..7712837bc 100644
--- a/tests/local.mk
+++ b/tests/local.mk
@@ -490,7 +490,9 @@ all_tests = \
tests/tac/tac-2-nonseekable.sh \
tests/tail/tail.pl \
tests/tee/append.sh \
+ tests/tee/short-write.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/short-write.sh b/tests/tee/short-write.sh
new file mode 100755
index 000000000..42709260d
--- /dev/null
+++ b/tests/tee/short-write.sh
@@ -0,0 +1,33 @@
+#!/bin/sh
+# Test 'tee' when a write is short.
+
+# 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
+
+printf 'abcdef' >file1-exp || framework_failure_
+printf 'f' >out-exp || framework_failure_
+
+# In coreutils-9.11, a short write would be treated as an error.
+strace -qqq -o /dev/null --trace-fds=1 -e trace=write \
+ -e inject=write:retval=1:when=1..5 tee file1 >out 2>err <file1-exp || fail=1
+compare file1-exp file1 || fail=1
+compare out-exp out || fail=1
+compare /dev/null err || fail=1
+
+Exit $fail
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