Subject: Re: bug#25624: [PATCH] timeout: Fix signal race in SIGALRM handling
From: Pádraig Brady <[email-masked]>
To: Tobias Stoeckmann <[email-masked]>
Cc: [email-masked]
Date: Thu, 9 Feb 2017 21:39:29 -0800
List-Id: <debbugs-submit.debbugs.gnu.org>

That looks good.

I've changed the naming a bit,
ensured we also block the term_signal,
made the build of timeout optional on sigsuspend availability,
adjusted the commit message,
and added a NEWS entry.

Marking this as done now.

I'll push in your name later.

thanks!
Pádraig
From 85c006bed19e4c3e0ec0f1be0f9863bf940ab630 Mon Sep 17 00:00:00 2001
From: Tobias Stoeckmann <[email-masked]>
Date: Sun, 5 Feb 2017 13:23:22 +0100
Subject: [PATCH] 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 disables 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
---
 NEWS                               |  6 +++
 build-aux/gen-lists-of-programs.sh |  2 +-
 configure.ac                       |  2 +
 src/timeout.c                      | 75 ++++++++++++++++++++++++++------------
 4 files changed, 61 insertions(+), 24 deletions(-)

diff --git a/NEWS b/NEWS
index 9528f89..deaab7b 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 2666492..cdbcd0a 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 a13295a..7c7c8c2 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 c38e365..d5a90fd 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)
         {
-- 
2.5.5