* NEWS: Adjust to mention that -k without -f is resilient to blocking.
* src/timeout.c (cleanup): Unblock SIGALRM when diagnosing, so
an armed --kill-after is honored.
* tests/timeout/timeout.sh: Add a test case.
Discovered by Anthropic's OSS Scanner, as bug ANT-2026-GV4Z4T3T.
---
NEWS | 3 ++-
src/timeout.c | 11 +++++++++++
tests/timeout/timeout.sh | 8 ++++++++
3 files changed, 21 insertions(+), 1 deletion(-)
diff --git a/NEWS b/NEWS
index 87bfbbba0..6c0ebb3b8 100644
--- a/NEWS
+++ b/NEWS
@@ -73,7 +73,8 @@ GNU coreutils NEWS -*-
outline -*-
'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.
- Also --kill-after without --foreground, will not have a diagnostic issued.
+ Also --kill-after without --foreground, will not have a diagnostic issued,
+ but will now be resilient to a blocked standard error, and no longer hangs.
[hang bug introduced in coreutils-8.29, and loop bug in coreutils 9.10]
'unexpand' no longer overflows a heap buffer on non-GLIBC platforms,
diff --git a/src/timeout.c b/src/timeout.c
index 31b01a836..175bb81d4 100644
--- a/src/timeout.c
+++ b/src/timeout.c
@@ -258,8 +258,19 @@ cleanup (int sig)
char signame[MAX (SIG2STR_MAX, INT_BUFSIZE_BOUND (int))];
if (sig == 0 || sig2str (sig, signame) != 0)
snprintf (signame, sizeof signame, "%d", sig);
+
+ /* Let --kill-after interrupt a blocked diagnostic. Without -f,
+ its group-wide SIGKILL terminates us before diagnosing again. */
+ sigset_t alarm_set, old_set;
+ sigemptyset (&alarm_set);
+ sigaddset (&alarm_set, SIGALRM);
+ bool restore_mask = (!foreground
+ && sigprocmask (SIG_UNBLOCK, &alarm_set,
+ &old_set) == 0);
error (0, 0, _("sending signal %s to command %s"),
signame, quote (command));
+ if (restore_mask)
+ sigprocmask (SIG_SETMASK, &old_set, NULL);
}
}
else if (monitored_pid == -1)
diff --git a/tests/timeout/timeout.sh b/tests/timeout/timeout.sh
index 577c68b29..6f4467e31 100755
--- a/tests/timeout/timeout.sh
+++ b/tests/timeout/timeout.sh
@@ -115,4 +115,12 @@ test -e kill.missed && fail=1
echo 137 > kill.exp || framework_failure_
compare kill.exp kill.status || fail=1
+# --kill-after must interrupt a blocked diagnostic for the initial signal.
+file=kill-after.status
+{ timeout -v -k .1 .1 sh -c 'trap "" TERM; exec yes >&2' 2>&1
+ echo $? >kill-after.status; } |
+ { retry_delay_ file_seen .1 6 || touch kill-after.missed; }
+test -e kill-after.missed && fail=1
+compare kill.exp kill-after.status || fail=1
+
Exit $fail
--
2.55.0