What is mprotect()?
Every region of virtual memory in a process has access permissions: can it be read? written? executed? Normally these permissions are set when the mapping is created — either by mmap() or by the kernel when it loads the program.
mprotect() lets you change those permissions on any region of virtual memory at runtime, without destroying or recreating the mapping. This is powerful: you can, for example, create a region as read-only and temporarily make it writable just for an update, then lock it read-only again.
Function Signature
#include <sys/mman.h>
int mprotect(void *addr, size_t length, int prot);
/* Returns 0 on success, -1 on error (sets errno) */
Three arguments:
- addr — starting address of the region. Must be page-aligned.
- length — number of bytes. Rounded up internally to the next page boundary.
- prot — new permissions bitmask.
Protection Flags (prot argument)
The prot argument must be either PROT_NONE alone, or a bitwise-OR of one or more of:
| Flag | Meaning | /proc/../maps char |
|---|---|---|
| PROT_NONE | No access at all — any access triggers SIGSEGV | — |
| PROT_READ | Pages can be read | r– |
| PROT_WRITE | Pages can be written | -w- |
| PROT_EXEC | Pages can be executed as code (JIT compilers, etc.) | –x |
| PROT_READ|PROT_WRITE | Read and write allowed | rw- |
| PROT_READ|PROT_EXEC | Read and execute — typical for code segments | r-x |
How mprotect() Works Internally
| Virtual Memory Region (e.g., 0xb7c00000 — 0xb7d00000) | ||
| Before mprotect() PROT_NONE (—) Any access → SIGSEGV |
→ | After mprotect() PROT_READ|PROT_WRITE (rw-) Read and write allowed |
| The kernel updates the page table entries for all pages in the range. The virtual addresses do not change — only the permissions metadata changes. | ||
Important Rules
The
addr argument must be a multiple of the system page size. Get the page size with sysconf(_SC_PAGESIZE) or getpagesize(). On most Linux systems this is 4096 bytes (4 KB).The kernel rounds
length up to the next page boundary internally. You don’t have to round it yourself, but it’s good practice to be aware that a few extra bytes at the end may be affected.If the process accesses memory in a way that violates the current
prot flags (e.g., writing to a read-only page), the kernel sends SIGSEGV to the process. This is the same signal as a null-pointer dereference.Example 1 — Basic mprotect() with mmap()
This is the classic example from TLPI. Create a 1 MB anonymous mapping with PROT_NONE, verify no access is possible, then change it to PROT_READ|PROT_WRITE.
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
#define LEN (1024 * 1024) /* 1 MB */
int main(void)
{
char *addr;
char cmd[128];
/* Step 1: Create anonymous mapping with PROT_NONE (no access) */
addr = mmap(NULL, LEN, PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}
printf("Mapped at: %p\n", addr);
/* Step 2: Check permissions via /proc/self/maps */
printf("\n--- Before mprotect() ---\n");
snprintf(cmd, sizeof(cmd),
"grep -E '[0-9a-f]+-[0-9a-f]+' /proc/%d/maps | tail -5",
getpid());
system(cmd);
/* You will see "---" in the permissions column */
/* Step 3: Change protection to READ + WRITE */
if (mprotect(addr, LEN, PROT_READ | PROT_WRITE) == -1) {
perror("mprotect");
munmap(addr, LEN);
exit(EXIT_FAILURE);
}
printf("\n--- After mprotect() ---\n");
system(cmd);
/* You will now see "rw-" in the permissions column */
/* Step 4: Now we can actually use the memory */
memset(addr, 0xAB, LEN);
printf("\nFirst byte value: 0x%02X (write succeeded!)\n",
(unsigned char)addr[0]);
munmap(addr, LEN);
return 0;
}
Compile & Run:
gcc -o ex1 ex1.c && ./ex1
You will see the mapping change from --- to rw- in /proc/self/maps.
Example 2 — SIGSEGV When Violating mprotect()
This example demonstrates what happens when you try to write to a read-only protected region. We catch the SIGSEGV with a signal handler to show the violation gracefully.
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/mman.h>
#include <setjmp.h>
#define PAGE_SIZE 4096
static sigjmp_buf jmp_env;
/* Signal handler — catches SIGSEGV */
static void sigsegv_handler(int sig)
{
printf("Caught SIGSEGV! Write to read-only page was blocked.\n");
siglongjmp(jmp_env, 1); /* Jump back safely */
}
int main(void)
{
char *addr;
/* Install SIGSEGV handler */
struct sigaction sa;
sa.sa_handler = sigsegv_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGSEGV, &sa, NULL);
/* Create a readable/writable mapping */
addr = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED) { perror("mmap"); exit(1); }
/* Write some data */
addr[0] = 'A';
printf("Wrote 'A' to page — OK (rw-)\n");
/* Now make it READ-ONLY */
if (mprotect(addr, PAGE_SIZE, PROT_READ) == -1) {
perror("mprotect"); exit(1);
}
printf("Changed protection to PROT_READ (r--)\n");
/* Try to read — should work */
printf("Read value: '%c' — OK\n", addr[0]);
/* Try to WRITE — should trigger SIGSEGV */
if (sigsetjmp(jmp_env, 1) == 0) {
printf("Attempting write to read-only page...\n");
addr[0] = 'B'; /* <<-- This will trigger SIGSEGV */
printf("This line should NOT print.\n");
}
/* Restore write permission so munmap can work cleanly */
mprotect(addr, PAGE_SIZE, PROT_READ | PROT_WRITE);
munmap(addr, LEN);
printf("Done.\n");
return 0;
}
Example 3 — Guard Pages Using PROT_NONE
A very common real-world use of mprotect() is creating guard pages — inaccessible pages placed around a buffer so that overflows are immediately detected via SIGSEGV instead of silently corrupting memory.
| GUARD PAGE PROT_NONE → SIGSEGV on access |
BUFFER (usable memory) PROT_READ | PROT_WRITE 4KB or more |
GUARD PAGE PROT_NONE → SIGSEGV on access |
| Any overflow or underflow immediately triggers SIGSEGV — easy to catch in debugging. | ||
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
/*
* Allocate a buffer surrounded by guard pages.
* Any overflow/underflow hits PROT_NONE page → SIGSEGV.
*/
char *alloc_guarded_buffer(size_t size)
{
long page_size = sysconf(_SC_PAGESIZE);
/* Round size up to page boundary */
size_t buf_pages = (size + page_size - 1) / page_size;
size_t total = (buf_pages + 2) * page_size; /* +2 guard pages */
/* Map entire region as PROT_NONE first */
char *base = mmap(NULL, total, PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (base == MAP_FAILED) { perror("mmap"); return NULL; }
/* Make the middle pages (the actual buffer) read/write */
char *buf = base + page_size; /* skip first guard page */
if (mprotect(buf, buf_pages * page_size,
PROT_READ | PROT_WRITE) == -1) {
perror("mprotect"); munmap(base, total); return NULL;
}
printf("Guard layout:\n");
printf(" [%p] GUARD (PROT_NONE)\n", base);
printf(" [%p] BUFFER (rw-) — %zu bytes\n", buf, size);
printf(" [%p] GUARD (PROT_NONE)\n",
base + page_size + buf_pages * page_size);
return buf;
}
int main(void)
{
size_t buf_size = 128;
char *buf = alloc_guarded_buffer(buf_size);
if (!buf) exit(1);
/* Normal use — fine */
strcpy(buf, "Hello, guard page!");
printf("\nBuffer contents: %s\n", buf);
printf("\nIf you access buf[-1] or buf[PAGE_SIZE], you get SIGSEGV.\n");
/* munmap full region (base = buf - page_size) */
long page_size = sysconf(_SC_PAGESIZE);
munmap(buf - page_size, page_size * 3);
return 0;
}
Example 4 — Page Alignment Helper
When you have a pointer to somewhere inside a page and need to call mprotect(), you must align it to the page boundary first.
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
/* Round address DOWN to page boundary */
#define PAGE_ALIGN_DOWN(addr, pgsz) \
((void *)((uintptr_t)(addr) & ~((uintptr_t)(pgsz) - 1)))
/* Round size UP to page boundary */
#define PAGE_ALIGN_UP(size, pgsz) \
(((size) + (pgsz) - 1) & ~((pgsz) - 1))
int set_memory_protection(void *ptr, size_t size, int prot)
{
long page_size = sysconf(_SC_PAGESIZE);
void *aligned = PAGE_ALIGN_DOWN(ptr, page_size);
size_t aligned_size = PAGE_ALIGN_UP(
size + ((uintptr_t)ptr - (uintptr_t)aligned),
page_size);
printf("Original: ptr=%p, size=%zu\n", ptr, size);
printf("Aligned: ptr=%p, size=%zu\n", aligned, aligned_size);
return mprotect(aligned, aligned_size, prot);
}
int main(void)
{
long page_size = sysconf(_SC_PAGESIZE);
printf("Page size: %ld bytes\n", page_size);
/* This would fail without alignment */
char *mem = mmap(NULL, page_size * 4, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mem == MAP_FAILED) { perror("mmap"); return 1; }
/* Change protection on second page only */
int ret = set_memory_protection(mem + page_size, page_size,
PROT_READ);
printf("mprotect returned: %d\n", ret);
munmap(mem, page_size * 4);
return 0;
}
Example 5 — JIT Code: Write Then Execute
Just-In-Time (JIT) compilers (like in V8, LuaJIT) use a common pattern: allocate a region, write machine code into it (needs PROT_WRITE), then switch to PROT_EXEC before running the code. This is the W^X (Write XOR Execute) security model.
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
/*
* W^X Pattern (Write XOR Execute):
* Phase 1: Write machine code into page → PROT_READ | PROT_WRITE
* Phase 2: Switch to executable → PROT_READ | PROT_EXEC
* Phase 3: Execute the code
* Never have WRITE and EXEC set at the same time (security risk).
*/
int main(void)
{
long page_size = sysconf(_SC_PAGESIZE);
/* Allocate page as writable (NOT executable yet) */
unsigned char *code = mmap(NULL, page_size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (code == MAP_FAILED) { perror("mmap"); return 1; }
/* x86-64 machine code: return the value 42
* Equivalent to: int f() { return 42; }
* Bytes: mov eax, 42 ; ret
*/
unsigned char shellcode[] = {
0xb8, 0x2a, 0x00, 0x00, 0x00, /* mov eax, 42 */
0xc3 /* ret */
};
memcpy(code, shellcode, sizeof(shellcode));
printf("Phase 1: Code written to page (PROT_WRITE)\n");
/* Switch to EXEC — remove WRITE first (W^X) */
if (mprotect(code, page_size, PROT_READ | PROT_EXEC) == -1) {
perror("mprotect"); munmap(code, page_size); return 1;
}
printf("Phase 2: Page switched to PROT_EXEC (no longer writable)\n");
/* Execute the JIT'd function */
int (*func)(void) = (int (*)(void))code;
int result = func();
printf("Phase 3: JIT function returned: %d\n", result);
/* Output: 42 */
munmap(code, page_size);
return 0;
}
Note: On systems with strict W^X enforcement (like OpenBSD), PROT_WRITE | PROT_EXEC simultaneously is not allowed at all. Always use the two-phase approach above.
Error Codes (errno)
| errno | Cause |
|---|---|
| EACCES | Tried to set PROT_WRITE on a file opened read-only via mmap() |
| EINVAL | addr is not page-aligned, or prot has invalid bits set |
| ENOMEM | Address range is not mapped, or kernel ran out of memory for internal tables |
Key Points to Remember
addrmust be a page-aligned address (multiple of page size)lengthis silently rounded up to the next page boundary by the kernelprotmust bePROT_NONEor a combination of READ/WRITE/EXEC flags- Violating protection causes SIGSEGV to be sent to the process
- mprotect() works on any mapped region — anonymous, file-backed, or shared
- You can verify current protections from
/proc/self/maps - The JIT W^X pattern requires switching from WRITE to EXEC (not both at once)
- Guard pages using PROT_NONE detect buffer overflows at page boundaries
Interview Questions & Answers
Q1. What does mprotect() do and what is its main use case?
mprotect() changes the access permissions (read/write/execute) on a region of virtual memory at runtime. Common use cases include: making code pages executable after writing JIT’d machine code, creating guard pages (PROT_NONE) to catch buffer overflows, temporarily making read-only shared data writable for updates, and implementing copy-on-write logic in user space.
Q2. Why must the addr argument be page-aligned?
Because the kernel manages virtual memory in units of pages (typically 4 KB). Page permissions are stored in page table entries, which are indexed by page number. There is no mechanism to set different permissions on individual bytes within a page. Therefore, the starting address must fall exactly on a page boundary. Passing a non-aligned address causes mprotect() to fail with EINVAL.
Q3. What signal is sent when a process violates memory protection?
The kernel sends SIGSEGV (Segmentation Fault) to the process. This is the same signal sent for a null-pointer dereference or any invalid memory access. The default action for SIGSEGV is to terminate the process and optionally generate a core dump.
Q4. What is PROT_NONE used for?
PROT_NONE means the page has no access at all — reads, writes, and executes all trigger SIGSEGV. It is used for: (1) guard pages around buffers to detect overflow/underflow, (2) reserving address space without committing physical memory, (3) initially creating a region that will be unlocked only when needed (lazy access control), (4) protecting sensitive regions from accidental access.
Q5. Explain the W^X (Write XOR Execute) security model and how mprotect() enables it.
W^X means a memory page should never be both writable AND executable at the same time. Allowing both is dangerous because an attacker could write shellcode into a page and immediately execute it. The mprotect()-based pattern: (1) allocate with PROT_READ|PROT_WRITE, (2) write code bytes, (3) call mprotect() to switch to PROT_READ|PROT_EXEC (removing write permission), (4) then execute. This way WRITE and EXEC are never simultaneously active.
Q6. Can mprotect() be applied to a file-backed mmap() region? What restriction applies?
Yes. However, you cannot set PROT_WRITE on a region that was mapped from a file opened read-only — that would fail with EACCES. Also, for shared file mappings, PROT_WRITE implies writes go back to the file. For private mappings (MAP_PRIVATE), PROT_WRITE triggers copy-on-write, so the original file is not modified.
Q7. How can you verify the current protection of a memory region?
Read /proc/self/maps (or /proc/PID/maps for another process). Each line shows address range, permissions (e.g., rw-p), offset, device, inode, and path. The permission string uses r, w, x, and - for read, write, execute, and denied respectively. The fourth character is p (private/MAP_PRIVATE) or s (shared/MAP_SHARED).
Q8. What is the difference between PROT_NONE mapping and unmapped memory?
Both cause SIGSEGV on access, but they are fundamentally different. PROT_NONE means the virtual address range is mapped (it appears in /proc/maps) but access is forbidden. Unmapped memory has no entry in the page table at all. PROT_NONE is useful when you want to reserve a contiguous address range (so it won’t be used by other mmap calls) but not allow access yet. Unmapped memory cannot be selectively enabled later without a new mmap() call.
