clinuxsignalssigintsetjmp

How do I ensure the `SIGINT` signal handler is called as many times as `Ctrl+C` is pressed (with `longjmp`)?


Setup

In the code below, which simply prints some text until it times out, I added a handler (onintr()) for SIGINT. The handler onintr() does the following:

  1. Resets itself as the default handler.
  2. Prints out some text.
  3. Calls longjmp().

Issue

It seems only the first Ctrl+C is interpreted correctly.

After pressing Ctrl+C the first time the print statement in onintr() appears on the screen and execution returns to where setjmp() was called. However, subsequent Ctrl+C calls get ignored.

Moreover, resetting onintr() as the handler doesn't seem to make a difference. In other words, if I let SIG_DFL become the default handler after onintr() was called once, subsequent Ctrl+C-s get ignored just as when onintr() was the handler; and I can't terminate the program.

The code signal.c:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <setjmp.h>

jmp_buf sjbuf;
void onintr(int);

void 
onintr(int i) 
{
    signal(SIGINT, onintr);
    printf("\nInterrupt(%d)\n", i);
    longjmp(sjbuf, 0);
}

int 
main(int argc, char* argv[]) 
{
    int sleep_t = 1; 
    int ctr     = 0; 
    int timeout = 10;

    if (signal(SIGINT, SIG_IGN) != SIG_IGN)
        signal(SIGINT, onintr);

    setjmp(sjbuf);
    printf("Starting loop...\n");

    while (ctr < timeout) {
        printf("Going to sleep for %d second(s)\n", sleep_t);
        ctr++;
        sleep(sleep_t);
    }

    printf("\n");
    return 0;
}

Behavior

On Ubuntu 22.04 I get the following:

gomfy:signal$ gcc --version
gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

gomfy:signal$ gcc signal.c
gomfy:signal$
gomfy:signal$ ./a.out 
Starting loop...
Going to sleep for 1 second(s)
Going to sleep for 1 second(s)
^C
Interrupt(2)
Starting loop...
Going to sleep for 1 second(s)
Going to sleep for 1 second(s)
^CGoing to sleep for 1 second(s)
^CGoing to sleep for 1 second(s)
^CGoing to sleep for 1 second(s)
^CGoing to sleep for 1 second(s)
^CGoing to sleep for 1 second(s)
Going to sleep for 1 second(s)

gomfy:signal$ 

As you can see the subsequent Ctrl+C-s (^C) get ignored. Only the first one seems to call the handler.


Solution

  • For starters, from signal-safety(7) we learn:

    An async-signal-safe function is one that can be safely called from within a signal handler. Many functions are not async-signal-safe. In particular, nonreentrant functions are generally unsafe to call from a signal handler.

    This raises two problems:

    If a signal handler interrupts the execution of an unsafe function, and the handler terminates via a call to longjmp(3) or siglongjmp(3) and the program subsequently calls an unsafe function, then the behavior of the program is undefined.

    This means if the delivery of SIGINT happens to interrupt a call to printf then the program can no longer be reliably reasoned about.


    As a possible red herring, the generic Linux manual for sleep(3) makes the claim:

    On Linux, sleep() is implemented via nanosleep(2).

    Portability notes
    On some systems, sleep() may be implemented using alarm(2) and SIGALRM (POSIX.1 permits this); mixing calls to alarm(2) and sleep() is a bad idea.

    and then ambiguously states:

    Using longjmp(3) from a signal handler or modifying the handling of SIGALRM while sleeping will cause undefined results.

    It is not entirely clear if this is only in reference to the previously mentioned alarm-based sleeps, but seems likely. The POSIX manual page for sleep(3) would appear to clarify this by making a similar claim:

    If a signal-catching function interrupts sleep() and calls siglongjmp() or longjmp() to restore an environment saved prior to the sleep() call, the action associated with the SIGALRM signal and the time at which a SIGALRM signal is scheduled to be generated are unspecified.

    and nanosleep(2) states:

    POSIX.1 explicitly specifies that [nanosleep] does not interact with signals

    Suffice to say, it does not appear that sleep contributes to this problem, at least on Linux.


    The Linux manual on signal(2) highlights its issues with portability, and encourages the use of sigaction(2).

    An interesting note is:

    If the disposition is set to a function, then first either the disposition is reset to SIG_DFL, or the signal is blocked (see Portability below), and then handler is called with argument signum. If invocation of the handler caused the signal to be blocked, then the signal is unblocked upon return from the handler.

    The portability section later details the differences between System V (reset) and BSD (block) semantics, and notes that glibc 2+ uses BSD semantics by default (by wrapping around sigaction(2)).

    So if we longjmp out of a handler, is the signal ever unblocked?

    Linux's overview of signals, signal(7), ultimately clarifies things in a section labeled Execution of signal handlers, detailing a five step process.

    The last part of step one involves:

    Any signals specified in act->sa_mask when registering the handler with sigprocmask(2) are added to the thread's signal mask. The signal being delivered is also added to the signal mask, unless SA_NODEFER was specified when registering the handler. These signals are thus blocked while the handler executes.

    While step four and five are:

    1. When the signal handler returns, control passes to the signal trampoline code.
    1. The signal trampoline calls sigreturn(2), a system call that uses the information in the stack frame created in step 1 to restore the thread to its state before the signal handler was called. The thread's signal mask and alternate signal stack settings are restored as part of this procedure. Upon completion of the call to sigreturn(2), the kernel transfers control back to user space, and the thread recommences execution at the point where it was interrupted by the signal handler.

    Again, what happens if we longjmp out of a handler? Here is the most important piece of information:

    Note that if the signal handler does not return (e.g., control is transferred out of the handler using siglongjmp(3), or the handler executes a new program with execve(2)), then the final step is not performed. In particular, in such scenarios it is the programmer's responsibility to restore the state of the signal mask (using sigprocmask(2)), if it is desired to unblock the signals that were blocked on entry to the signal handler. (Note that siglongjmp(3) may or may not restore the signal mask, depending on the savesigs value that was specified in the corresponding call to sigsetjmp(3).)

    So the answer is that, by jumping out of the signal handler, the signal mask retains SIGINT, and blocks delivery of subsequent signals. The manual mentions the use of sigprocmask(2) or sigsetjmp(3) and siglongjmp(3) to solve this problem.

    Here is a simple example of using the latter. sigsetjmp needs a non-zero value as its second argument, which tells the pair of functions to save and restore the signal mask. siglongjmp simply replaces longjmp, and sigjmp_buf replaces jmpbuf.

    signal is moved after sigsetjmp to avoid Undefined Behaviour in the event SIGINT is delivered before sigsetjmp executes, which would cause siglongjmp to operate on a garbage sigjmp_buf.

    Additionally, ctr must be declared as volatile, as otherwise its value is unspecified. From setjmp(3) (applies to sigsetjmp as well):

    [...] the values of automatic variables are unspecified after a call to longjmp() if they meet all the following criteria:

    • they are local to the function that made the corresponding setjmp() call;
    • their values are changed between the calls to setjmp() and longjmp(); and
    • they are not declared as volatile.
    #include <setjmp.h>
    #include <signal.h>
    #include <string.h>
    #include <unistd.h>
    
    sigjmp_buf sjbuf;
    
    void dump(const char *s)
    {
        write(STDOUT_FILENO, s, strlen(s));
    }
    
    void onintr(int i)
    {
        dump("SIGINT caught. Jumping away...\n");
        siglongjmp(sjbuf, 42);
    }
    
    int main(void)
    {
        volatile int ctr = 0;
        int timeout = 10;
    
        if (0 != sigsetjmp(sjbuf, 1))
            dump("Stuck the landing!\n");
        else
            signal(SIGINT, onintr);
    
        dump("Starting loop...\n");
    
        while (ctr < timeout) {
            dump("Going to sleep...\n");
            ctr++;
            sleep(1);
        }
    
        dump("\n");
    }
    
    Starting loop...
    Going to sleep...
    ^CSIGINT caught. Jumping away...
    Stuck the landing!
    Starting loop...
    Going to sleep...
    ^CSIGINT caught. Jumping away...
    Stuck the landing!
    Starting loop...
    Going to sleep...
    Going to sleep...
    Going to sleep...
    ^CSIGINT caught. Jumping away...
    Stuck the landing!
    Starting loop...
    Going to sleep...
    Going to sleep...
    Going to sleep...
    ^CSIGINT caught. Jumping away...
    Stuck the landing!
    Starting loop...
    Going to sleep...
    Going to sleep...
    

    If this still does not work, you may need to define a macro: _BSD_SOURCE on glibc 2.19 and earlier or _DEFAULT_SOURCE in glibc 2.19 and later. See: feature_test_macros(7).