Configuration Information [Automatically generated, do not change]:
Machine: x86_64
OS: linux-gnu
Compiler: gcc
Compilation CFLAGS:  -DPROGRAM='bash' -DCONF_HOSTTYPE='x86_64'
-DCONF_OSTYPE='linux-gnu' -DCONF_MACHTYPE='x86_64-unknown-linux-gnu'
-DCONF_VENDOR='unknown' -DLOCALEDIR='/usr/share/locale' -DPACKAGE='bash'
-DSHELL -DHAVE_CONFIG_H   -I.  -I. -I./include -I./lib
-D_FORTIFY_SOURCE=2 -march=x86-64 -mtune=generic -O2 -pipe
-fstack-protector-strong --param=ssp-buffer-size=4
-DDEFAULT_PATH_VALUE='/usr/local/sbin:/usr/local/bin:/usr/bin'
-DSTANDARD_UTILS_PATH='/usr/bin' -DSYS_BASHRC='/etc/bash.bashrc'
-DSYS_BASH_LOGOUT='/etc/bash.bash_logout'
uname output: Linux PatrickDesktop 3.19.2-1-ARCH #1 SMP PREEMPT Wed Mar
18 16:21:02 CET 2015 x86_64 GNU/Linux
Machine Type: x86_64-unknown-linux-gnu

Bash Version: 4.3
Patch Level: 39
Release Status: release

Description:
There's a bug that happens when waiting for a child process to complete
(via wait_for in jobs.c) that isn't part of a job, like command
substitution subshells. If a SIGINT is caught by the
wait_sigint_handler, the handler sets the wait_sigint_received flag
which is checked in set_job_status_and_cleanup. But
set_job_status_and_cleanup is called from waitchld only if the pid
belongs to a process that is part of a job. The result is that a SIGINT
trap, if it exists, is not called if a SIGINT is received while waiting
for a comsub subshell.

Repeat-By:
Running the following script (requires python) and giving it a SIGINT
(via ^C) when it blocks doesn't produce any output from the script.
Because SIGINT is trapped it should print “trapped”, however. Closing
stdout causes wait_for to be called in command_substitute:subst.c.

subst-sleep.sh:
  trap 'echo trapped >&2; exit 1' INT

  foo="$(python -c 'import time, os; os.close(1); time.sleep(20)')"

Fix:
The attached patch makes a conservative change that moves the code that
calls the SIGINT trap handler from set_job_status_and_cleanup to a new
function handle_sigint_caught_during_wait. This function is then
additionally called from waitchld for dead children that don't belong to
a job.

Related to the code the fix touches: handle_sigint_caught_during_wait
contains some duplicated code. Also, wait_for calls waitchld as long as
the child or job is still running and waitchld and
set_job_status_and_cleanup in turn call handle_sigint_caught_during_wait
if the child or job is dead. This seems to ensure that the SIGINT trap
handler is called at most once for a call to wait_for but isn't wait_for
a better place to check wait_sigint_received after having restored the
sigint handler (restore_sigint_handler()), especially since it already
contains CHECK_WAIT_INTR. Not to mention that a SIGINT could be received
after checking the wait_sigint_received flag and before restoring the
signal handler, as far as I understand it.
--- a/jobs.c
+++ b/jobs.c
@@ -260,6 +260,7 @@ static int print_job __P((JOB *, int, int, int));
 static int process_exit_status __P((WAIT));
 static int process_exit_signal __P((WAIT));
 static int set_job_status_and_cleanup __P((int));
+static void handle_sigint_caught_during_wait __P((PROCESS *, int));
 
 static WAIT job_signal_status __P((int));
 static WAIT raw_job_exit_status __P((int));
@@ -3311,7 +3312,11 @@ itrace("waitchld: waitpid returns %d block = %d", pid, 
block);
        }
         
       if (job == NO_JOB)
-       continue;
+        {
+          if (PEXITED (child))
+            handle_sigint_caught_during_wait (child, NO_JOB);
+          continue;
+        }
 
       call_set_current += set_job_status_and_cleanup (job);
 
@@ -3371,18 +3376,104 @@ itrace("waitchld: waitpid returns %d block = %d", pid, 
block);
   return (children_exited);
 }
 
-/* Set the status of JOB and perform any necessary cleanup if the job is
-   marked as JDEAD.
+/* Handle SIGINTs received while waiting for a foreground JOB, or CHILD if it 
+   isn't part of a job, to finish. */
+static void handle_sigint_caught_during_wait (child, job)
+     PROCESS *child;
+     int job;
+{
+  SigHandler *temp_handler;
+  if (job != NO_JOB)
+    child = jobs[job]->pipe;
+  int tstatus, job_has_job_control, is_foreground_job;
+
+  job_has_job_control = job == NO_JOB ? job_control != 0 : IS_JOBCONTROL (job);
+  is_foreground_job = job == NO_JOB ? interactive_shell == 0 : 
+    IS_FOREGROUND (job);
+
+  if (!(wait_sigint_received && is_foreground_job))
+    return;
+
+  /* If we're running a shell script and we get a SIGINT with a
+     SIGINT trap handler, but the foreground job handles it and
+     does not exit due to SIGINT, run the trap handler but do not
+     otherwise act as if we got the interrupt. */
+  if (interactive_shell == 0 &&
+      child_caught_sigint && signal_is_trapped (SIGINT))
+  {
+    int old_frozen;
+    wait_sigint_received = 0;
+    last_command_exit_value = process_exit_status (child->status);
+
+    old_frozen = jobs_list_frozen;
+    jobs_list_frozen = 1;
+    tstatus = maybe_call_trap_handler (SIGINT);
+    jobs_list_frozen = old_frozen;
+  }
+
+  /* If the foreground job is killed by SIGINT when job control is not
+     active, we need to perform some special handling.
+
+     The check of wait_sigint_received is a way to determine if the
+     SIGINT came from the keyboard (in which case the shell has already
+     seen it, and wait_sigint_received is non-zero, because keyboard
+     signals are sent to process groups) or via kill(2) to the foreground
+     process by another process (or itself).  If the shell did receive the
+     SIGINT, it needs to perform normal SIGINT processing. */
+  else if (child_caught_sigint == 0 && job_has_job_control == 0)
+  {
+    int old_frozen;
+
+    wait_sigint_received = 0;
+
+    /* If SIGINT is trapped, set the exit status so that the trap
+       handler can see it. */
+    if (signal_is_trapped (SIGINT))
+      last_command_exit_value = process_exit_status (child->status);
+
+    /* If the signal is trapped, let the trap handler get it no matter
+       what and simply return if the trap handler returns.
+       maybe_call_trap_handler() may cause dead jobs to be removed from
+       the job table because of a call to execute_command.  We work
+       around this by setting JOBS_LIST_FROZEN. */
+    old_frozen = jobs_list_frozen;
+    jobs_list_frozen = 1;
+    tstatus = maybe_call_trap_handler (SIGINT);
+    jobs_list_frozen = old_frozen;
+    if (tstatus == 0 && old_sigint_handler != INVALID_SIGNAL_HANDLER)
+    {
+      /* wait_sigint_handler () has already seen SIGINT and
+         allowed the wait builtin to jump out.  We need to
+         call the original SIGINT handler, if necessary.  If
+         the original handler is SIG_DFL, we need to resend
+         the signal to ourselves. */
+
+      temp_handler = old_sigint_handler;
+
+      /* Bogus.  If we've reset the signal handler as the result
+         of a trap caught on SIGINT, then old_sigint_handler
+         will point to trap_handler, which now knows nothing about
+         SIGINT (if we reset the sighandler to the default).
+         In this case, we have to fix things up.  What a crock. */
+      if (temp_handler == trap_handler && signal_is_trapped (SIGINT) == 0)
+        temp_handler = trap_to_sighandler (SIGINT);
+      restore_sigint_handler ();
+      if (temp_handler == SIG_DFL)
+        termsig_handler (SIGINT);      /* XXX */
+      else if (temp_handler != SIG_IGN)
+        (*temp_handler) (SIGINT);
+    }
+  }
+}
 
-   Currently, the cleanup activity is restricted to handling any SIGINT
-   received while waiting for a foreground job to finish. */
+/* Set the status of JOB and perform any necessary cleanup if the job is
+   marked as JDEAD.  */
 static int
 set_job_status_and_cleanup (job)
      int job;
 {
   PROCESS *child;
-  int tstatus, job_state, any_stopped, any_tstped, call_set_current;
-  SigHandler *temp_handler;
+  int job_state, any_stopped, any_tstped, call_set_current;
 
   child = jobs[job]->pipe;
   jobs[job]->flags &= ~J_NOTIFIED;
@@ -3467,81 +3558,7 @@ set_job_status_and_cleanup (job)
    */
 
   if (JOBSTATE (job) == JDEAD)
-    {
-      /* If we're running a shell script and we get a SIGINT with a
-        SIGINT trap handler, but the foreground job handles it and
-        does not exit due to SIGINT, run the trap handler but do not
-        otherwise act as if we got the interrupt. */
-      if (wait_sigint_received && interactive_shell == 0 &&
-         child_caught_sigint && IS_FOREGROUND (job) &&
-         signal_is_trapped (SIGINT))
-       {
-         int old_frozen;
-         wait_sigint_received = 0;
-         last_command_exit_value = process_exit_status (child->status);
-
-         old_frozen = jobs_list_frozen;
-         jobs_list_frozen = 1;
-         tstatus = maybe_call_trap_handler (SIGINT);
-         jobs_list_frozen = old_frozen;
-       }
-
-      /* If the foreground job is killed by SIGINT when job control is not
-        active, we need to perform some special handling.
-
-        The check of wait_sigint_received is a way to determine if the
-        SIGINT came from the keyboard (in which case the shell has already
-        seen it, and wait_sigint_received is non-zero, because keyboard
-        signals are sent to process groups) or via kill(2) to the foreground
-        process by another process (or itself).  If the shell did receive the
-        SIGINT, it needs to perform normal SIGINT processing. */
-      else if (wait_sigint_received &&
-             child_caught_sigint == 0 &&
-             IS_FOREGROUND (job) && IS_JOBCONTROL (job) == 0)
-       {
-         int old_frozen;
-
-         wait_sigint_received = 0;
-
-         /* If SIGINT is trapped, set the exit status so that the trap
-            handler can see it. */
-         if (signal_is_trapped (SIGINT))
-           last_command_exit_value = process_exit_status (child->status);
-
-         /* If the signal is trapped, let the trap handler get it no matter
-            what and simply return if the trap handler returns.
-           maybe_call_trap_handler() may cause dead jobs to be removed from
-           the job table because of a call to execute_command.  We work
-           around this by setting JOBS_LIST_FROZEN. */
-         old_frozen = jobs_list_frozen;
-         jobs_list_frozen = 1;
-         tstatus = maybe_call_trap_handler (SIGINT);
-         jobs_list_frozen = old_frozen;
-         if (tstatus == 0 && old_sigint_handler != INVALID_SIGNAL_HANDLER)
-           {
-             /* wait_sigint_handler () has already seen SIGINT and
-                allowed the wait builtin to jump out.  We need to
-                call the original SIGINT handler, if necessary.  If
-                the original handler is SIG_DFL, we need to resend
-                the signal to ourselves. */
-
-             temp_handler = old_sigint_handler;
-
-             /* Bogus.  If we've reset the signal handler as the result
-                of a trap caught on SIGINT, then old_sigint_handler
-                will point to trap_handler, which now knows nothing about
-                SIGINT (if we reset the sighandler to the default).
-                In this case, we have to fix things up.  What a crock. */
-             if (temp_handler == trap_handler && signal_is_trapped (SIGINT) == 
0)
-                 temp_handler = trap_to_sighandler (SIGINT);
-               restore_sigint_handler ();
-             if (temp_handler == SIG_DFL)
-               termsig_handler (SIGINT);       /* XXX */
-             else if (temp_handler != SIG_IGN)
-               (*temp_handler) (SIGINT);
-           }
-       }
-    }
+    handle_sigint_caught_during_wait (NULL, job);
 
   return call_set_current;
 }

Reply via email to