* 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


Reply via email to