Cursed C: Catching access violations

SEGFAULT is something of a mystical error, often-cursed for subtle memory bugs in seemingly-good code. To better understand it, let’s take a look at what happens when it’s raised.

When a program attempts to access memory, the processor checks to make sure the virtual (aka software-side) address is mapped to hardware memory, and it is allowed to access the memory in that manner. If not, the processor looks at the interrupt table to jump to the appropriate handler defined by the kernel. Black box magic1 happens as the kernel translates this. Eventually, the kernel checks if the program has registered a signal handler for SIGSEGV, and either calls it or terminates the program if it isn’t present.

From the C side we have signal(), which registers a signal handler and returns the previously-registered signal handler. This is useful if we want to do any special processing before we exit (such as with atexit), or implement special handling of non-fatal signals like responding to a SIGPOLL with a status report. However, we could theoretically also escape a fatal signal if we could return back to the right piece of user code.

We could use return-oriented programming here, but we have setjmp, which is much more reliable and architecture-agnostic.

jmp_buf escapeBuf;
extern "C" void __cdecl SegfaultEscape(int sig)
{
    longjmp(escapeBuf, true);
}

int main()
{
    //Snapshot signal handler and set escaper
    auto prevSignalHandler = signal(SIGSEGV, SegfaultEscape);

    if (!setjmp(escapeBuf))
    {
        //Cause segfault
        char* invalid = reinterpret_cast<char*>(0xDEADBEEF);
        *invalid = 0;
    }
    else
    {
        printf("Caught segfault\n");
    }

    //Restore signal handler
    signal(SIGSEGV, prevSignalHandler);
}

Demo on replit

Drawbacks

This technique is far from ideal, largely because we’re using setjmp. We may introduce instability by skipping cleanup code2 like C++ destructors, creating subtle memory leaks or inducing invalid state leading to unrecoverable crashes. Although it’s unlikely, this could even affect the kernel. Additionally, the C++ std::signal documentation specifies that almost all standard library functions are undefined behavior, which it’s highly likely user code will call.

Footnotes

  1. On x86 systems, this will be a page fault with Present=1 and User=0. The kernel can also see what type of behavior was blocked: Write=1 (can’t write), Write=0 (can’t read), or Instruction Fetch=0 (can’t execute) if R^X is set. ↩︎
  2. MSVC offers the ability to call destructors as longjmp unwinds the stack by passing /EH options, but it also lets us directly catch hardware faults with SEH which makes our custom signal handler unnecessary. ↩︎