Debugging Embedded Linux Memory Corruption with
mprotect
Memory corruption—often called memory stomping—is one of the most frustrating problems in embedded software development. These bugs can remain hidden throughout internal testing, only to surface unexpectedly in production with seemingly random crashes.
When conventional debugging tools fail to expose the root cause, Linux provides a powerful built-in mechanism: mprotect(). By modifying page permissions at runtime, mprotect can immediately trap illegal memory accesses, allowing developers to identify the exact instruction responsible for the fault.
This article explains how mprotect works, its strengths and limitations, and how it can be used to detect memory corruption in Embedded Linux applications.
What Is mprotect()?
#
mprotect() is a Linux/Unix system call that changes the access permissions of existing memory pages. Instead of detecting corruption after it has already occurred, it actively prevents unauthorized access by modifying the Memory Management Unit (MMU) page tables.
Whenever code attempts to violate those permissions, the processor raises a page fault, which the kernel delivers to the application as a SIGSEGV (Segmentation Fault) signal.
The function prototype is:
#include <unistd.h>
#include <sys/mman.h>
int mprotect(const void *addr, size_t len, int prot);
The specified memory region is assigned one or more protection flags:
PROT_READ Read access allowed
PROT_WRITE Write access allowed
PROT_EXEC Execute access allowed
PROT_NONE No access permitted
Because protection is enforced by the MMU itself, illegal accesses are detected immediately rather than long after corruption has propagated through the program.
Why Use mprotect?
#
Compared with traditional memory debugging tools, mprotect offers several practical advantages.
Immediate Fault Detection #
Instead of discovering corruption after a crash or during postmortem analysis, mprotect stops execution at the exact instruction that performs the invalid access.
This dramatically reduces debugging time.
Minimal Runtime Overhead #
Unlike heavyweight instrumentation tools, page protection can be enabled only while debugging specific components, leaving normal application performance largely unaffected.
Selective Protection #
Developers can protect only the memory regions that matter most, such as:
- Critical control structures
- Driver buffers
- Shared memory
- Communication queues
- Frequently corrupted objects
Limitations #
Although extremely useful, mprotect is not a universal debugging solution.
The primary limitations include:
- Protection operates at the page level (typically 4 KB)
- Frequent permission changes require system calls
- It cannot detect corruption occurring entirely within the same protected page
Despite these restrictions, it remains one of the most effective techniques for tracking elusive buffer overflows.
How mprotect Works
#
The basic concept is straightforward.
Imagine system memory as a building divided into rooms. mprotect places electronic locks on selected rooms. Whenever someone attempts to enter a restricted room, an alarm is triggered immediately.
Internally, the process looks like this:
CPU Memory Access
│
▼
┌──────────────────────┐
│ MMU Page Permission │
│ Check │
└──────────┬───────────┘
│
Access Allowed
│
▼
Continue Execution
│
▼
Access Denied
│
▼
Page Fault
│
▼
Kernel Sends SIGSEGV
│
▼
Signal Handler Executes
Instead of silently overwriting memory, the program stops exactly where the illegal access occurs.
Installing a SIGSEGV Handler #
The first step is installing a signal handler capable of reporting the offending address.
#include <signal.h>
#include <unistd.h>
void sigsegv_handler(int sig, siginfo_t *si, void *unused)
{
char buf[128];
int len = snprintf(buf,
sizeof(buf),
"SIGSEGV at address %p\n",
si->si_addr);
write(STDERR_FILENO, buf, len);
_exit(EXIT_FAILURE);
}
The si_addr field identifies the exact address that triggered the fault.
Inside signal handlers, only async-signal-safe functions should be called. Low-level system calls such as write() are preferred over functions like printf() or malloc().
Protecting Memory with Guard Pages #
A common debugging strategy places inaccessible pages immediately before and after the buffer being monitored.
The resulting memory layout resembles this:
┌─────────────────────────────┐
│ Guard Page (PROT_NONE) │
├─────────────────────────────┤
│ Protected Data │
├─────────────────────────────┤
│ Guard Page (PROT_NONE) │
└─────────────────────────────┘
If a buffer overflow or underflow extends beyond the valid region, execution immediately enters one of the guard pages, causing a SIGSEGV.
This technique is widely used by modern memory allocators and sanitizers.
Dynamically Changing Page Permissions #
Applications can also toggle write access on demand.
#define PAGE_SIZE 4096
void *align_to_page(void *addr)
{
uintptr_t p = (uintptr_t)addr;
return (void *)(p & ~(PAGE_SIZE - 1));
}
int set_page_permission(void *addr, int writable)
{
void *page = align_to_page(addr);
int prot = PROT_READ;
if (writable)
prot |= PROT_WRITE;
return mprotect(page, PAGE_SIZE, prot);
}
The alignment operation:
addr & ~(PAGE_SIZE - 1)
efficiently rounds an address down to its page boundary using bitwise arithmetic, avoiding slower division operations.
Detecting Buffer Overflows #
The following example allocates three contiguous pages.
The middle page stores application data, while the surrounding pages act as guard pages.
char *pages = mmap(NULL,
PAGE_SIZE * 3,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1,
0);
char *pre_guard = pages;
char *data = pages + PAGE_SIZE;
char *post_guard = pages + PAGE_SIZE * 2;
mprotect(pre_guard, PAGE_SIZE, PROT_NONE);
mprotect(post_guard, PAGE_SIZE, PROT_NONE);
data[0] = 'A'; // Safe
data[PAGE_SIZE] = 'B'; // Triggers SIGSEGV
Writing one byte beyond the valid data region immediately enters the protected page, allowing the bug to be identified at the exact point of failure.
Temporarily Enabling Writes #
Sometimes developers want memory to remain read-only except during controlled updates.
char *page = mmap(NULL,
PAGE_SIZE,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1,
0);
mprotect(page, PAGE_SIZE, PROT_READ);
set_page_permission(page, 1);
page[0] = 'Y';
set_page_permission(page, 0);
This pattern is useful when protecting configuration data or shared structures that should rarely be modified.
Typical Embedded Linux Applications #
mprotect is particularly valuable when debugging:
- Buffer overflows
- Buffer underflows
- Memory stomping
- Illegal writes to configuration tables
- Shared-memory corruption
- DMA buffer misuse
- Heap corruption
- Driver debugging
Because the MMU catches violations immediately, it significantly shortens the time required to locate intermittent bugs.
mprotect vs. Valgrind
#
Although both tools help detect memory issues, they serve different purposes.
| Feature | mprotect |
Valgrind |
|---|---|---|
| Detection Time | Immediate | During instrumentation |
| Runtime Overhead | Low | High |
| Granularity | Page-level | Byte-level |
| Deployment | Native execution | Instrumented execution |
| Best For | Buffer overflows and illegal writes | Memory leaks and invalid accesses |
In many embedded environments where Valgrind cannot easily be deployed, mprotect provides a lightweight and practical alternative.
Best Practices #
To maximize the effectiveness of mprotect:
- Surround critical buffers with guard pages.
- Protect configuration data as read-only after initialization.
- Install robust SIGSEGV handlers during debugging.
- Enable page protection only in debug builds when performance matters.
- Combine
mprotectwith AddressSanitizer or Valgrind during desktop testing for broader coverage.
Using multiple techniques together yields the highest probability of catching difficult memory bugs.
Final Thoughts #
Memory corruption bugs are notoriously difficult to reproduce because the actual overwrite often occurs long before the program crashes.
mprotect changes the debugging strategy entirely. Rather than investigating corruption after the damage has been done, it prevents unauthorized accesses from succeeding in the first place. Every illegal read or write becomes an immediate, deterministic failure with an exact fault address, making root-cause analysis dramatically easier.
While page-level granularity and system call overhead prevent it from being a universal solution, mprotect remains one of the most effective debugging tools available for Embedded Linux developers. Combined with guard pages and thoughtful memory layout, it can quickly expose elusive buffer overflows and memory stomping issues that might otherwise take days—or even weeks—to isolate.