Map unallocated memory after SIGSEGV

87 Views Asked by At

After catching a SIGSEGV through a signal handler, I am trying to use mmap to map the address. I can't figure out why mmap fails with Cannot allocate memory error.

Here is the C code. I am trying to run this on macOS.

#include <signal.h>
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>

void *addr;

static void catch_function(int signo, siginfo_t *si, void *unused) {
  printf("Got SIGSEGV at address: 0x%lx\n", (long)si->si_addr);

  void *ptr = mmap(addr, 100, PROT_READ | PROT_WRITE,
                   MAP_SHARED | MAP_ANON | MAP_FIXED, 0, 0);

  if (ptr == MAP_FAILED) {
    perror("mmap");
  }
}

int main(void) {
  int page_size = getpagesize();

  if (signal(SIGSEGV, catch_function) == SIG_ERR) {
    fputs("An error occurred while setting a signal handler.\n", stderr);
    return 1;
  }

  addr = (void *)(page_size * 100);

  // trigger segfault
  *((int *)addr) = 1;
}
2

There are 2 best solutions below

2
Yousha Aleayoub On

The cause of the segmentation fault(SIGSEGV) is the attempt to write to an invalid memory address.

In your case, I think the addr variable is set to a memory address that is not allocated.

Here's the updated code:

#include <signal.h>
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>

void *addr;

static void catch_function(int signo, siginfo_t *si, void *unused) {
  printf("Got SIGSEGV at address: 0x%lx\n", (long)si->si_addr);

  void *ptr = mmap(NULL, 100, PROT_READ | PROT_WRITE, // Here
                   MAP_SHARED | MAP_ANON, -1, 0); // Here

  if (ptr == MAP_FAILED) {
    perror("mmap");
  } else {
    addr = ptr; // Here
  }
}

int main(void) {
  int page_size = getpagesize();

  if (signal(SIGSEGV, catch_function) == SIG_ERR) {
    fputs("An error occurred while setting a signal handler.\n", stderr);
    return 1;
  }

  addr = (void *)(page_size * 100);

  // trigger segfault
  *((int *)addr) = 1;
}

(Didn't tested it yet)

4
John Bollinger On

I can't figure out why mmap fails with Cannot allocate memory error.

That would be an ENOMEM error. The manual for MacOS's version of mmap() documents these conditions under which ENOMEM will be raised:

 [ENOMEM]          MAP_FIXED is specified and the address range specified
                   exceeds the address space limit for the process.

 [ENOMEM]          MAP_FIXED is specified and the address specified by
                   the addr parameter isn't available.

 [ENOMEM]          MAP_ANON is specified and insufficient memory is
                   available.

You are trying to map only 100 bytes, in a process with a very small image to begin with, so the first and third alternatives both seem implausible. That leaves the second -- you used MAP_FIXED (which you did) and the address you asked for wasn't available. That should be understood as inclusive of any address in the 100-byte range starting at the requested address not being available for mapping.

The address you asked for was very low, so it's entirely plausible that the OS reserves it. Or that it was already using it for something else (protected from the program's attempt to read it). You can reduce the likelihood of such issues (see below), but it is extremely difficult to eliminate the possibility altogether.

But that's not the only problem with this short code. There is also at least:

  • Given this function signature:

    static void catch_function(int signo, siginfo_t *si, void *unused) {
    

    ... this call to signal():

      if (signal(SIGSEGV, catch_function) == SIG_ERR) {
    

    produces undefined behavior because the type of the second argument is incompatible with the declared type (void (*)(int)) of the second parameter to signal(). Your compiler should be warning you, and you should be paying attention to it. If your compiler is not warning you about that issue then either turn up the warning level until it does, or get a better compiler.

    You should instead be using sigaction() to register such a handler. And to register any handler, really, because signal()'s behavior is non-portable except for resetting a signal to its default disposition.

  • When you call mmap() with MAP_ANON, you generally should pass -1 as the file descriptor. Some systems require that. MacOS, on the other hand, attributes special significance to you passing a valid, open file descriptor.

  • The printf(), mmap(), and perror() functions are not async-signal-safe, so calling them from a signal handler produces undefined behavior. This is something that really does sometimes break in the real world. That leaves you without any well-defined way to make the accessed address readable, so I see no portable way for your on-demand allocation idea to work.

  • In the event that the mmap() call fails, so that you cannot make the value of addr a valid address, you should ensure that the program terminates. If you don't, and if the system does not reset the disposition for SIGSEGV when the handler is invoked, then you will end up with a segfault loop.

But perhaps you have knowledge that on your particular system it is ok to call mmap() from a signal handler, or you want to act as if you did. In that event, instead of choosing a random address to demonstrate with, a better strategy would be to allocate some memory and then deliberately perform out of bounds accesses relative to that allocation. Maybe something more like this:

#include <signal.h>
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>

void *addr;
volatile sigatomic_t stop = 0;

static void catch_function(int signo, siginfo_t *si, void *unused) {
  printf("Got SIGSEGV at address: %p\n", (void *)si->si_addr);
  stop = 1;

  void *ptr = mmap(addr, 4096, PROT_READ | PROT_WRITE,
                   MAP_SHARED | MAP_ANON | MAP_FIXED, -1, 0);

  if (ptr == MAP_FAILED) {
    perror("mmap");
    abort();
  }
}

int main(void) {
  int page_size = getpagesize();

  if (signal(SIGSEGV, catch_function) == SIG_ERR) {
    fputs("An error occurred while setting a signal handler.\n", stderr);
    return 1;
  }

  addr = malloc(page_size);
  if (!addr) {
    fputs("Initial memory allocation failed\n", stderr);
    exit(1);
  }

  // trigger segfault. It might take more than one attempt.
  unsigned int sum = 0;
  do {
    addr += page_size;
    sum += *(unsigned int *)addr;
  } while (!stop);

  // Ensure that the program has external behavior that depends
  // on the memory accesses, else the accesses could be optimized away.
  printf("The sum is %s\n", sum);
}