longjmp and RAII
Asked Answered
S

2

11

So I have a library (not written by me) which unfortunately uses abort() to deal with certain errors. At the application level, these errors are recoverable so I would like to handle them instead of the user seeing a crash. So I end up writing code like this:

static jmp_buf abort_buffer;
static void abort_handler(int) {
    longjmp(abort_buffer, 1); // perhaps siglongjmp if available..
}

int function(int x, int y) {

    struct sigaction new_sa;
    struct sigaction old_sa;

    sigemptyset(&new_sa.sa_mask);
    new_sa.sa_handler = abort_handler;
    sigaction(SIGABRT, &new_sa, &old_sa);

    if(setjmp(abort_buffer)) {
        sigaction(SIGABRT, &old_sa, 0);
        return -1
    }

    // attempt to do some work here
    int result = f(x, y); // may call abort!

    sigaction(SIGABRT, &old_sa, 0);
    return result;
}

Not very elegant code. Since this pattern ends up having to be repeated in a few spots of the code, I would like to simplify it a little and possibly wrap it in a reusable object. My first attempt involves using RAII to handle the setup/teardown of the signal handler (needs to be done because each function needs different error handling). So I came up with this:

template <int N>
struct signal_guard {
    signal_guard(void (*f)(int)) {
        sigemptyset(&new_sa.sa_mask);
        new_sa.sa_handler = f;
        sigaction(N, &new_sa, &old_sa);
    }

    ~signal_guard() {
        sigaction(N, &old_sa, 0);
    }
private:
    struct sigaction new_sa;
    struct sigaction old_sa;
};


static jmp_buf abort_buffer;
static void abort_handler(int) {
    longjmp(abort_buffer, 1);
}

int function(int x, int y) {
    signal_guard<SIGABRT> sig_guard(abort_handler);

    if(setjmp(abort_buffer)) {
        return -1;
    }

    return f(x, y);
}

Certainly the body of function is much simpler and more clear this way, but this morning a thought occurred to me. Is this guaranteed to work? Here's my thoughts:

  1. No variables are volatile or change between calls to setjmp/longjmp.
  2. I am longjmping to a location in the same stack frame as the setjmp and returning normally, so I am allowing the code to execute the cleanup code that the compiler emitted at the exit points of the function.
  3. It appears to work as expected.

But I still get the feeling that this is likely undefined behavior. What do you guys think?

Suggestibility answered 22/3, 2011 at 15:45 Comment(5)
Why are you not using a C++ exception? It trivially solves your problem.Botulism
@Hans - h21007.www2.hp.com/portal/site/dspp/…Kristikristian
@Hans: c++ exceptions don't catch abort calls.Suggestibility
throw them in your abort_handler(), this isn't different.Botulism
@Hans throwing exceptions in signal handlers is not guaranteed to work in all platforms and so is essentially not portable.Gimpel
A
9

I assume that f is in a third party library/app, because otherwise you could just fix it to not call abort. Given that, and that RAII may or may not reliably produce the right results on all platforms/compilers, you have a few options.

  • Create a tiny shared object that defines abort and LD_PRELOAD it. Then you control what happens on abort, and NOT in a signal handler.
  • Run f within a subprocess. Then you just check the return code and if it failed try again with updated inputs.
  • Instead of using the RAII, just call your original function from multiple call points and let it manually do the setup/teardown explicitly. It still eliminates the copy-paste in that case.
Antipyretic answered 22/3, 2011 at 16:17 Comment(1)
+1, I was thinking about adding interposing or thread/fork as a suggestion in my answerKristikristian
M
3

I actually like your solution, and have coded something similar in test harnesses to check that a target function assert()s as expected.

I can't see any reason for this code to invoke undefined behaviour. The C Standard seems to bless it: handlers resulting from an abort() are exempted from the restriction on calling library functions from a handler. (Caveat: this is 7.14.1.1(5) of C99 - sadly, I don't have a copy of C90, the version referenced by the C++ Standard).

C++03 adds a further restriction: If any automatic objects would be destroyed by a thrown exception transferring control to another (destination) point in the program, then a call to longjmp(jbuf, val) at the throw point that transfers control to the same (destination) point has undefined behavior. I'm supposing that your statement that 'No variables are volatile or change between calls to setjmp/longjmp' includes instantiating any automatic C++ objects. (I guess this is some legacy C library?).

Nor is POSIX async signal safety (or lack thereof) an issue - abort() generates its SIGABRT synchronously with program execution.

The biggest concern would be corrupting the global state of the 3rd party code: it's unlikely that the author will take pains to get the state consistent before an abort(). But, if you're correct that no variables change, then this isn't a problem.

If someone with a better understanding of the standardese can prove me wrong, I'd appreciate the enlightenment.

Mariomariology answered 22/3, 2011 at 21:29 Comment(1)
I am fairly certain that no variables are changed. This is a C library, and it only aborts when it is asked to do an operation which requires it to malloc an obscene amount of space which is associated with the initialization of an opaque type). If it fails, then I can just consider than opaque type uninitialized and try to create a new one with more sane values.Suggestibility

© 2022 - 2024 — McMap. All rights reserved.