This behavior was depended on in our trap_sigpipe_or_skip_ helper,
and now that we're handling all terminating signals, we should
consistently honor their ignored signal dispositions.
* NEWS: Mention the change in behavior, especially in regard
to shell background jobs.
* src/timeout.c (sig_needs_handling): A new helper that return TRUE,
for --signal, SIG_ALRM, or non ignored signals.
(cleanup_install): Filter handled signals with the helper.
(block_cleanup_and_chld): Likewise.
* tests/timeout/timeout-group.sh: Adjust to use the now required
`env --default-signal=...` wrapper to reset (auto) ignored signals.
Also change the termination signal from SIGINT to SIGUSR1
to generalize the test signals not specially handled by the shell,
and newly handled by timeout(1).
* tests/timeout/timeout.sh: Add a test case for SIGPIPE
to ensure the ignored signal disposition is honored.
---
NEWS | 6 +++++
src/timeout.c | 29 +++++++++++++++++++----
tests/timeout/timeout-group.sh | 43 ++++++++++++++++++++++++----------
tests/timeout/timeout.sh | 3 +++
4 files changed, 64 insertions(+), 17 deletions(-)
diff --git a/NEWS b/NEWS
index 834c490d1..1df4a8045 100644
--- a/NEWS
+++ b/NEWS
@@ -26,6 +26,12 @@ GNU coreutils NEWS -*-
outline -*-
'tail' now accepts the --debug option, which is currently used to
detail the --follow implementation being used.
+** Changes in behavior
+
+ 'timeout' now honors ignored signals and will not propagate them. For e.g.
+ timeout(1) in a shell backgrounded job, will not terminate upon receiving
+ SIGINT or SIGQUIT, as these are ignored by default in shell background jobs.
+
* Noteworthy changes in release 9.9 (2025-11-10) [stable]
diff --git a/src/timeout.c b/src/timeout.c
index 541017cd7..2e770e9ff 100644
--- a/src/timeout.c
+++ b/src/timeout.c
@@ -444,6 +444,23 @@ install_sigchld (void)
unblock_signal (SIGCHLD);
}
+/* Filter out signals that were ignored. */
+
+static bool
+sig_needs_handling (int sig, int sigterm)
+{
+ if (sig == SIGALRM || sig == sigterm)
+ return true; /* We can't ignore these. */
+
+ /* Note background jobs in shells have SIGINT and SIGQUIT
+ set to SIG_IGN by default. I.e., those signals will
+ not be propagated through background timeout jobs. */
+ struct sigaction old_sa;
+ sigaction (sig, nullptr, &old_sa);
+ bool ret = old_sa.sa_handler != SIG_IGN;
+ return ret;
+}
+
static void
install_cleanup (int sigterm)
{
@@ -454,11 +471,13 @@ install_cleanup (int sigterm)
more likely to work cleanly. */
for (int i = 0; i < countof (term_sig); i++)
- sigaction (term_sig[i], &sa, nullptr);
+ if (sig_needs_handling (term_sig[i], sigterm))
+ sigaction (term_sig[i], &sa, nullptr);
/* Real Time signals also terminate by default. */
for (int s = SIGRTMIN; s <= SIGRTMAX; s++)
- sigaction (s, &sa, nullptr);
+ if (sig_needs_handling (s, sigterm))
+ sigaction (s, &sa, nullptr);
sigaction (sigterm, &sa, nullptr); /* user specified termination signal. */
}
@@ -475,10 +494,12 @@ block_cleanup_and_chld (int sigterm, sigset_t *old_set)
sigemptyset (&block_set);
for (int i = 0; i < countof (term_sig); i++)
- sigaddset (&block_set, term_sig[i]);
+ if (sig_needs_handling (term_sig[i], sigterm))
+ sigaddset (&block_set, term_sig[i]);
for (int s = SIGRTMIN; s <= SIGRTMAX; s++)
- sigaddset (&block_set, s);
+ if (sig_needs_handling (s, sigterm))
+ sigaddset (&block_set, s);
sigaddset (&block_set, sigterm);
diff --git a/tests/timeout/timeout-group.sh b/tests/timeout/timeout-group.sh
index f73e6fb7c..81dadcf9d 100755
--- a/tests/timeout/timeout-group.sh
+++ b/tests/timeout/timeout-group.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_
require_kill_group_
@@ -26,18 +26,19 @@ require_kill_group_
# group.sh - separate group
# timeout.cmd - same group as group.sh
#
-# We then send a SIGINT to the "separate group"
-# to simulate what happens when a Ctrl-C
+# We then send a SIGUSR1 to the "separate group"
+# to simulate what happens when a terminating signal
# is sent to the foreground group.
setsid true || skip_ "setsid required to control groups"
printf '%s\n' '#!'"$SHELL" > timeout.cmd || framework_failure_
cat >> timeout.cmd <<\EOF
-trap 'touch int.received; exit' INT
+trap 'touch sig.received; exit' USR1
+trap
touch timeout.running
count=$1
-until test -e int.received || test $count = 0; do
+until test -e sig.received || test $count = 0; do
sleep 1
count=$(expr $count - 1)
done
@@ -46,9 +47,25 @@ chmod a+x timeout.cmd
cat > group.sh <<EOF
#!$SHELL
-trap '' INT
-timeout --foreground 25 ./timeout.cmd 20&
+
+# trap '' ensures this script ignores the signal,
+# so that the 'wait' below is not interrupted.
+# Note this then requires env --default... to reset
+# the signal disposition so that 'timeout' handles it.
+# Alternatively one could use trap ':' USR1
+# and then handle the retry in wait like:
+# while wait; test \$? -gt 128; do :; done
+# Note also INT and QUIT signals are special for backgrounded
+# processes like this in shell as they're auto ignored
+# and can't be reset with trap to any other disposition.
+# Therefore we use the ignored signal method so any
+# termination signal can be used.
+trap '' USR1
+
+env --default-signal=USR1 \
+timeout -v --foreground 25 ./timeout.cmd 20&
wait
+echo group.sh wait returned \$ret
EOF
chmod a+x group.sh
@@ -68,11 +85,11 @@ setsid ./group.sh & pid=$!
# Wait 6.3s for timeout.cmd to start
retry_delay_ check_timeout_cmd_running .1 6 || fail=1
# Simulate a Ctrl-C to the group to test timely exit
-kill -INT -- -$pid
+kill -USR1 -- -$pid
wait
-test -e int.received || fail=1
+test -e sig.received || fail=1
-rm -f int.received timeout.running
+rm -f sig.received timeout.running
# Ensure cascaded timeouts work
@@ -84,8 +101,8 @@ start=$(date +%s)
# Note the first timeout must send a signal that
# the second is handling for it to be propagated to the command.
-# SIGINT, SIGTERM, SIGALRM etc. are implicit.
-timeout -sALRM 30 timeout -sINT 25 ./timeout.cmd 20 & pid=$!
+# termination signals are implicitly handled unless ignored.
+timeout -sALRM 30 timeout -sUSR1 25 ./timeout.cmd 20 & pid=$!
# Wait 6.3s for timeout.cmd to start
retry_delay_ check_timeout_cmd_running .1 6 || fail=1
kill -ALRM $pid # trigger the alarm of the first timeout command
@@ -93,7 +110,7 @@ wait $pid
ret=$?
test $ret -eq 124 ||
skip_ "timeout returned $ret. SIGALRM not handled?"
-test -e int.received || fail=1
+test -e sig.received || fail=1
end=$(date +%s)
diff --git a/tests/timeout/timeout.sh b/tests/timeout/timeout.sh
index d799cbfd9..94f53ba78 100755
--- a/tests/timeout/timeout.sh
+++ b/tests/timeout/timeout.sh
@@ -78,5 +78,8 @@ done
echo 125 > timeout.exp || framework_failure_
{ timeout -v .1 sleep 10 2>&1; echo $? >timeout.status; } | :
compare timeout.exp timeout.status || fail=1
+# Ensure we don't catch/propagate ignored signals
+(trap '' PIPE && timeout 10 yes |:) 2>&1 |
+ grep 'Broken pipe' >/dev/null || fail=1
Exit $fail
--
2.51.1