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
This commit is contained in:
Collin Funk committed 2026-05-18 20:50:48 -07:00
1 parent fbd3072360
commit 0d6fcb99d6
8 files changed
+111 -25

No files matched your search

+7
View File
@@ -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]
+1
View File
@@ -86,6 +86,7 @@ Bernd Leibing bernd.leibing@rz.uni-ulm.de
Bernd Melchers melchers@cis.fu-berlin.de
Bernhard Baehr bernhard.baehr@gmx.de
Bernhard Gabler bernhard@uni-koblenz.de
Bernhard M. Wiedemann bwiedemann@suse.de
Bernhard Rosenkraenzer bero@redhat.de
Bert Deknuydt Bert.Deknuydt@esat.kuleuven.ac.be
Bert Wesarg bert.wesarg@googlemail.com
+36 -23
View File
@@ -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);
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;
}
}
if (written < 0)
written = 0;
size -= written;
if (size <= 0) /* everything written */
return true;
if (! wait_for_nonblocking_write (fd))
return false;
buf += written;
{
/* 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;
}
-1
View File
@@ -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);
+1 -1
View File
@@ -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;
+2
View File
@@ -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 \
+33
View File
@@ -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
+31
View File
@@ -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