git: 613310263ae1 - main - timeout(1): Kill self with the same signal that terminated the child

From: Baptiste Daroussin <bapt_at_FreeBSD.org>
Date: Wed, 16 Apr 2025 19:46:38 UTC
The branch main has been updated by bapt:

URL: https://cgit.FreeBSD.org/src/commit/?id=613310263ae1f0f65df498dc8e98f460b781946d

commit 613310263ae1f0f65df498dc8e98f460b781946d
Author:     Aaron LI <aly@aaronly.me>
AuthorDate: 2025-04-03 02:51:06 +0000
Commit:     Baptiste Daroussin <bapt@FreeBSD.org>
CommitDate: 2025-04-16 19:45:38 +0000

    timeout(1): Kill self with the same signal that terminated the child
    
    A shell may not set '$?' to '128 + signal_number' when the process was
    terminated by a signal.  For example, KornShell 93 sets '$?' to
    '256 + signal_number' in such cases.  In order to avoid any possible
    ambiguity, the POSIX.1-2024 standard requires that timeout mimic the
    wait status of the child process by terminating itself with the same
    signal, while disabling core generation.
    
    Update the man page accordingly.
    
    Obtained-from: DragonFly BSD
    Reference: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/timeout.html
---
 bin/timeout/timeout.1 | 51 ++++++++++++++++++++++++---------------------------
 bin/timeout/timeout.c | 35 +++++++++++++++++++++++++++++++++--
 2 files changed, 57 insertions(+), 29 deletions(-)

diff --git a/bin/timeout/timeout.1 b/bin/timeout/timeout.1
index 44525daaec59..43a484dac76f 100644
--- a/bin/timeout/timeout.1
+++ b/bin/timeout/timeout.1
@@ -146,37 +146,33 @@ hours
 days
 .El
 .Sh EXIT STATUS
-If the timeout was not reached, the exit status of
-.Ar command
-is returned.
-.Pp
-If the timeout was reached and
-.Fl -preserve-status
-is set, the exit status of
-.Ar command
-is returned.
-If
+If the time limit was reached and the
 .Fl -preserve-status
-is not set, an exit status of 124 is returned.
-.Pp
-If an invalid parameter is passed to
-.Fl s
-or
-.Fl k ,
-the exit status returned is 125.
-.Pp
-If
+option is not specified, the exit status is 124.
+Otherwise,
+.Nm
+exits with the same exit status as the
+.Ar command .
+For example,
+.Nm
+will terminate itself with the same signal if the
 .Ar command
-is an otherwise invalid program, the exit status returned is 126.
+is terminated by a signal.
 .Pp
-If
+If an error occurred, the following exit values are returned:
+.Bl -tag -offset indent with indent -compact
+.It 125
+An error other than the two described below occurred.
+For example, an invalid duration or signal was specified.
+.It 126
+The
 .Ar command
-refers to a non-existing program, the exit status returned is 127.
-.Pp
-If
+was found but could not be executed.
+.It 127
+The
 .Ar command
-exits after receiving a signal, the exit status returned is the signal number
-plus 128.
+could not be found.
+.El
 .Sh EXAMPLES
 Run
 .Xr sleep 1
@@ -202,7 +198,8 @@ $ echo $?
 .Pp
 Same as above but preserving status.
 The exit status is 128 + signal number (15 for
-.Dv SIGTERM ) :
+.Dv SIGTERM )
+for most shells:
 .Bd -literal -offset indent
 $ timeout --preserve-status 2 sleep 4
 $ echo $?
diff --git a/bin/timeout/timeout.c b/bin/timeout/timeout.c
index 6e93e9e2911c..e9c4e22fc7d3 100644
--- a/bin/timeout/timeout.c
+++ b/bin/timeout/timeout.c
@@ -28,6 +28,7 @@
 
 #include <sys/cdefs.h>
 #include <sys/procctl.h>
+#include <sys/resource.h>
 #include <sys/time.h>
 #include <sys/wait.h>
 
@@ -239,6 +240,34 @@ set_interval(double iv)
 		err(EXIT_FAILURE, "setitimer()");
 }
 
+/*
+ * In order to avoid any possible ambiguity that a shell may not set '$?' to
+ * '128+signal_number', POSIX.1-2024 requires that timeout mimic the wait
+ * status of the child process by terminating itself with the same signal,
+ * while disabling core generation.
+ */
+static void __dead2
+kill_self(int signo)
+{
+	sigset_t mask;
+	struct rlimit rl;
+
+	/* Reset the signal disposition and make sure it's unblocked. */
+	signal(signo, SIG_DFL);
+	sigfillset(&mask);
+	sigdelset(&mask, signo);
+	sigprocmask(SIG_SETMASK, &mask, NULL);
+
+	/* Disable core generation. */
+	memset(&rl, 0, sizeof(rl));
+	setrlimit(RLIMIT_CORE, &rl);
+
+	logv("killing self with signal %s(%d)", sys_signame[signo], signo);
+	kill(getpid(), signo);
+	err(128 + signo, "signal %s(%d) failed to kill self",
+	    sys_signame[signo], signo);
+}
+
 int
 main(int argc, char **argv)
 {
@@ -430,10 +459,12 @@ main(int argc, char **argv)
 	if (timedout && !preserve) {
 		pstat = EXIT_TIMEOUT;
 	} else {
+		if (WIFSIGNALED(pstat))
+			kill_self(WTERMSIG(pstat));
+			/* NOTREACHED */
+
 		if (WIFEXITED(pstat))
 			pstat = WEXITSTATUS(pstat);
-		else if (WIFSIGNALED(pstat))
-			pstat = 128 + WTERMSIG(pstat);
 	}
 
 	return (pstat);