Search code examples
clinuxsegmentation-faultsigaction

When catching SIGSEGV from within, how to known the kind of invalid access involved?


As you known, it is possible catch any signal but kill and stop/count with an handler.
There’s three kind of invalid address access :

  • The attempt to execute/jump at an invalid address.
  • The attempt to read at an invalid address.
  • The attempt to write at an invalid address.

I’m only interested in rejecting invalid read accesses. So the idea is to catch all segmention faults and abort() if it’s not an invalid read access.

So far, I only know how to use SEGV_MAPERR and SEGV_ACCERR with sigaction which is irrelevant of course.


Solution

  • It turns out that in Linux on x86-64 (aka AMD64) architecture, this is in fact quite feasible.

    Here is an example program, crasher.c:

    #define  _POSIX_C_SOURCE 200809L
    #define  _GNU_SOURCE
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/mman.h>
    #include <ucontext.h>
    #include <signal.h>
    #include <string.h>
    #include <stdio.h>
    #include <errno.h>
    
    #if !defined(__linux__) || !defined(__x86_64__)
    #error This example only works in Linux on x86-64.
    #endif
    
    #define  ALTSTACK_SIZE  262144
    
    static const char hex_digit[16] = {
        '0', '1', '2', '3', '4', '5', '6', '7',
        '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
    };
    
    static inline const char *signal_name(const int signum)
    {
        switch (signum) {
        case SIGSEGV: return "SIGSEGV";
        case SIGBUS:  return "SIGBUS";
        case SIGILL:  return "SIGILL";
        case SIGFPE:  return "SIGFPE";
        case SIGTRAP: return "SIGTRAP";
        default:      return "(unknown)";
        }
    }
    
    static inline ssize_t internal_write(int fd, const void *buf, size_t len)
    {
        ssize_t retval;
        asm volatile ( "syscall\n\t"
                     : "=a" (retval)
                     : "a" (1), "D" (fd), "S" (buf), "d" (len)
                     : "rcx", "r11" );
        return retval;
    }
    
    static inline int wrerr(const char *p, const char *q)
    {
        while (p < q) {
            ssize_t n = internal_write(STDERR_FILENO, p, (size_t)(q - p));
            if (n > 0)
                p += n;
            else
            if (n == 0)
                return EIO;
            else
                return -n;
        }
        return 0;
    }
    
    static inline int wrs(const char *p)
    {
        if (p) {
            const char *q = p;
            while (*q)
                q++;
            return wrerr(p, q);
        }
        return 0;
    }
    
    static inline int wrh(unsigned long h)
    {
        static char buffer[4 + 2 * sizeof h];
        char       *p = buffer + sizeof buffer;
    
        do {
            *(--p) = hex_digit[h & 15];
            h /= 16UL;
        } while (h);
    
        *(--p) = 'x';
        *(--p) = '0';
    
        return wrerr(p, buffer + sizeof buffer);
    }
    
    static void crash_handler(int signum, siginfo_t *info, void *contextptr)
    {
        if (info) {
            ucontext_t *const ctx = (ucontext_t *const)contextptr;
            wrs(signal_name(signum));
            if (ctx->uc_mcontext.gregs[REG_ERR] & 16) {
                const unsigned long sp = ctx->uc_mcontext.gregs[REG_RSP];
                /* Instruction fetch */
                wrs(": Bad jump to ");
                wrh((unsigned long)(info->si_addr));
                if (sp && !(sp & 7)) {
                    wrs(" probably by the instruction just before ");
                    wrh(*(unsigned long *)sp);
                }
                wrs(".\n");
            } else
            if (ctx->uc_mcontext.gregs[REG_ERR] & 2) {
                /* Write access */
                wrs(": Invalid write attempt to ");
                wrh((unsigned long)(info->si_addr));
                wrs(" by instruction at ");
                wrh(ctx->uc_mcontext.gregs[REG_RIP]);
                wrs(".\n");
            } else {
                /* Read access */
                wrs(": Invalid read attempt from ");
                wrh((unsigned long)(info->si_addr));
                wrs(" by instruction at ");
                wrh(ctx->uc_mcontext.gregs[REG_RIP]);
                wrs(".\n");
            }
        }
    
        raise(SIGKILL);
    }
    
    static int install_crash_handler(void)
    {
        stack_t           altstack;
        struct sigaction  act;
    
        altstack.ss_size = ALTSTACK_SIZE;
        altstack.ss_flags = 0;
        altstack.ss_sp = mmap(NULL, altstack.ss_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_GROWSDOWN, -1, 0);
        if (altstack.ss_sp == MAP_FAILED) {
            const int retval = errno;
            fprintf(stderr, "Cannot map memory for alternate stack: %s.\n", strerror(retval));
            return retval;
        }
        if (sigaltstack(&altstack, NULL)) {
            const int retval = errno;
            fprintf(stderr, "Cannot use alternate signal stack: %s.\n", strerror(retval));
            return retval;
        }
    
        memset(&act, 0, sizeof act);
        sigemptyset(&act.sa_mask);
        act.sa_flags = SA_SIGINFO | SA_ONSTACK;
        act.sa_sigaction = crash_handler;
        if (sigaction(SIGSEGV, &act, NULL) == -1 ||
            sigaction(SIGBUS,  &act, NULL) == -1 ||
            sigaction(SIGILL,  &act, NULL) == -1 ||
            sigaction(SIGFPE,  &act, NULL) == -1) {
            const int retval = errno;
            fprintf(stderr, "Cannot install crash signal handlers: %s.\n", strerror(retval));
            return retval;
        }
    
        return 0;
    }
    
    int main(int argc, char *argv[])
    {
        void         (*jump)(void) = 0;
        unsigned char *addr = (unsigned char *)0;
    
        if (argc < 2 || argc > 3 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
            fprintf(stderr, "\n");
            fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
            fprintf(stderr, "       %s call [ address ]\n", argv[0]);
            fprintf(stderr, "       %s read [ address ]\n", argv[0]);
            fprintf(stderr, "       %s write [ address ]\n", argv[0]);
            fprintf(stderr, "\n");
            return EXIT_SUCCESS;
        }
        if (argc > 2 && argv[2][0] != '\0') {
            char          *end = NULL;
            unsigned long  val;
    
            errno = 0;
            val = strtoul(argv[2], &end, 0);
            if (errno) {
                fprintf(stderr, "%s: %s.\n", argv[2], strerror(errno));
                return EXIT_FAILURE;
            }
            if (end)
                while (*end == '\t' || *end == '\n' || *end == '\v' ||
                       *end == '\f' || *end == '\r' || *end == ' ')
                    end++;
            if (!end || end <= argv[2] || *end) {
                fprintf(stderr, "%s: Not a valid address.\n", argv[2]);
                return EXIT_FAILURE;
            }
    
            jump = (void *)val;
            addr = (void *)val;
        }
    
        if (install_crash_handler())
            return EXIT_FAILURE;
    
        if (argv[1][0] == 'c' || argv[1][0] == 'C') {
            printf("Calling address %p: ", (void *)jump);
            fflush(stdout);
            jump();
            printf("Done.\n");
    
        } else
        if (argv[1][0] == 'r' || argv[1][0] == 'R') {
            unsigned char  val;
    
            printf("Reading from address %p: ", (void *)addr);
            fflush(stdout);
            val = *addr;
            printf("0x%02x, done.\n", val);
    
        } else
        if (argv[1][0] == 'w' || argv[1][1] == 'W') {
            printf("Writing 0xC4 to address %p: ", (void *)addr);
            fflush(stdout);
            *addr = 0xC4;
            printf("Done.\n");
        }
    
        printf("No crash.\n");
        return EXIT_SUCCESS;
    }
    

    Compile it using e.g.

    gcc -Wall -O2 crasher.c -o crasher
    

    You can test a call, a read, or a write to an arbitrary address by specifying the operation and optionally the address on the command line. Run without parameters to see the usage.

    Some example runs on my machine:

    ./crasher call 0x100
    Calling address 0x100: SIGSEGV: Bad jump to 0x100 probably by the instruction just before 0x400c4e.
    Killed
    
    ./crasher write 0x24
    Writing 0xC4 to address 0x24: SIGSEGV: Invalid write attempt to 0x24 by instruction at 0x400bad.
    Killed
    
    ./crasher read 0x16
    Reading from address 0x16: SIGSEGV: Invalid read attempt from 0x16 by instruction at 0x400ca3.
    Killed
    
    ./crasher write 0x400ca3
    Writing 0xC4 to address 0x400ca3: SIGSEGV: Invalid write attempt to 0x400ca3 by instruction at 0x400bad.
    Killed
    
    ./crasher read 0x400ca3
    Reading from address 0x400ca3: 0x41, done.
    No crash.
    

    Note that the type of the access is obtained from the ((ucontext_t *)contextptr)->uc_mcontext.gregs[REG_ERR] register (from the signal handler context); it matches the x86_pf_error_code enums as defined in arch/x86/mm/fault.c in the Linux kernel sources.

    The crash handler itself is quite straightforward, only needing to exmine the aforementioned "register" to obtain the information the OP seeks.

    For outputting the crash report, I open-coded the write() syscall. (For some reason, the small buffer needed by the wrh() function cannot be on the stack, so I just made it static instead.)

    I did not bother to implement the mincore() syscall to verify for example the stack address (sp in the crash_handler() function); it might be necessary to avoid double faults (SIGSEGV occurring in the crash_handler() itself).

    Similarly, I didn't bother to open-code the raise() at the end of crash_handler(), because nowadays on x86-64 it is implemented in the C library using the tgkill(pid, tid, signum) syscall, which means I'd also had to open-code the getpid() and gettid() syscalls. I was just lazy.

    Finally, the above code is written quite carelessly, as I myself only found this after exchanging comments with the OP, user2284570, and just wanted to throw something together to see if this approach actually works reliably. (It seems it does, but I've only tested this lightly and only on one machine.) So, if you notice any bugs, typos, thinkos, or other things to fix in the code, please let me know in a comment, so I can fix it.