Search code examples
csignalsposix

Raising subsequent alarm()


While trying to reproduce a completely unrelated presumed false positive stack-buffer-overflow warning from asan, I noticed something odd. When I ask for two alarm() signals subsequently, the second apparently never fires. Why is that?

Here is a MWE:

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

static jmp_buf jump_buffer;

void f()
{
    while(true) {};
}

void handle_timeout(int)
{
    longjmp(jump_buffer, 1);
}

void test()
{
    if (setjmp(jump_buffer) == 0)
    {
            f();
    }
}

int main()
{
    signal (SIGALRM, handle_timeout);
    alarm(2);
    test();
    signal (SIGALRM, handle_timeout);
    alarm(2);
    test();
    return 0;
}

If you uncomment the second call to test, the program terminates as intended after 2s, but as it is it runs forever.

I am well aware that the "signal is automatically blocked [...] during the time the handler is running" according to gnu.org, but is that time not ended by longjump()?


Solution

  • As I noted in comments, in general it is better to use sigaction() rather than signal() because it gives you more precise control over how the signal handling is done. It is also better to use pause() (at least in non-threaded applications) to wait until some signal arrives than it is to make the CPU spin its wheels in a tight infinite loop.

    As Some programmer dude noted in a comment, it is better to use sigsetjmp() and siglongjmp() than to use setjmp() and longjmp().

    However, to my considerable surprise, I am able to reproduce the problem on macOS High Sierra 10.13.2. My instrumented version of the code uses pause() in place of a spin-loop, and uses sigsetjmp() (with a savemask argument of 0) and siglongjmp(), and it recovers from the first alarm and never receives the second. On a Mac, alarm() is documented in section 3 (functions) and not section 2 (system calls). It shouldn't make a difference, but the man page indicates that setitimer() is in use under the covers.

    When I removed the sigsetjmp()/siglongjmp() calls, the second alarm was delivered (it worked) — so it appears that the non-local gotos have an effect. I was using sigsetjmp() with 0 as the last parameter. When I changed that to 1, then the code worked with the sigsetjmp()/siglongjmp() code. So, I think it is some combination of non-local goto and signal masks that give trouble.

    Here's a variant of your code with rather extensive instrumentation. It uses my preferred error reporting functions, which are available on GitHub in my SOQ (Stack Overflow Questions) repository as files stderr.c and stderr.h in the src/libsoq sub-directory. Those make it easy to report the times when messages are reported, etc, which is beneficial.

    #include <assert.h>
    #include <setjmp.h>
    #include <signal.h>
    #include <stdbool.h>
    #include <stdio.h>
    #include <unistd.h>
    #include "stderr.h"
    
    static bool use_jmpbuf = false;
    static int  save_mask  = 1;
    static sigjmp_buf jump_buffer;
    
    static void handle_timeout(int signum)
    {
        assert(signum == SIGALRM);
        if (use_jmpbuf)
            siglongjmp(jump_buffer, 1);
    }
    
    static void handle_sigint(int signum)
    {
        err_error("Got signal %d (SIGINT)\n", signum);
        /*NOTREACHED*/
    }
    
    static void test(void)
    {
        err_remark("Entering %s()\n", __func__);
        if (use_jmpbuf)
        {
            if (sigsetjmp(jump_buffer, save_mask) == 0)
            {
                err_remark("Pausing in %s()\n", __func__);
                pause();
            }
        }
        else
        {
            err_remark("Pausing in %s()\n", __func__);
            pause();
        }
        err_remark("Leaving %s()\n", __func__);
    }
    
    static void set_sigalrm(void)
    {
        void (*handler)(int) = signal(SIGALRM, handle_timeout);
        if (handler == SIG_ERR)
            err_syserr("signal failed: ");
        if (handler == SIG_IGN)
            err_remark("SIGALRM was ignored\n");
        else if (handler == SIG_DFL)
            err_remark("SIGALRM was defaulted\n");
        else
            err_remark("SIGALRM was being handled\n");
    }
    
    static const char optstr[] = "hjm";
    static const char usestr[] = "[-hjm]";
    static const char hlpstr[] =
        "  -h  Print this help information and exit\n"
        "  -j  Use sigsetjmp()\n"
        "  -m  Do not save signal mask when using sigsetjmp\n"
        ;
    
    int main(int argc, char **argv)
    {
        err_setarg0(argv[0]);
        int opt;
        while ((opt = getopt(argc, argv, optstr)) != -1)
        {
            switch (opt)
            {
            case 'h':
                err_help(usestr, hlpstr);
                /*NOTREACHED*/
            case 'j':
                use_jmpbuf = true;
                break;
            case 'm':
                use_jmpbuf = true;
                save_mask = 0;
                break;
            default:
                err_usage(usestr);
                /*NOTREACHED*/
            }
        }
        if (optind != argc)
            err_usage(usestr);
    
        signal(SIGINT, handle_sigint);
        err_setlogopts(ERR_MILLI);
        err_stderr(stdout);
    
        if (use_jmpbuf)
            err_remark("Config: using sigsetjmp() %s saving signal mask\n", save_mask ? "with" : "without");
        else
            err_remark("Config: no use of sigsetjmp\n");
        set_sigalrm();
        unsigned left;
    
        left = alarm(2);
        err_remark("Left over from previous alarm: %u\n", left);
        test();
        err_remark("In %s() once more\n", __func__);
        set_sigalrm();
        left = alarm(2);
        err_remark("Left over from previous alarm: %u\n", left);
        test();
        err_remark("Exiting %s() once more\n", __func__);
        return 0;
    }
    

    Sample runs (program name alrm61):

    $ alrm61 -h
    Usage: alrm61 [-hjm]
      -h  Print this help information and exit
      -j  Use sigsetjmp()
      -m  Do not save signal mask when using sigsetjmp
    $ alrm61
    alrm61: 2018-01-02 21:34:01.893 - Config: no use of sigsetjmp
    alrm61: 2018-01-02 21:34:01.894 - SIGALRM was defaulted
    alrm61: 2018-01-02 21:34:01.894 - Left over from previous alarm: 0
    alrm61: 2018-01-02 21:34:01.894 - Entering test()
    alrm61: 2018-01-02 21:34:01.894 - Pausing in test()
    alrm61: 2018-01-02 21:34:03.898 - Leaving test()
    alrm61: 2018-01-02 21:34:03.898 - In main() once more
    alrm61: 2018-01-02 21:34:03.898 - SIGALRM was being handled
    alrm61: 2018-01-02 21:34:03.898 - Left over from previous alarm: 0
    alrm61: 2018-01-02 21:34:03.898 - Entering test()
    alrm61: 2018-01-02 21:34:03.898 - Pausing in test()
    alrm61: 2018-01-02 21:34:05.902 - Leaving test()
    alrm61: 2018-01-02 21:34:05.902 - Exiting main() once more
    $ alrm61 -j
    alrm61: 2018-01-02 21:34:23.103 - Config: using sigsetjmp() with saving signal mask
    alrm61: 2018-01-02 21:34:23.104 - SIGALRM was defaulted
    alrm61: 2018-01-02 21:34:23.104 - Left over from previous alarm: 0
    alrm61: 2018-01-02 21:34:23.104 - Entering test()
    alrm61: 2018-01-02 21:34:23.104 - Pausing in test()
    alrm61: 2018-01-02 21:34:25.108 - Leaving test()
    alrm61: 2018-01-02 21:34:25.108 - In main() once more
    alrm61: 2018-01-02 21:34:25.108 - SIGALRM was being handled
    alrm61: 2018-01-02 21:34:25.108 - Left over from previous alarm: 0
    alrm61: 2018-01-02 21:34:25.109 - Entering test()
    alrm61: 2018-01-02 21:34:25.109 - Pausing in test()
    alrm61: 2018-01-02 21:34:27.112 - Leaving test()
    alrm61: 2018-01-02 21:34:27.112 - Exiting main() once more
    $ alrm61 -m
    alrm61: 2018-01-02 21:34:37.578 - Config: using sigsetjmp() without saving signal mask
    alrm61: 2018-01-02 21:34:37.578 - SIGALRM was defaulted
    alrm61: 2018-01-02 21:34:37.578 - Left over from previous alarm: 0
    alrm61: 2018-01-02 21:34:37.578 - Entering test()
    alrm61: 2018-01-02 21:34:37.578 - Pausing in test()
    alrm61: 2018-01-02 21:34:39.584 - Leaving test()
    alrm61: 2018-01-02 21:34:39.584 - In main() once more
    alrm61: 2018-01-02 21:34:39.584 - SIGALRM was being handled
    alrm61: 2018-01-02 21:34:39.584 - Left over from previous alarm: 0
    alrm61: 2018-01-02 21:34:39.584 - Entering test()
    alrm61: 2018-01-02 21:34:39.584 - Pausing in test()
    ^Calrm61: 2018-01-02 21:35:00.638 - Got signal 2 (SIGINT)
    $