Two processes can use the same virtual address, such as 0x7f100000, without gaining access to the same memory. A process can read its heap but cannot normally read the kernel's data. It can modify its writable variables but should not be able to rewrite its executable instructions.
These boundaries are enforced by memory protection.
Memory protection combines hardware privilege levels, virtual address translation, page permissions, and kernel policy. The processor checks the active mapping on every instruction fetch, load, and store. When an access violates the mapping, execution traps into the kernel instead of silently reaching the forbidden bytes.
This mechanism serves both reliability and security. It contains ordinary pointer bugs, isolates mutually untrusted processes, protects the kernel, and makes several code-injection techniques harder.
Memory protection defines which virtual-memory operations are permitted in the current address space and privilege mode.
A multiprogramming system places unrelated workloads on the same physical machine. Without enforced boundaries, any process that discovered a physical address could read another process's credentials, overwrite its state, or modify kernel code.
Memory protection must prevent at least three kinds of unwanted access:
| Attempted access | Outcome |
|---|---|
| Process A reaching process B's private memory | Blocked |
| User mode reaching kernel-only memory | Blocked |
| An ordinary store reaching read-only or executable code | Blocked |
The first boundary isolates processes. The second separates unprivileged applications from the operating system. The third gives different regions inside one address space different roles.
These boundaries must be enforced independently of application cooperation. A malicious process will not voluntarily avoid another process's memory, and a corrupted program cannot be trusted to check every pointer correctly.
The processor and kernel therefore place the policy below the application. Application instructions can request memory operations, but hardware checks decide whether those operations may complete.
A CPU instruction uses a virtual address. The memory-management unit, or MMU, translates the virtual page containing that address to a physical frame.
The translation also contains protection information:
Each virtual page has a page-table entry recording:
For each memory operation, the processor considers both the mapping and the requested access:
It also considers the current CPU privilege mode. A page available to supervisor mode may be unavailable to user mode even if both use the same virtual address.
Recent translations and their permissions are normally cached in the translation lookaside buffer, or TLB. A TLB hit does not bypass protection; the cached entry includes the permissions needed for the hardware check.
If no valid page-table translation exists or the resolved permissions disallow the access, the processor raises a synchronous fault and transfers control to the kernel. The fault identifies facts such as the address, operation type, and current privilege context.
The kernel then decides whether the access represents a valid condition it can resolve or an invalid operation that should be reported to or terminate the process.
Memory protection would be meaningless if an application could directly change a read-only page to writable or map arbitrary physical frames belonging to the kernel.
Page tables and the privileged registers that select them are controlled by the kernel. An application can request mapping changes through interfaces such as mmap(), mprotect(), and munmap(), but the kernel validates the request and updates the hardware state on the application's behalf.
The distinction is:
The application asks and the kernel decides. Nothing the application does writes page permissions directly.
A process can usually make its own writable mapping read-only. It cannot use mprotect() to take ownership of an unmapped address belonging to another process or grant itself access to arbitrary kernel memory.
When the kernel changes permissions, it must also invalidate stale TLB entries that could still authorize the old access. On a multicore machine, this can require coordinating with other CPU cores running threads from the same address space.
Protection depends on both parts: page tables must contain the intended policy, and no processor may continue using an obsolete, more permissive translation.
The virtual address is interpreted in the context of the current address space.
Suppose two processes both access virtual address 0x400000:
The same virtual address in both processes reaches different physical memory. Neither process can name the other's frame, because naming happens through its own page table.
The numeric addresses match, but the mappings lead to different physical memory. Process A cannot reach frame 307 merely by using Process B's virtual address.
During a context switch, the kernel changes the active address-space context. Hardware then interprets virtual addresses using the selected process's translations. Address-space identifiers supported by the processor can keep translations from multiple processes cached while still preventing one process from using another's entries.
Separate address spaces protect processes even when they run under the same UID. User identity can control whether one process may ask the kernel to inspect or debug another, but equal UIDs do not merge their page tables.
Cross-process access must use an explicit kernel-controlled mechanism, such as shared memory, a debugger interface, or interprocess communication. The kernel can apply authorization at that boundary.
The kernel needs access to memory that ordinary applications must not modify: page tables, process metadata, filesystem caches, device state, credentials, and the kernel's own code.
Page-table entries distinguish pages that user mode may access from pages reserved for supervisor mode. If user-mode code attempts to load from or store into a supervisor-only page, the processor raises a protection fault.
Conceptually:
The exact layout differs by architecture and operating-system configuration. Some systems keep many kernel mappings out of a user process's active page tables except during controlled transitions. Others retain mappings marked inaccessible to user mode.
The invariant is more important than the layout:
User-mode execution must not gain ordinary load, store, or instruction-fetch access to kernel-only pages.
Kernel mode can deliberately access user memory while serving a system call, but that does not make user pointers trustworthy. The kernel must treat an application-supplied pointer as untrusted input and copy through guarded routines that can handle missing, inaccessible, or concurrently changing mappings.
A process address space is divided into regions with different purposes. The kernel converts those region policies into page-table permissions.
A typical native process uses patterns such as:
| Region | Typical permissions | Reason |
|---|---|---|
| Executable code | r-x | Instructions must execute but should not be rewritten |
| Read-only constants | r-- | Data may be observed but should not change |
| Global writable data | rw- | Program state changes during execution |
| Heap | rw- | Dynamic objects are read and modified |
| Thread stack | rw- | Function calls continuously update stack state |
| Guard page | --- | Any access should fail |
The exact permissions vary by program, runtime, and hardware. Some architectures cannot independently represent every combination. For example, writable memory may implicitly be readable, and execute-only mappings may not be available or used.
The general least-privilege rule is still valuable:
If every page were readable, writable, and executable, a single memory-corruption bug would have far more useful outcomes for an attacker. Separating permissions reduces what one faulty access can accomplish.
Modern processors support a page permission commonly called execute disable, NX, or XD. The names differ, but the purpose is the same: prevent instruction fetches from pages intended only for data.
A stack typically needs to hold return addresses, local variables, and temporary values. It does not normally need to contain instructions that the CPU may execute.
If corrupted control flow jumps to bytes on a non-executable stack or heap, the processor raises a protection fault rather than decoding those bytes as instructions.
Non-executable data is an important mitigation, but it is not complete protection against control-flow attacks. An attacker may try to redirect execution to instructions that already exist in executable regions. Memory protection removes one path; it does not prove that control flow is correct.
The inverse matters too. Executable code is commonly mapped without write permission. A store that targets code should fault rather than changing the running program.
Write XOR execute, written W^X, is a policy that a page should not be writable and executable at the same time.
The safe steady-state patterns are:
The risky pattern is:
An rwx page lets a memory-writing bug change bytes that the processor can immediately execute. W^X forces code generation and code execution into separate permission states.
Just-in-time compilers need to generate machine code dynamically. A common workflow is:
At no point is the page both writable and executable, which is what the pattern exists to avoid.
Some runtimes use separate writable and executable mappings of the same backing storage or platform-specific APIs. Those designs must prevent untrusted code from freely modifying the executable view.
NX and W^X are related but different. NX supplies a hardware ability to mark particular pages non-executable. W^X is a system policy that avoids mappings which are writable and executable simultaneously.
An executable file describes segments that need to be mapped when the program starts. Each loadable segment includes intended read, write, and execute flags.
The loader and kernel use that metadata to create mappings such as:
Shared libraries are divided similarly. One library can appear as several adjacent mappings because its instructions, constants, and mutable state require different permissions.
Some runtime data must be writable while the dynamic linker applies relocations and can become read-only afterward. Toolchains can arrange for those regions to be protected once initialization finishes. This is the idea behind relocation read-only, commonly called RELRO.
The executable can also indicate whether its stack requires execute permission. Ordinary programs should not request an executable stack.
These protections are meaningful only at runtime. A file's disk permissions, such as 0755, say who may read, modify, or launch the executable file. Page permissions such as r-x say what the CPU may do with the mapped memory after the program is loaded. They are different protection layers.
mmap()On Linux, mmap() can create an anonymous mapping or map a file into an address space. Its protection argument describes the requested access:
Flags can be combined:
The kernel considers the requested protection, the backing object's access mode, filesystem constraints, architecture rules, and system policy. Requesting a permission does not guarantee that the kernel will grant it.
PROT_NONE is particularly useful for reserving inaccessible address ranges and creating guard pages. The address can remain part of a known mapping while every load, store, and instruction fetch is rejected.
Memory protections operate at page granularity. If a 16-byte object shares a writable page with other objects, the OS cannot make only those 16 bytes read-only using ordinary page permissions.
This creates an alignment requirement for low-level designs: data that needs a distinct protection lifecycle should occupy page-aligned ranges that do not share pages with unrelated writable data.
mprotect()mprotect() changes the access rights of pages in an existing mapping.
Its Linux interface is:
The starting address must be aligned to a page boundary. The range covers every page touched by the requested length.
A program can initialize a page as writable and then freeze it:
After the successful mprotect(), a load can still succeed while a store should raise a protection fault.
Permission changes have a cost. The kernel must update mapping metadata and page tables, invalidate stale translations, and coordinate with other cores that may be running threads from the address space. Repeatedly toggling permissions on hot paths can therefore be expensive.
Calling mprotect() is not, by itself, a permanent boundary against arbitrary code already controlling the process. If that code can make unrestricted mapping requests, it may ask the kernel to change permitted mappings again. The protection is strongest when permission transitions are one-way by design or reinforced by an independent policy.
A guard page is an intentionally inaccessible page placed next to a usable region.
Thread stacks commonly use guard space near a growth boundary:
The guard page has no permissions at all. A stack that grows past its limit hits it and faults, instead of silently running into whatever is mapped below.
If stack growth reaches the guard page, the next access faults instead of silently entering an adjacent mapping.
Allocators and debugging tools can place guard pages around sensitive or test allocations. This helps detect overflows that cross a page boundary.
Guard pages do not detect every out-of-bounds access. A small overflow can corrupt another object located in the same writable page without touching the guard. Page-level protection has page-level precision.
Guard regions also reserve address space without necessarily consuming a physical frame. Their value comes from the absence of accessible translation, not from stored data.
Loading simulation...
Process isolation does not require all physical frames to belong to exactly one process. The kernel can map the same frame into multiple address spaces deliberately.
Permissions can differ by mapping:
One frame, two mappings, two different sets of rights. Sharing memory does not require sharing the ability to modify it.
The producer may update the shared page while the consumer can only read it. The virtual addresses do not need to match.
Once a writable shared mapping is established, individual loads and stores do not require a system call. The processor enforces the page permissions directly, which makes shared memory fast.
This also means the kernel does not validate the application-level meaning of every write. If two processes share a writable page, they need a correct protocol for data layout and synchronization.
Revoking shared access requires changing or removing the mapping and invalidating cached translations. Revocation cannot erase data that the consumer already copied into its private memory.
The kernel can initially map one physical frame read-only into multiple address spaces even when each process conceptually owns a private copy.
Reads can use the shared frame. When Process A attempts the first write, hardware raises a protection fault because its mapping is read-only.
The kernel recognizes that the page is marked for copy-on-write, allocates a private frame, copies the data, and changes Process A's mapping to writable:
After A's write, the sharing has ended for this page. B still sees the original contents.
The fault is part of the intended mechanism rather than a security violation. Page protection gives the kernel a precise point at which to create private state.
This illustrates a broader principle: a protection fault reports that the current hardware mapping rejects an access. The kernel's higher-level metadata determines whether to repair the mapping or treat the access as invalid.
Threads in the same process normally share an address space. A page mapped writable for one thread is writable for the others as well.
Page permissions are a property of the address space, not of a thread. They cannot separate these three from each other.
Page-table protection therefore does not isolate ordinary threads from one another. A bad pointer in one thread can corrupt heap objects, global variables, or another thread's stack if those pages are writable in the shared address space.
Using separate threads is a concurrency choice, not a strong memory-security boundary. Components that must not be able to read or modify each other's memory generally need separate address spaces or a more specialized isolation mechanism.
There are hardware features that can vary some access rights between threads within one address space, but ordinary read, write, and execute page permissions are shared process state.
Some processors support memory protection keys. Linux exposes this feature on supported architectures through interfaces such as pkey_mprotect().
A page-table entry can be tagged with a protection key. Each thread has a small hardware register that can disable selected kinds of data access for pages carrying that key.
Conceptually:
A page-table entry tagged with key 3 is evaluated against the thread-local key register, which can have both reads and writes for key 3 disabled.
Changing the thread-local register is much faster than rewriting page tables and invalidating TLB entries. Runtimes can use keys to make a region temporarily inaccessible while leaving its underlying mappings intact.
Protection keys are not automatically a strong boundary against arbitrary native code in the same process. If that code is allowed to change the key register, it can re-enable access. They are most useful when the runtime controls which code may manipulate keys and carefully manages every thread's state.
The feature also has limited key slots and architecture-specific semantics, so software needs a fallback when hardware support is absent.
System calls often receive pointers to application buffers:
The pointer value belongs to the calling process's address space. The kernel must verify that the range is a valid user-accessible mapping for the requested direction.
The mapping can fault while the kernel is copying. It can also be changed by another thread in the same process. Kernels therefore use specialized user-copy routines and guarded fault-recovery paths rather than trusting a pointer after one superficial range check.
Many processors add protections that make accidental kernel access to user pages harder:
These features protect the user-kernel boundary from kernel mistakes and corrupted kernel control flow. They do not stop user code from accessing its own user pages according to their normal permissions.
A device performing direct memory access, or DMA, can transfer data without having the CPU issue a load or store for every byte. Ordinary CPU page-table checks do not automatically constrain those device accesses.
An I/O memory-management unit, or IOMMU, translates and restricts device-visible addresses:
A device DMA address passes through the IOMMU mapping and permission check before reaching approved physical memory.
The kernel configures an IOMMU domain so a device can access the buffers assigned to it rather than arbitrary physical memory.
This matters for both reliability and security. A faulty device or driver should not be able to overwrite unrelated kernel data merely because the device can initiate DMA.
IOMMU protection is separate from the process page tables used for CPU instructions. Both are required when CPUs and devices can independently access memory.
Address-space layout randomization, or ASLR, changes where regions such as the executable, libraries, heap, and stack are placed.
Randomization makes useful addresses harder to predict. It can raise the difficulty of exploiting a memory-corruption bug.
ASLR does not mark memory read-only, non-executable, or inaccessible. Once an address is known, the ordinary page permissions decide whether an access is allowed.
An information disclosure can weaken randomization by revealing addresses without changing a single page permission. Conversely, a fixed-address process can still have correctly enforced read, write, and execute protections.
ASLR is therefore an exploit mitigation that complements memory protection rather than replacing it.
The MMU understands pages, permissions, address spaces, and privilege modes. It does not understand C arrays, object lifetimes, Rust ownership, Java references, or application data structures.
Suppose two arrays occupy one writable page:
Both arrays sit inside one rw- page. Page permissions cannot distinguish them, so an overrun from A into B is not a protection violation.
If a C program writes beyond the end of array A and into array B, every store can remain inside the same writable page. The hardware sees an allowed write even though the program has violated an object boundary.
Memory protection catches accesses that cross protected mapping boundaries. It does not catch:
Memory-safe languages, compiler instrumentation, allocators, runtime checks, and careful application design address finer-grained rules. OS memory protection supplies the containment boundary on which those techniques can build.
Linux exposes the mappings of process 12345 through:
A line has a form such as:
The permission field contains:
A dash means that permission is absent. Private versus shared describes update semantics; it is not a replacement for the read, write, and execute bits.
Inspect the current shell's stack mapping:
A typical result contains rw-p, not rwxp:
Inspect the segments requested by an executable:
The LOAD entries show segment flags. The GNU_STACK entry indicates whether the executable requests an executable stack. A normal stack request contains read and write flags without execute.
/proc/<pid>/smaps provides more detail for each mapping, while pmap offers a summarized view. Tool access can be restricted for processes owned by another identity.
The following Linux program allocates one writable page, writes to it, changes it to read-only, and then deliberately attempts another write:
Compile and run it:
The two reads should print successfully. The final store conflicts with the read-only page mapping and should cause the process to receive a protection-related signal, commonly reported by the shell as a segmentation fault.
The C assignment itself is valid syntax. The failure comes from the runtime mapping enforced by the processor.
To let the program finish normally, remove the deliberate store or call:
before that store. This demonstrates that the page's role can change while its virtual address and contents remain the same.
Consider a backend process that loads configuration, initializes a routing table, accepts requests, and uses a just-in-time expression engine.
A sensible memory layout can enforce different roles:
Making the routing snapshot read-only after construction turns an accidental later write into an immediate fault. Without that transition, corruption may remain silent until a request takes an incorrect route.
Keeping stacks and the ordinary heap non-executable prevents injected data there from being fetched as instructions. Keeping executable code non-writable prevents an ordinary data-write bug from modifying it directly.
The JIT has a legitimate need to create code, but it does not require writable and executable access simultaneously. Its permission transition should be narrow in range and time.
Separate worker processes provide a stronger boundary than worker threads. If a parser handling untrusted input corrupts memory in one process, page-table isolation can keep it from directly overwriting another worker's heap. Threads in the same process would share that heap and its permissions.
None of these controls proves the service is free of memory bugs. They reduce the reachable consequences and turn some invalid operations into detectable faults.
When a process crashes on a memory access, start with four facts:
Common patterns include:
A debugger can report the faulting instruction and register state. /proc/<pid>/maps or a captured core file can help classify the address by region and permission.
Do not diagnose solely from the phrase “segmentation fault.” That user-visible result can arise from an unmapped address, a write to read-only memory, an instruction fetch from non-executable data, or other invalid access.
Also distinguish an expected protection mechanism from an application failure. Copy-on-write deliberately uses a write fault that the kernel resolves transparently. A guard-page access is deliberately made fatal to expose an invalid boundary crossing.
Memory protection combines page tables, hardware privilege modes, and kernel-controlled mapping policy. Every instruction fetch, load, and store is checked against the active address space and page permissions.
Separate address spaces isolate processes, while user/supervisor permissions protect kernel memory. Shared frames are available only through deliberately created mappings whose permissions can differ by process.
Read, write, execute, and no-access mappings give code, data, constants, stacks, and guard regions different authority. NX blocks execution from data pages, while W^X avoids simultaneous writable and executable access.
mmap() creates mappings and mprotect() changes page-level rights. Guard pages, copy-on-write, protected user-copy paths, protection keys, and IOMMU mappings all apply the same idea at different boundaries.
Page protection is not object-level memory safety. It detects access that violates a mapping boundary, but bugs wholly inside an allowed writable page require language, compiler, runtime, and application-level defenses.
5 quizzes