choeger
choeger

Reputation: 3567

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()?

Upvotes: 1

Views: 166

Answers (2)

Jean-Baptiste Yun&#232;s
Jean-Baptiste Yun&#232;s

Reputation: 36391

Rewrite the code with sigset*and non-active wait (pause) as suggested by Jonathan Leffler:

static sigjmp_buf jump_buffer;

void f() {
  pause();
}

void handle_timeout(int sig) {
  siglongjmp(jump_buffer, 1);
}

void test() {
  if (sigsetjmp(jump_buffer,0) == 0) // SAVE or NOT...
    {
      f();
    }
}

int main() {
  printf("1\n");
  signal (SIGALRM, handle_timeout);
  alarm(2);
  test();
  sigset_t m;
  sigprocmask(0,NULL,&m);
  printf("%d\n",m);
  printf("2\n");
  signal (SIGALRM, handle_timeout);
  alarm(2);
  test();
  return 0;
}

Then execution is blocked on the second part because exiting the handler with a jmp doesn't restores the mask, and as signal blocks the currently delivered signal then after the first call to test() signal mask contains SIGALRM which is then blocked, see execution:

$ ./test
1
8192 #SIGALRM
2    <-blocked

Now if change value 0 to 1 (line commented as SAVE or NOT) as documentation about sigsetjmp says:

The sigsetjmp()/siglongjmp() function pairs save and restore the signal mask if the argument savemask is non-zero; otherwise, only the register set and the stack are saved.

the signal mask after the first call to test is restored, see execution:

$ ./test
1
0
2
$

Upvotes: 2

Jonathan Leffler
Jonathan Leffler

Reputation: 753615

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)
$ 

Upvotes: 2

Related Questions