These signals are used by timeout(1) itself, so need special handling.
Since commit v8.13-11-g80a1aca0d we incorrectly ignored them,
which was exacerbated by commit v8.26-39-g2f69dba5d which caused
the -sCHLD case to hang upon child exit.
* src/timeout.c (send_sig): Don't ignore SIGHLD, and discard
the self sent SIGALRM, and rearm the handler.
* tests/timeout/timeout.sh: Add test cases.
* NEWS: Mention the fixes.
Discovered by Anthropic's OSS Scanner, as bug ANT-2026-2EQL2L05.
---
NEWS | 5 ++++
src/timeout.c | 50 +++++++++++++++++++++++++++++-----------
tests/timeout/timeout.sh | 12 +++++++++-
3 files changed, 52 insertions(+), 15 deletions(-)
diff --git a/NEWS b/NEWS
index 6c0ebb3b8..abd1cf9b7 100644
--- a/NEWS
+++ b/NEWS
@@ -70,6 +70,11 @@ GNU coreutils NEWS -*-
outline -*-
if events arrived faster than tail could process them.
[bug introduced in coreutils-7.5]
+ 'timeout --signal=ALRM -k' again honors the -k option.
+ Similarly 'timeout --signal=CHLD' no longer reports "No child processes",
+ or hangs as it has done since coreutils-8.27, once the command exits.
+ [bugs introduced in coreutils-8.14]
+
'timeout --verbose' no longer hangs or loops forever in the edge case
where writing the diagnostics would block or generate a signal respectively.
Instead these edge cases will not have a corresponding diagnostic issued.
diff --git a/src/timeout.c b/src/timeout.c
index 175bb81d4..bc6e97b18 100644
--- a/src/timeout.c
+++ b/src/timeout.c
@@ -187,10 +187,30 @@ send_sig (pid_t where, int sig)
propagate the first one, which hopefully won't be an issue. Note this
process can be implicitly multithreaded due to some timer_settime()
implementations, therefore a signal sent to the group, can be sent
- multiple times to this process. */
- if (where == 0)
- signal (sig, SIG_IGN);
- return kill (where, sig);
+ multiple times to this process. SIGCHLD is not ignored though,
+ as it's needed to notice the command exiting, and its handler
+ doesn't send signals so there is no loop to avoid. */
+ struct sigaction old_sa;
+ bool ignore = where == 0 && sig != SIGCHLD;
+ if (ignore)
+ {
+ sigaction (sig, NULL, &old_sa);
+ signal (sig, SIG_IGN);
+ }
+
+ int ret = kill (where, sig);
+
+ /* SIGALRM is needed to notice the --kill-after timeout, so restore its
+ handler. First discard the copy sent to ourselves above, which may
+ be pending rather than ignored, as SIGALRM is blocked while in the
+ SIGALRM handler. */
+ if (ignore && sig == SIGALRM)
+ {
+ signal (sig, SIG_IGN);
+ sigaction (sig, &old_sa, NULL);
+ }
+
+ return ret;
}
/* Signal handler which is required for sigsuspend() to be interrupted
@@ -222,16 +242,6 @@ cleanup (int sig)
}
if (0 < monitored_pid)
{
- if (kill_after)
- {
- int saved_errno = errno; /* settimeout may reset. */
- /* Start a new timeout after which we'll send SIGKILL. */
- term_signal = SIGKILL;
- settimeout (kill_after, false);
- kill_after = 0; /* Don't let later signals reset kill alarm. */
- errno = saved_errno;
- }
-
/* Send the signal directly to the monitored child,
in case it has itself become group leader,
or is not running in a separate group. */
@@ -249,6 +259,18 @@ cleanup (int sig)
}
}
+ /* Do this after send_sig() so the new SIGALRM can't be discarded,
+ but before diagnosing, as the diagnostic could block. */
+ if (kill_after)
+ {
+ int saved_errno = errno; /* settimeout may reset. */
+ /* Start a new timeout after which we'll send SIGKILL. */
+ term_signal = SIGKILL;
+ settimeout (kill_after, false);
+ kill_after = 0; /* Don't let later signals reset kill alarm. */
+ errno = saved_errno;
+ }
+
/* Diagnose only after sending signals, since stderr could block.
Note this implies we don't diagnose a --kill-after without -f,
but being killed will be apparent in the exit status and
diff --git a/tests/timeout/timeout.sh b/tests/timeout/timeout.sh
index 6f4467e31..8d736c7fe 100755
--- a/tests/timeout/timeout.sh
+++ b/tests/timeout/timeout.sh
@@ -17,7 +17,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
-print_ver_ timeout
+print_ver_ timeout env
require_trap_signame_
# no timeout
@@ -47,6 +47,16 @@ returns_ 124 timeout -s0 -k1 .1 sleep 10 && fail=1
# Ensure a consistent exit status with --foreground
returns_ 124 timeout --foreground -s0 -k1 .1 sleep 10 && fail=1
+# Keep handling SIGALRM after sending it to the process group.
+# Also use a tiny kill delay to check that its SIGALRM is not discarded.
+for delay in .1 1e-9; do
+ returns_ 137 timeout -s ALRM -k $delay .1 \
+ env --ignore-signal=ALRM sleep 10 || fail=1
+done
+
+# Keep handling SIGCHLD so the command's exit wakes the monitor.
+returns_ 124 timeout -s KILL 10 timeout -s CHLD .1 sleep .5 || fail=1
+
# Ensure 'timeout' is immune to parent's SIGCHLD handler
# Use a subshell and an exec to work around a bug in FreeBSD 5.0 /bin/sh.
(
--
2.55.0