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; }