I'd doc this an an improvement rather than a bug fix really.
Also I can't see any practical cases this addresses.
If a monitored command did PTRACE_TRACEME say to avoid debugging,
then it would get stopped with any signal, so practially it
would need to be a minimal tracer itself, thus timeout would not be involved.
So really this is a robustness improvement for commands doing weird things.
The resume_command() name and return value contract confused me a little,
especially as the PTRACE_DETACH result was ignored.
In the attached I renamed the function to handle_ptrace_stop().
The test has a race I think where timeout could send the SIGTERM
before env has the ignore in place. So I've adjusted to using
SIGURG which is ignored by default.
Also the test could hang in the `grep TracerPid` check
if a signal (say SIGWINCH from a resized terminal) caused
the process to become stopped due to the preloaded PTRACE_TRACEME.
It's better to protect all PTRACE_TRACEME uses with timeout.
Also I think this test might ERROR on FreeBSD
due to ptrace() being available but different.
So I relaxed the ERROR to a SKIP when failing to build the shared lib.
The attached addresses all of the above points,
for squashing in as you see fit.
cheers,
Padraig
From 94e90b8b485ac79ced20756195186e661395dd6a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C3=A1draig=20Brady?= <[email protected]>
Date: Fri, 13 Nov 2026 15:52:28 +0000
Subject: [PATCH] timeout: adjustments to PTRACE_TRACEME handling
To squash in as per email
---
NEWS | 9 ++++-----
src/timeout.c | 22 +++++++++++++---------
tests/timeout/ptrace-traceme.sh | 15 ++++++++-------
3 files changed, 25 insertions(+), 21 deletions(-)
diff --git a/NEWS b/NEWS
index 7b377a480..128861bc0 100644
--- a/NEWS
+++ b/NEWS
@@ -263,11 +263,6 @@ GNU coreutils NEWS -*- outline -*-
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]
- 'timeout' no longer leaves commands running, with or without --kill-after,
- when they turn 'timeout' into their tracer using ptrace with the
- PTRACE_TRACEME option.
- [bug introduced with timeout in coreutils-7.0]
-
'unexpand' no longer overflows a heap buffer on non-GLIBC platforms,
given whitespace characters whose display width is negative. E.g.,
this edge case can occur with \u0085 (next line) characters on musl.
@@ -359,6 +354,10 @@ GNU coreutils NEWS -*- outline -*-
output to the same file. This is consistent with the existing bounded
operation when tailing based from the end of a file.
+ 'timeout' no longer leaves commands running, even with --kill-after,
+ in the unusual case where they turn 'timeout' into their tracer
+ using ptrace with the PTRACE_TRACEME option.
+
'wc -m' counts ASCII up to 14x faster, and UTF-8 up to 7x faster.
Commands that support hardware acceleration like 'cksum' and 'wc',
diff --git a/src/timeout.c b/src/timeout.c
index 40b238c22..e2d1ccdca 100644
--- a/src/timeout.c
+++ b/src/timeout.c
@@ -110,27 +110,28 @@ static struct option const long_options[] =
};
-/* Resumed the monitored command that was stopped with signal SIG.
- Return true if it was resumed.
-
+/* Resume the monitored command that was stopped with signal SIG.
This applies to child processes on Linux who use PTRACE_TRACEME
- to turn us into their tracer. */
+ to turn us into their tracer.
+ Return true if we should continue waiting for the command. */
static bool
-resume_command (MAYBE_UNUSED int sig)
+handle_ptrace_stop (MAYBE_UNUSED int sig)
{
#if HAVE_PTRACE && (defined __linux__ || defined __ANDROID__)
siginfo_t si;
/* If stopped by SIGTRAP, it may be because the monitored command made a
successful call to execve(2). If that is not the case, or if SIG is
- not SIGTRAP, we want to send the signal to the monitored command. */
+ not SIGTRAP, we want to send the signal to the monitored command.
+ XXX: Note we drop the TRAP signal in the very unlikely edge case
+ where a command is sending TRAP to itself and using PTRACE_TRACEME. */
if (sig == SIGTRAP
&& ptrace (PTRACE_GETSIGINFO, monitored_pid, NULL, &si) == 0
&& si.si_code == SI_USER && si.si_pid == monitored_pid)
sig = 0;
/* Restart the monitored command, send signal SIG, and detach from it. */
- ptrace (PTRACE_DETACH, monitored_pid, NULL, sig);
+ (void) ptrace (PTRACE_DETACH, monitored_pid, NULL, sig);
return true;
#else
return false;
@@ -717,11 +718,14 @@ main (int argc, char **argv)
/* Note signals remain blocked in parent here, to ensure
we don't cleanup() after waitpid() reaps the child,
- to avoid sending signals to a possibly different process. */
+ to avoid sending signals to a possibly different process.
+
+ Note we only get stopped status for traced processes,
+ as we're not using WUNTRACED. */
while ((wait_result = waitpid (monitored_pid, &status, WNOHANG)) == 0
|| (0 < wait_result && WIFSTOPPED (status)
- && resume_command (WSTOPSIG (status))))
+ && handle_ptrace_stop (WSTOPSIG (status))))
{
if (wait_result == 0)
sigsuspend (&orig_set); /* Wait with cleanup signals unblocked. */
diff --git a/tests/timeout/ptrace-traceme.sh b/tests/timeout/ptrace-traceme.sh
index fdee101cf..a92fbbfd2 100755
--- a/tests/timeout/ptrace-traceme.sh
+++ b/tests/timeout/ptrace-traceme.sh
@@ -23,7 +23,7 @@ require_gcc_shared_
grep '^#define HAVE_PTRACE 1' $CONFIG_HEADER > /dev/null \
|| skip_ 'ptrace is not supported on this system'
-# Set up a shared library to use PTRACE_TRACE upon loading.
+# Set up a shared library to use PTRACE_TRACEME upon loading.
cat > k.c <<'EOF' || framework_failure_
#include <sys/ptrace.h>
#include <stddef.h>
@@ -34,12 +34,12 @@ traceme (void)
ptrace (PTRACE_TRACEME, 0, NULL, NULL);
}
EOF
-gcc_shared_ k.c k.so || framework_failure_ 'failed to build shared library'
+gcc_shared_ k.c k.so || skip_ 'failed to build ptrace shared lib'
# Make sure we can use it.
-env LD_PRELOAD="$LD_PRELOAD":./k.so grep '^TracerPid:' /proc/self/status \
- | cut -f2 > tracer-pid
-test "$?" -eq 0 && test "$$" = "$(cat tracer-pid)" \
+timeout --kill-after=1 10 env LD_PRELOAD="$LD_PRELOAD":./k.so \
+ grep '^TracerPid:.[1-9][0-9]*$' /proc/self/status \
+ > /dev/null \
|| skip_ 'Unable to use PTRACE_TRACEME'
# Test that the signal terminates the program.
@@ -50,10 +50,11 @@ compare /dev/null out || fail=1
compare /dev/null err || fail=1
# The KILL signal should be sent if the timeout signal is ignored.
+# URG is ignored by default, avoiding a race to install SIG_IGN.
# coreutils-9.12 and earlier would exit with a status of 124, but with
# the command left running.
-returns_ 137 timeout --foreground --kill-after .1 .1 \
- env --ignore-signal=TERM LD_PRELOAD="$LD_PRELOAD":./k.so \
+returns_ 137 timeout --foreground --signal=URG --kill-after .1 .1 \
+ env LD_PRELOAD="$LD_PRELOAD":./k.so \
sleep 10 >out 2>err || fail=1
compare /dev/null out || fail=1
compare /dev/null err || fail=1
--
2.56.0