Signals in Memory Mappings SIGSEGV and SIGBUS

 

Signals in Memory Mappings
When and why SIGSEGV and SIGBUS are delivered during mmap() access

Two Signals to Know

Accessing a memory-mapped region can result in two different signals, depending on the nature of the fault. Both kill the process by default (unless caught). Understanding which signal fires — and when — is important for writing robust programs and for debugging segfaults.

SIGSEGV vs SIGBUS — Quick Reference
Signal Cause Example Scenario
SIGSEGV Accessing memory in a way that violates the page’s protection bits, or accessing an unmapped address entirely Writing to a PROT_READ-only mapping; reading from an unmapped region; NULL pointer dereference
SIGBUS Accessing a page in a file-backed mapping for which no corresponding file content exists (mapping extends beyond end of file) File is 3 pages, mapping is 4 pages, and you access the 4th page

SIGSEGV — Segmentation Violation

SIGSEGV is delivered whenever the processor generates a protection fault or the kernel determines an access is illegal. In the context of mmap():

Scenario 1: Writing to a read-only mapping (PROT_READ only)
Scenario 2: Reading from a no-access mapping (PROT_NONE)
Scenario 3: Accessing an address outside any mapped region (e.g., after munmap())
Scenario 4: NULL pointer dereference (address 0 is typically unmapped)

Example 1: Triggering SIGSEGV by Writing to Read-Only Mapping
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <signal.h>
#include <unistd.h>

void sigsegv_handler(int sig) {
    printf("Caught SIGSEGV (signal %d): tried to write to read-only mapping!\n", sig);
    /* Real programs should not do complex work in a signal handler */
    _exit(1);
}

int main(void) {
    int fd;
    struct stat sb;
    char *addr;

    signal(SIGSEGV, sigsegv_handler);

    fd = open("/etc/hostname", O_RDONLY);
    if (fd == -1) { perror("open"); exit(1); }
    fstat(fd, &sb);

    /* Map with PROT_READ only */
    addr = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (addr == MAP_FAILED) { perror("mmap"); exit(1); }
    close(fd);

    printf("File content: %.*s\n", (int)sb.st_size, addr);

    /* This write violates PROT_READ — SIGSEGV is delivered */
    addr[0] = 'X';   /* <-- triggers SIGSEGV */

    munmap(addr, sb.st_size);
    return 0;
}

Example 2: SIGSEGV After munmap()
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <signal.h>

void handler(int sig) {
    printf("SIGSEGV: accessed unmapped region after munmap()\n");
    _exit(1);
}

int main(void) {
    signal(SIGSEGV, handler);

    char *addr = mmap(NULL, 4096,
                      PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANONYMOUS,
                      -1, 0);
    if (addr == MAP_FAILED) { perror("mmap"); exit(1); }

    addr[0] = 'A';        /* OK: mapping is valid */
    printf("Wrote: %c\n", addr[0]);

    munmap(addr, 4096);   /* Remove the mapping */

    /* Accessing addr now is illegal — SIGSEGV */
    char c = addr[0];     /* <-- triggers SIGSEGV */
    printf("Should not reach here: %c\n", c);

    return 0;
}

SIGBUS — Bus Error from File Mapping

SIGBUS is specific to file-backed mappings. It fires when you access a page that is within the mapped virtual address range but beyond the actual end of the file. The last page in the file is zero-padded up to the page boundary — but any whole pages after that generate SIGBUS.

File Page 0
(valid data)
File Page 1
(valid data)
File Page 2
(partial: zero-padded to page boundary)
Mapped Page 3
SIGBUS — no file content!
← file size = 2.5 pages → ← mapping extended beyond file →

Example 3: Triggering SIGBUS by Accessing Beyond File End
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <signal.h>
#include <unistd.h>

void sigbus_handler(int sig) {
    printf("Caught SIGBUS (signal %d): "
           "accessed beyond end of file in mapping!\n", sig);
    _exit(1);
}

int main(void) {
    int fd;
    char *addr;
    long ps = sysconf(_SC_PAGESIZE);

    signal(SIGBUS, sigbus_handler);

    /* Create a file that is only 100 bytes — much less than one page */
    fd = open("small.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
    write(fd, "Hello", 5);   /* file size = 5 bytes */

    /* Map TWO pages even though file is only 5 bytes */
    addr = mmap(NULL, 2 * ps, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) { perror("mmap"); exit(1); }
    close(fd);

    /* Accessing within first page is fine (zero-padded past 5 bytes) */
    printf("addr[0] = %c\n", addr[0]);
    printf("addr[100] = %d (zero-padded)\n", addr[100]);

    /* Accessing the second page (beyond end of file) = SIGBUS */
    printf("About to access second page...\n");
    char c = addr[ps];   /* <-- triggers SIGBUS */
    printf("Should not reach: %c\n", c);

    munmap(addr, 2 * ps);
    return 0;
}

Example 4: Handling SIGBUS Gracefully with sigsetjmp/siglongjmp

Real programs (like database engines) sometimes catch SIGBUS to detect when a file has been truncated under an active mapping:

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

static sigjmp_buf jump_buf;

void sigbus_handler(int sig) {
    siglongjmp(jump_buf, 1);
}

int main(void) {
    int fd;
    long ps = sysconf(_SC_PAGESIZE);
    char *addr;

    struct sigaction sa = { .sa_handler = sigbus_handler };
    sigemptyset(&sa.sa_mask);
    sigaction(SIGBUS, &sa, NULL);

    fd = open("test.dat", O_RDWR | O_CREAT | O_TRUNC, 0644);
    write(fd, "DATA", 4);  /* 4-byte file */

    addr = mmap(NULL, ps, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) { perror("mmap"); exit(1); }
    close(fd);

    if (sigsetjmp(jump_buf, 1) == 0) {
        /* Attempt to read from second page — triggers SIGBUS */
        char c = addr[ps - 1];   /* Near end of page, no file content */
        printf("Read: %d\n", c);
    } else {
        printf("Recovered from SIGBUS — file shorter than mapping\n");
    }

    munmap(addr, ps);
    return 0;
}

Changing Protection: mprotect()

After a mapping is created, you can change its protection flags using mprotect():

#include <sys/mman.h>

int mprotect(void *addr, size_t len, int prot);
/* Returns 0 on success, -1 on error */
/* Example: JIT compiler workflow */
/* Step 1: Allocate writable anonymous memory */
char *code = mmap(NULL, 4096,
                  PROT_READ | PROT_WRITE,
                  MAP_PRIVATE | MAP_ANONYMOUS,
                  -1, 0);

/* Step 2: Write machine code into it */
/* ... emit_instructions(code) ... */

/* Step 3: Make it executable (remove write, add exec) */
mprotect(code, 4096, PROT_READ | PROT_EXEC);

/* Step 4: Call the generated code */
/* ((void(*)(void))code)(); */

Interview Questions & Answers
Q1. What is the difference between SIGSEGV and SIGBUS in the context of mmap()?

A: SIGSEGV means a protection violation — you accessed memory with the wrong permission (e.g., writing to a read-only page) or accessed an unmapped address entirely. SIGBUS is specific to file-backed mappings and means the page you accessed exists in the virtual address range but has no corresponding content in the file (the file is shorter than the mapping). Both kill the process by default.

Q2. You map a 4096-byte file into a 2-page (8192 byte) region. What happens if you access byte 4097?

A: Byte 4097 is in the second page, which is entirely beyond the file. The kernel delivers SIGBUS. However, bytes 4096–4096 (the last byte of the first page past the file’s 4096-byte content) are fine because the kernel zero-pads the last page up to the page boundary.

Q3. Can SIGBUS happen with anonymous mappings?

A: No. SIGBUS is specific to file-backed mappings. Anonymous mappings are always backed by physical RAM or swap, so there is always content for any page within the mapped range. Accessing out-of-range anonymous pages generates SIGSEGV, not SIGBUS.

Q4. A program creates a MAP_SHARED mapping of a 1 MB file, and another process truncates the file to 512 KB while the first process is reading it. What happens?

A: If the first process accesses pages in the range 512 KB – 1 MB (pages that now have no corresponding file content), the kernel delivers SIGBUS. This is a real-world risk with mmap-based databases — you must coordinate access carefully, or catch SIGBUS to handle the truncation gracefully.

Q5. What is mprotect() used for and give a real-world example?

A: mprotect() changes the protection of an existing mapping. A classic use is in JIT (Just-In-Time) compilers: first allocate a PROT_READ|PROT_WRITE anonymous mapping to write machine code into, then call mprotect() to change it to PROT_READ|PROT_EXEC before executing it. This prevents accidentally writing to executable code, and satisfies the W^X (write XOR execute) security principle.

Leave a Reply

Your email address will not be published. Required fields are marked *