summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTobias Stoeckmann <tobias@stoeckmann.org>2017-02-05 13:23:22 +0100
committerPádraig Brady <P@draigBrady.com>2017-02-09 22:15:43 -0800
commit2f69dba5df8caaf9eda658c1808b1379e9949f22 (patch)
tree06b6591378063be7c68882eb1cac0698eb25b20b
parent99deaff7e804298cecc326142bbe7263e2576224 (diff)
downloadcoreutils-2f69dba5df8caaf9eda658c1808b1379e9949f22.tar.xz
timeout: fix race possibly terminating wrong process
The race is unlikely, as timeout(1) needs to receive a signal in the few operations between waitpid() returning and exit(). Also the system needs to have reallocated the just released pid in this time window. Previously we never disabled the signal handler that sent the termination signal to the "child" pid. However once waitpid() has reaped the child, the system is free to allocate that pid, so we must ensure we don't process any further signals. * build-aux/gen-lists-of-programs.sh: Build timeout(1) optionally... * configure.ac: ...predicated on sigsuspend() being available. * src/timeout.c (block_cleanup): A new function to ensure the cleanup() handler is disabled after waitpid has returned. (main): Use sigsuspend() to wait with cleanup() enabled but disabled once it returns, and thus disabled for the waitpid() call. (monitored_pid): Change to the more accurate pid_t. * NEWS: Mention the fix. Fixes http://bugs.gnu.org/25624
-rw-r--r--NEWS6
-rwxr-xr-xbuild-aux/gen-lists-of-programs.sh2
-rw-r--r--configure.ac2
-rw-r--r--src/timeout.c76
4 files changed, 62 insertions, 24 deletions
diff --git a/NEWS b/NEWS
index 9528f89e0..deaab7bcf 100644
--- a/NEWS
+++ b/NEWS
@@ -16,6 +16,12 @@ GNU coreutils NEWS -*- outline -*-
which could be triggered especially when tail was suspended and resumed.
[bug introduced with inotify support added in coreutils-7.5]
+ timeout no longer has a race that may terminate the wrong process.
+ The race is unlikely, as timeout(1) needs to receive a signal right
+ after the command being monitored finishes. Also the system needs
+ to have reallocated that command's pid in that short time window.
+ [bug introduced when timeout was added in coreutils-7.0]
+
wc --bytes --files0-from now correctly reports byte counts.
Previously it may have returned values that were too large,
depending on the size of the first file processed.
diff --git a/build-aux/gen-lists-of-programs.sh b/build-aux/gen-lists-of-programs.sh
index 2666492e8..cdbcd0a9e 100755
--- a/build-aux/gen-lists-of-programs.sh
+++ b/build-aux/gen-lists-of-programs.sh
@@ -32,6 +32,7 @@ build_if_possible_progs='
pinky
stdbuf
stty
+ timeout
uptime
users
who
@@ -120,7 +121,6 @@ normal_progs='
tail
tee
test
- timeout
touch
tr
true
diff --git a/configure.ac b/configure.ac
index a13295aa1..7c7c8c238 100644
--- a/configure.ac
+++ b/configure.ac
@@ -252,6 +252,8 @@ AC_CHECK_FUNCS([chroot],
gl_ADD_PROG([optional_bin_progs], [chroot]))
AC_CHECK_FUNCS([gethostid],
gl_ADD_PROG([optional_bin_progs], [hostid]))
+AC_CHECK_FUNCS([sigsuspend],
+ gl_ADD_PROG([optional_bin_progs], [timeout]))
gl_WINSIZE_IN_PTEM
diff --git a/src/timeout.c b/src/timeout.c
index c38e3657a..6fe0542b7 100644
--- a/src/timeout.c
+++ b/src/timeout.c
@@ -79,7 +79,7 @@
static int timed_out;
static int term_signal = SIGTERM; /* same default as kill command. */
-static int monitored_pid;
+static pid_t monitored_pid;
static double kill_after;
static bool foreground; /* whether to use another program group. */
static bool preserve_status; /* whether to use a timeout status or not. */
@@ -102,16 +102,6 @@ static struct option const long_options[] =
{NULL, 0, NULL, 0}
};
-static void
-unblock_signal (int sig)
-{
- sigset_t unblock_set;
- sigemptyset (&unblock_set);
- sigaddset (&unblock_set, sig);
- if (sigprocmask (SIG_UNBLOCK, &unblock_set, NULL) != 0)
- error (0, errno, _("warning: sigprocmask"));
-}
-
/* Start the timeout after which we'll receive a SIGALRM.
Round DURATION up to the next representable value.
Treat out-of-range values as if they were maximal,
@@ -121,10 +111,6 @@ static void
settimeout (double duration, bool warn)
{
- /* We configure timers below so that SIGALRM is sent on expiry.
- Therefore ensure we don't inherit a mask blocking SIGALRM. */
- unblock_signal (SIGALRM);
-
/* timer_settime() provides potentially nanosecond resolution.
setitimer() is more portable (to Darwin for example),
but only provides microsecond resolution and thus is
@@ -165,11 +151,11 @@ settimeout (double duration, bool warn)
/* send SIG avoiding the current process. */
static int
-send_sig (int where, int sig)
+send_sig (pid_t where, int sig)
{
/* If sending to the group, then ignore the signal,
so we don't go into a signal loop. Note that this will ignore any of the
- signals registered in install_signal_handlers(), that are sent after we
+ signals registered in install_cleanup(), that are sent after we
propagate the first one, which hopefully won't be an issue. Note this
process can be implicitly multithreaded due to some timer_settime()
implementations, therefore a signal sent to the group, can be sent
@@ -179,6 +165,14 @@ send_sig (int where, int sig)
return kill (where, sig);
}
+/* Signal handler which is required for sigsuspend() to be interrupted
+ whenever SIGCHLD is received. */
+static void
+chld (int sig)
+{
+}
+
+
static void
cleanup (int sig)
{
@@ -330,7 +324,7 @@ parse_duration (const char* str)
}
static void
-install_signal_handlers (int sigterm)
+install_cleanup (int sigterm)
{
struct sigaction sa;
sigemptyset (&sa.sa_mask); /* Allow concurrent calls to handler */
@@ -346,6 +340,33 @@ install_signal_handlers (int sigterm)
sigaction (sigterm, &sa, NULL); /* user specified termination signal. */
}
+/* Blocks all signals which were registered with cleanup
+ as signal handler. Return original mask in OLD_SET. */
+static void
+block_cleanup (int sigterm, sigset_t *old_set)
+{
+ sigset_t block_set;
+ sigemptyset (&block_set);
+ sigaddset (&block_set, SIGALRM);
+ sigaddset (&block_set, SIGINT);
+ sigaddset (&block_set, SIGQUIT);
+ sigaddset (&block_set, SIGHUP);
+ sigaddset (&block_set, SIGTERM);
+ sigaddset (&block_set, sigterm);
+ if (sigprocmask (SIG_BLOCK, &block_set, old_set) != 0)
+ error (0, errno, _("warning: sigprocmask"));
+}
+
+static void
+unblock_signal (int sig)
+{
+ sigset_t unblock_set;
+ sigemptyset (&unblock_set);
+ sigaddset (&unblock_set, sig);
+ if (sigprocmask (SIG_UNBLOCK, &unblock_set, NULL) != 0)
+ error (0, errno, _("warning: sigprocmask"));
+}
+
/* Try to disable core dumps for this process.
Return TRUE if successful, FALSE otherwise. */
static bool
@@ -433,10 +454,10 @@ main (int argc, char **argv)
/* Setup handlers before fork() so that we
handle any signals caused by child, without races. */
- install_signal_handlers (term_signal);
+ install_cleanup (term_signal);
signal (SIGTTIN, SIG_IGN); /* Don't stop if background child needs tty. */
signal (SIGTTOU, SIG_IGN); /* Don't stop if background child needs tty. */
- signal (SIGCHLD, SIG_DFL); /* Don't inherit CHLD handling from parent. */
+ signal (SIGCHLD, chld); /* Interrupt sigsuspend() when child exits. */
monitored_pid = fork ();
if (monitored_pid == -1)
@@ -462,11 +483,19 @@ main (int argc, char **argv)
pid_t wait_result;
int status;
+ /* We configure timers so that SIGALRM is sent on expiry.
+ Therefore ensure we don't inherit a mask blocking SIGALRM. */
+ unblock_signal (SIGALRM);
+
settimeout (timeout, true);
- while ((wait_result = waitpid (monitored_pid, &status, 0)) < 0
- && errno == EINTR)
- continue;
+ /* Ensure we don't cleanup() after waitpid() reaps the child,
+ to avoid sending signals to a possibly different process. */
+ sigset_t cleanup_set;
+ block_cleanup (term_signal, &cleanup_set);
+
+ while ((wait_result = waitpid (monitored_pid, &status, WNOHANG)) == 0)
+ sigsuspend (&cleanup_set); /* Wait with cleanup signals unblocked. */
if (wait_result < 0)
{
@@ -487,6 +516,7 @@ main (int argc, char **argv)
{
/* exit with the signal flag set. */
signal (sig, SIG_DFL);
+ unblock_signal (sig);
raise (sig);
}
status = sig + 128; /* what sh returns for signaled processes. */