mirror of
https://github.com/coreutils/coreutils.git
synced 2026-10-28 09:44:34 +08:00
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:
1 parent
fbd3072360
commit
0d6fcb99d6
8 files changed
+111
-25
No files matched your search
@@ -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]
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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 \
|
||||
|
||||
Executable
+33
@@ -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
|
||||
Executable
+31
@@ -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
|
||||
Reference in new issue
Block a user