Execution continuing after SIGINT received

classic Classic list List threaded Threaded
8 messages Options
Reply | Threaded
Open this post in threaded view
|  
Report Content as Inappropriate

Execution continuing after SIGINT received

Kevin Brodsky
Hi,

I have run into a rather weird behaviour related to SIGINT, which
doesn't seem to be intended, as it's not consistent with other shells
(and is so unexpected that it took me a while to figure out what was
going wrong in my script!).

When Bash receives SIGINT while executing a command, it normally waits
for the command to complete, and then aborts execution. However, it
looks like somehow, this is not the case if the command handles SIGINT,
and execution continues after the command completes. For instance:

$ bash -c '(trap "echo INT; exit 1" INT; sleep 60s); echo after'
^CINT
after
$

Whereas:

$ bash -c '(sleep 60s); echo after'
^C
$

The command doesn't need to be a subshell; for instance, since Python
handles SIGINT by default, execution continues as well:

$ bash -c 'python -c "import time; time.sleep(60)"; echo after'
^CTraceback (most recent call last):
  File "<string>", line 1, in <module>
KeyboardInterrupt
after
$

dash, mksh, zsh don't exhibit this behaviour: in all cases, execution is
aborted. Bash seems to have always behaved like that, at least since 4.0.

Kevin

Reply | Threaded
Open this post in threaded view
|  
Report Content as Inappropriate

Re: Execution continuing after SIGINT received

Bob Proulx
Kevin Brodsky wrote:
> $ bash -c '(trap "echo INT; exit 1" INT; sleep 60s); echo after'
> ^CINT
> after
> $

This is a good example of a bad example case.  You shouldn't "exit 1"
or you will replace the information that the process is exiting due to
a signal with an error code.  The trap handler should kill itself
instead.  Use this test case instead.

> $ bash -c '(trap "echo INT; trap - INT; kill -s INT $$" INT; sleep 60); echo after'

Of course that doesn't change the end result here.  But at least the
program exit WIFSIGNALED information is now correct.

Signal handlers should always raise the signal on themselves after
handling whatever they need to handle first.

In any case I can't recreate your problem when using real processes in
separate shells not all on one line.

Bob

Reply | Threaded
Open this post in threaded view
|  
Report Content as Inappropriate

Re: Execution continuing after SIGINT received

Chet Ramey
In reply to this post by Kevin Brodsky
On 8/4/17 7:52 PM, Kevin Brodsky wrote:

> When Bash receives SIGINT while executing a command, it normally waits
> for the command to complete, and then aborts execution. However, it
> looks like somehow, this is not the case if the command handles SIGINT,
> and execution continues after the command completes. For instance:

The question of what happens when bash receives SIGINT while waiting for a
foreground job to complete has come up many times in the past.

See this for a good discussion of the issue:

https://www.cons.org/cracauer/sigint.html

The basic idea is that the user intends a keyboard-generated SIGINT to go
to the foreground process; that process gets to decide how to handle it;
and bash reacts accordingly.  If the process dies to due SIGINT, bash acts
as if it received the SIGINT; if it does not, bash assumes the process
handled it and effectively ignores it.

Consider a process (emacs is the usual example) that uses SIGINT for its
own purposes as a normal part of operation. If you run that program in a
script, you don't want the shell aborting the script unexpectedly as a
result.

Chet
--
``The lyf so short, the craft so long to lerne.'' - Chaucer
                 ``Ars longa, vita brevis'' - Hippocrates
Chet Ramey, UTech, CWRU    [hidden email]    http://cnswww.cns.cwru.edu/~chet/

Reply | Threaded
Open this post in threaded view
|  
Report Content as Inappropriate

Re: Execution continuing after SIGINT received

Kevin Brodsky
On 05/08/2017 15:53, Chet Ramey wrote:

> On 8/4/17 7:52 PM, Kevin Brodsky wrote:
>
>> When Bash receives SIGINT while executing a command, it normally waits
>> for the command to complete, and then aborts execution. However, it
>> looks like somehow, this is not the case if the command handles SIGINT,
>> and execution continues after the command completes. For instance:
> The question of what happens when bash receives SIGINT while waiting for a
> foreground job to complete has come up many times in the past.
>
> See this for a good discussion of the issue:
>
> https://www.cons.org/cracauer/sigint.html
>
> The basic idea is that the user intends a keyboard-generated SIGINT to go
> to the foreground process; that process gets to decide how to handle it;
> and bash reacts accordingly.  If the process dies to due SIGINT, bash acts
> as if it received the SIGINT; if it does not, bash assumes the process
> handled it and effectively ignores it.
>
> Consider a process (emacs is the usual example) that uses SIGINT for its
> own purposes as a normal part of operation. If you run that program in a
> script, you don't want the shell aborting the script unexpectedly as a
> result.
>
> Chet

Thank you for your answer Chet. The article you linked is extremely
informative, I wasn't aware of the various possible strategies shells
can use to handle SIGINT!

So in summary, there is no bug on the Bash side, it simply implements
the WCE strategy, which does what the user wants... as long as the
invoked commands are well-behaved! And I just got very unlucky, as I did
stumble upon a misbehaved program, and all the other shells I tried
don't implement WCE... Some more experimentations showed that all three
(dash, mksh and zsh) implement WUE (I would have expected zsh to follow
Bash?).

Knowing this, it's now clear that the bug is on the Python side, as it
uses exit(1) when receiving SIGINT instead of doing the signal()/kill()
dance mentioned in the article. Other interpreters like Perl or Ruby
behave correctly, which confirms that there's a problem with Python. To
be fair, killing oneself when receiving SIGINT is quite
counter-intuitive, POSIX is not helping here.

Thanks,
Kevin

Reply | Threaded
Open this post in threaded view
|  
Report Content as Inappropriate

Re: Execution continuing after SIGINT received

Kevin Brodsky
In reply to this post by Bob Proulx
On 05/08/2017 03:22, Bob Proulx wrote:

> Kevin Brodsky wrote:
>> $ bash -c '(trap "echo INT; exit 1" INT; sleep 60s); echo after'
>> ^CINT
>> after
>> $
> This is a good example of a bad example case.  You shouldn't "exit 1"
> or you will replace the information that the process is exiting due to
> a signal with an error code.  The trap handler should kill itself
> instead.  Use this test case instead.
>
>> $ bash -c '(trap "echo INT; trap - INT; kill -s INT $$" INT; sleep 60); echo after'
> Of course that doesn't change the end result here.  But at least the
> program exit WIFSIGNALED information is now correct.
>
> Signal handlers should always raise the signal on themselves after
> handling whatever they need to handle first.
>
> In any case I can't recreate your problem when using real processes in
> separate shells not all on one line.
>
> Bob

You're right Bob, I didn't think about the difference between exit()ing
and being kill()ed, notably in terms of WIFSIGNALED. Chet's reply
explains well the rationale, and it's now clear that the problem is on
Python's side (reproduced in my dummy example!).

Thanks,
Kevin

Reply | Threaded
Open this post in threaded view
|  
Report Content as Inappropriate

Re: Execution continuing after SIGINT received

Bob Proulx
In reply to this post by Kevin Brodsky
Kevin Brodsky wrote:
> To be fair, killing oneself when receiving SIGINT is quite
> counter-intuitive, POSIX is not helping here.

Really?  It seems intuitive to me that at any trap handling level one
should handle what needs to be handled and then raise the signal
higher to the next level of the program.  Software is all about layers
and abstraction.  Sending the signal to one self to raise the signal
again feels good to me.

POSIX even added a raise(3) call to make this easier.  (Although I
still do things the old way.)

  man 3 raise

Bob

Reply | Threaded
Open this post in threaded view
|  
Report Content as Inappropriate

Re: Execution continuing after SIGINT received

Kevin Brodsky
On 05/08/2017 20:35, Bob Proulx wrote:
>
> Really?  It seems intuitive to me that at any trap handling level one
> should handle what needs to be handled and then raise the signal
> higher to the next level of the program.  Software is all about layers
> and abstraction.  Sending the signal to one self to raise the signal
> again feels good to me.

The thing is, "the next level of the program" really is another program,
i.e. the one that invoked it, and you are communicating via the exit
status, so it's certainly not as explicit as re-throwing an exception in
C++, for instance. But sure, once you are aware of this mechanism, it's
not difficult to understand the rationale.

Actually, IMHO, what makes it look very counter-intuitive is the fact
that you need to first reset the signal handler for SIGINT. Of course
this is necessary to avoid invoking the handler recursively, but it
feels very much like a workaround. WIFSIGNALED is true if "the child
process [...] terminated due to the receipt of a signal that was not
caught". That's not really what we want to know here; we want to know if
the child process received a signal that caused it to terminate. Whether
it handled SIGINT to clean up resources is irrelevant; what's relevant
is that it eventually terminated as a consequence of SIGINT. Ideally,
exit()ing from a signal handler should set a bit in the exit status
expressing exactly this.

I'll stop digressing, POSIX is what it is and we won't change it anyway
;-) For now, there's no other way to communicate with the shell, so
that's fair enough.

> POSIX even added a raise(3) call to make this easier.  (Although I
> still do things the old way.)
>
>   man 3 raise

That's a good point, it's arguably more self-explanatory than
kill(getpid(), ...).

Kevin

Reply | Threaded
Open this post in threaded view
|  
Report Content as Inappropriate

Re: Execution continuing after SIGINT received

Bob Proulx
Kevin Brodsky wrote:
> The thing is, "the next level of the program" really is another program,
> i.e. the one that invoked it, and you are communicating via the exit
> status, so it's certainly not as explicit as re-throwing an exception in
> C++, for instance. But sure, once you are aware of this mechanism, it's
> not difficult to understand the rationale.

One shouldn't confuse exception handling with signal handling.  They
are really quite different things. :-)

> Actually, IMHO, what makes it look very counter-intuitive is the fact
> that you need to first reset the signal handler for SIGINT. Of course
> this is necessary to avoid invoking the handler recursively, but it
> feels very much like a workaround.

You probably remember that signal(2) is unsafe because it does exactly
that and resets the signal handler to the default.  This creates a
race condition.  There is a window of time in the signal handler when
a second signal can catch the program with the default state set
before it can reset the signal handler.  That is the main reason why
sigaction(2) should be used instead.

> WIFSIGNALED is true if "the child process [...] terminated due to
> the receipt of a signal that was not caught". That's not really what
> we want to know here; we want to know if the child process received
> a signal that caused it to terminate. Whether it handled SIGINT to
> clean up resources is irrelevant; what's relevant is that it
> eventually terminated as a consequence of SIGINT. Ideally, exit()ing
> from a signal handler should set a bit in the exit status expressing
> exactly this.

Well...  Maybe.  But how are you going to know if the program
"eventually terminated as a consequence of SIGINT" or not?  And of
course then you would need a way to provide a program to do the
opposite when required.

But it has been 40 years already with the current way things are done
and it is a little late to change things now.

> I'll stop digressing, POSIX is what it is and we won't change it anyway
> ;-) For now, there's no other way to communicate with the shell, so
> that's fair enough.

I don't see how POSIX is even involved in this.  Other than stopping
the proliferation of differences on different systems.  Before POSIX
came along every system did this slightly differently.  It was awful.
POSIX.1 simply picked BSD as the behavior to standardize upon.  And
then we could stop putting #ifdef's in our code for every different
system quirk.

What you are seeing here is just the basic behavior of the old Unix
and BSD kernels and now every kernel that has followed since.  Since
that is the common behavior among all systems the standards bodies
will mostly say, freeze on that behavior, do it the same way, don't do
it differently.  Surely that is a good thing, right?

The subtleties of https://www.cons.org/cracauer/sigint.html about how
programs should behave were learned not all at once but over the
course of time.

Bob

Loading...