Linux Virtual Address Space Layout
On x86-64, every process sees a 64-bit-wide virtual address space that is, in practice, only partly usable: the hardware implements either 48 or 57 address bits, the top bits are sign-extended, and Linux splits the resulting canonical range into two disjoint halves separated by an enormous non-canonical hole. The lower half is user space — private to each process, carved into the familiar text/data/heap/stack/mmap regions and re-randomized at every
exec()by Address Space Layout Randomization (ASLR). The upper half is kernel space — identical in every process, the same kernel mapped into the top of every address space so a system call never needs to switch page tables (cf. the Kernel Address Space and the Direct Map). The default user window is 47 bits (128 TiB) even when the hardware supports 57; the extra reach of Five-Level Paging is opt-in to avoid breaking software that stashes data in the high bits of pointers (kernel mm.rst, v6.12; 5level-paging.rst, v6.12).
This note describes the x86-64 layout specifically; the abstract idea of a hardware page-table walk translating these virtual addresses lives in Memory Management Unit, and a small-architecture worked example is Sv32 Virtual Memory. The kernel half is covered in depth by its sibling Kernel Address Space and the Direct Map; this note focuses on the per-process user half and the split itself.
Uncertain
Addresses below are pinned to the v6.12 LTS
Documentation/arch/x86/x86_64/mm.rsttable, cross-checked against v6.18 LTS. The kernel half is byte-for-byte identical between the two. The user-space top changed in 6.18: 6.12 lists user space as the flat0x0–0x00007fffffffffff(128 TiB); 6.18 carves a 4 KiB guard hole at the very top (0x00007ffffffff000–0x00007fffffffffff), making the usable user range0x0–0x00007fffffffefff(~128 TiB), and adds notes that Linear Address Masking (LAM) relaxes the canonicality check in the non-canonical hole. Both facts are called out in place below. Reason: the doc table was revised between the two LTS releases. To resolve: the v6.18mm.rstis the authority for 6.18 systems. uncertain
Mental Model — Two Halves and a Hole
Think of the 64-bit address as a number line that the hardware refuses to use in the middle. Only addresses whose top bits are a sign-extension of the highest implemented bit are canonical — legal to dereference. With 48 implemented bits (4-level paging), bit 47 is the sign bit: it must be copied into bits 48–63. That makes exactly two legal islands — a low island where the top 17 bits are all 0 (user space) and a high island where they are all 1 (kernel space) — with a vast non-canonical hole between them that no instruction may touch. The processor faults (#GP) on any non-canonical access.
flowchart TB subgraph TOP["64-bit address space (4-level paging, 48 implemented bits)"] direction TB U["USER half — canonical low<br/>0x0 .. 0x00007fffffffffff<br/>128 TiB, PRIVATE per process<br/>bits 47..63 = 0"] H["NON-CANONICAL HOLE<br/>0x0000800000000000 .. 0xffff7fffffffffff<br/>~16 million TiB, UNUSABLE<br/>#GP on access"] K["KERNEL half — canonical high<br/>0xffff800000000000 .. 0xffffffffffffffff<br/>128 TiB, SHARED across all processes<br/>bits 47..63 = 1"] U --> H --> K end
The two-halves-and-a-hole model on x86-64 with 4-level paging. What it shows: the address space is not a contiguous 16-exabyte expanse; it is two 128-TiB canonical islands (user low, kernel high) separated by a colossal forbidden gap. The insight: the split is a free consequence of the hardware’s sign-extension rule — the kernel did not have to choose a split point, the canonical-address requirement handed it one. The user half is re-created and re-randomized for every process; the kernel half is one set of page-table entries shared (mapped at the same virtual addresses) into the top of every process’s page tables.
Why the Hole Exists — Canonical Addresses
The x86-64 architecture defines a 64-bit virtual address but lets an implementation support fewer bits. As the v6.12 doc states: “Architecture defines a 64-bit virtual address. Implementations can support less. Currently supported are 48- and 57-bit virtual addresses. Bits 63 through to the most-significant implemented bit are sign extended. This causes [a] hole between user space and kernel addresses if you interpret them as unsigned.” (mm.rst, v6.12).
Concretely, with 48 implemented bits the canonical addresses are those where bits 48–63 all equal bit 47. The kernel encodes this rule directly:
/* arch/x86/include/asm/page.h, v6.12 */
static __always_inline u64 __canonical_address(u64 vaddr, u8 vaddr_bits)
{
return ((s64)vaddr << (64 - vaddr_bits)) >> (64 - vaddr_bits);
}
static __always_inline u64 __is_canonical_address(u64 vaddr, u8 vaddr_bits)
{
return __canonical_address(vaddr, vaddr_bits) == vaddr;
}The trick is the arithmetic (sign-extending) right shift. Casting to s64 and shifting left by 64 - vaddr_bits (16 for 48-bit) puts bit 47 into the sign position; the arithmetic right shift back down then replicates that sign bit through the top 16 bits. If the result equals the original, the address was already correctly sign-extended — i.e., canonical. This is exactly the hardware’s rule expressed in C, used wherever the kernel must validate a userspace pointer that the CPU has not yet checked.
The practical consequence: the usable low half ends at 0x00007fffffffffff and the usable high half begins at 0xffff800000000000. Everything between is the non-canonical hole. Because it is unusable, it costs nothing — it is the slack that lets the address space grow to 57 bits later without renumbering anything.
Uncertain
The v6.18 table annotates the non-canonical hole with: “LAM relaxes canonicallity check allowing to create aliases for userspace memory here” (and
LAM_SUPfor kernel memory). Linear Address Masking (LAM) lets the CPU ignore some high address bits so software can store metadata (tags) there without the address being treated as non-canonical. This row does not appear in the v6.12 table. Verify the exact LAM-enabled behavior and which bits are masked against the v6.18mm.rstand the LAM-specific kernel docs before relying on it. Reason: feature note added between LTS releases. uncertain
The User Half — Region by Region
A freshly exec()-ed process has its user half populated by the loader and the kernel with a conventional set of Virtual Memory Areas (VMAs — the contiguous, uniform-permission segments the mm_struct tracks). From low to high addresses:
- Program text (
.text) — the executable’s code, mapped read-only/executable from the ELF file. For a PIE (Position-Independent Executable, the default for most modern distros) its base is randomized by ASLR; for a classic non-PIE binary it loads at a fixed link-time address (historically near0x400000). - Initialized and uninitialized data (
.data,.bss) — read-write, mapped just above the text. - The heap (
brkregion) — grows upward from just past the program’s data via thebrk/sbrksystem calls (mmap brk and Address Space System Calls). The C library’smallocuses this for small allocations and the mmap region for large ones. - The mmap region — where shared libraries, anonymous
mallocarenas, and explicitmmap()mappings land. On modern Linux this region uses the top-down (flexible) layout: the mmap base sits high in the address space, just below the stack, and successive mappings grow downward toward the heap. - The stack — the main thread’s stack, placed near the top of the user window and growing downward. The kernel leaves a guard gap below it so the heap/mmap region and the stack cannot silently collide.
- The vDSO and vvar pages — a small kernel-provided shared object mapped near the stack, letting
gettimeofday()and friends run without a real syscall.
The classic ASCII intuition (low at bottom, high at top):
0x00007fffffffffff +---------------------------+ high user addresses
| [stack] (grows DOWN) |
| | |
| v |
| ~128 MB+ gap + ASLR |
| ^ |
| | |
| [mmap region] (grows DOWN)| <- libraries, large malloc, anon mmap
| |
| ^ |
| | |
| [heap / brk] (grows UP) |
| [.bss] [.data] [.text] | <- the program image (ASLR base if PIE)
0x0000000000000000 +---------------------------+ (page 0 unmapped; NULL deref faults)
The lowest pages are deliberately left unmapped (and mmap_min_addr forbids userspace from mapping them) so that a NULL-pointer dereference faults instead of reading valid memory.
The mmap base and the ~128 MB gap
Where the mmap region starts is computed in arch/x86/mm/mmap.c. The default (non-legacy) layout is top-down, anchored a controlled distance below the top of the user window:
/* arch/x86/mm/mmap.c, v6.12 (abridged) */
static unsigned long mmap_base(unsigned long rnd, unsigned long task_size,
struct rlimit *rlim_stack)
{
unsigned long gap = rlim_stack->rlim_cur; /* the stack rlimit */
unsigned long pad = stack_maxrandom_size(task_size) + stack_guard_gap;
unsigned long gap_min, gap_max;
if (gap + pad > gap)
gap += pad;
gap_min = SIZE_128M; /* leave at least ~128 MB below the stack */
gap_max = (task_size / 6) * 5; /* but not more than 5/6 of the space */
if (gap < gap_min)
gap = gap_min;
else if (gap > gap_max)
gap = gap_max;
return PAGE_ALIGN(task_size - gap - rnd); /* mmap_base = top - gap - random */
}Reading it line by line: task_size is the top of the user window (DEFAULT_MAP_WINDOW, see below). The gap reserved for the stack starts as the stack’s soft RLIMIT_STACK (rlim_cur), padded by the stack’s own randomization range and the guard gap. That gap is then clamped to at least SIZE_128M (128 MiB) and at most five-sixths of the address space. Finally mmap_base = task_size − gap − rnd, where rnd is the per-process ASLR offset. The (gap + pad > gap) test is an overflow guard for RLIM_INFINITY. The net effect: the mmap region begins ~128 MiB (or one stack-rlimit’s worth, whichever is larger) below the very top, plus a random slide — leaving room for the downward-growing stack above it and the upward-growing heap far below.
If sysctl vm.legacy_va_layout is set (or the process requests ADDR_COMPAT_LAYOUT), the kernel instead uses the bottom-up legacy layout (mmap_legacy_base), where the mmap region grows upward from a low base — the layout used before flexible mmap was introduced. The choice is recorded as the MMF_TOPDOWN flag on the mm_struct.
Address Space Layout Randomization (ASLR)
ASLR makes the positions of these regions unpredictable from one execution to the next, so an attacker cannot hardcode the address of a function or a buffer. It is controlled by the kernel.randomize_va_space sysctl (kernel.rst, v6.12):
0— randomization off (also the effect of thenorandmapsboot parameter).1— randomize the mmap base, stack, and vDSO; shared libraries load at random addresses, and a PIE binary’s code base is randomized. This is the default whenCONFIG_COMPAT_BRKis set.2— additionally randomize the heap (brk). This is the default on a normal kernel (CONFIG_COMPAT_BRKdisabled). Thebrkrandomization is separated out only because a handful of ancient binaries assumedbrkbegan immediately after.bss.
The per-region random offsets come from arch_mmap_rnd():
/* arch/x86/mm/mmap.c, v6.12 */
unsigned long arch_mmap_rnd(void)
{
return arch_rnd(mmap_is_ia32() ? mmap32_rnd_bits : mmap64_rnd_bits);
}
static unsigned long arch_rnd(unsigned int rndbits)
{
if (!(current->flags & PF_RANDOMIZE))
return 0;
return (get_random_long() & ((1UL << rndbits) - 1)) << PAGE_SHIFT;
}mmap64_rnd_bits (tunable via vm.mmap_rnd_bits) controls how many bits of entropy the mmap base gets; the random value is page-shifted so it is always page-aligned. The stack gets its own randomization (stack_maxrandom_size), and the heap base is randomized separately when randomize_va_space == 2. ASLR is only applied when the task carries the PF_RANDOMIZE flag — set unless disabled by personality or boot flag.
The strength of ASLR is bounded by the number of entropy bits, which is in turn bounded by the size of the user window. A 47-bit window with, say, 28 bits of mmap entropy gives a 256-million-slot range — defeatable by an information leak, which is why ASLR is one layer in a stack (alongside NX, stack canaries, and kernel KASLR), not a standalone defense.
The 47-bit Default vs 57-bit with LA57
Original x86-64 used 4-level page tables (PGD → PUD → PMD → PTE), giving 48 implemented bits — 256 TiB of total canonical space, split 128 TiB user / 128 TiB kernel. Newer CPUs add LA57 (a CPUID feature flag for 57-bit linear addresses) enabling 5-level paging (an extra P4D level above the PUD; see Five-Level Paging), which lifts the limit to 128 PiB of virtual space and 4 PiB of physical (5level-paging.rst, v6.12).
The crucial Linux design choice: even on LA57 hardware, the default user window stays at 47 bits (128 TiB). The kernel encodes this with two distinct constants in arch/x86/include/asm/page_64_types.h:
/* arch/x86/include/asm/page_64_types.h, v6.12 */
#ifdef CONFIG_X86_5LEVEL
#define __VIRTUAL_MASK_SHIFT (pgtable_l5_enabled() ? 56 : 47)
#else
#define __VIRTUAL_MASK_SHIFT 47
#endif
#define TASK_SIZE_MAX task_size_max()
#define DEFAULT_MAP_WINDOW ((1UL << 47) - PAGE_SIZE)TASK_SIZE_MAX is the absolute ceiling (47-bit on 4-level hardware, 56-bit on LA57 — note the value is 2^56, one bit below the 57-bit hardware limit, because the very top page must stay unmapped for SYSRET correctness). But DEFAULT_MAP_WINDOW — where mmap() looks for free space by default — is always 2^47 − PAGE_SIZE, regardless of LA57. A process gets addresses above 47 bits only if it explicitly passes an mmap hint above the 47-bit boundary:
“To mitigate this, we are not going to allocate virtual address space above 47-bit by default. But userspace can ask for allocation from full address space by specifying hint address (with or without MAP_FIXED) above 47-bits. … Specifying high hint address on older kernel or on machine without 5-level paging support is safe. The hint will be ignored and kernel will fall back to allocation from 47-bit address space.” (5level-paging.rst, v6.12).
Why? Some software (notably certain JIT compilers and tagged-pointer schemes) stuffs bookkeeping bits into the unused top bits of a 47-bit pointer. If the kernel handed out 57-bit addresses by default, those bits would suddenly be significant and the pointers would point at the wrong memory — crashes. The opt-in-by-hint design means a legacy program never sees a wide address it cannot handle, while a memory allocator that wants the larger space can request it with one mmap hint and have all subsequent large allocations come from the full range.
task_size_max() selects between the two at runtime via an alternative-instruction patch keyed on the LA57 CPU feature:
/* arch/x86/include/asm/page_64.h, v6.12 */
static __always_inline unsigned long task_size_max(void)
{
unsigned long ret;
alternative_io("movq %[small],%0", "movq %[large],%0",
X86_FEATURE_LA57,
"=r" (ret),
[small] "i" ((1ul << 47) - PAGE_SIZE),
[large] "i" ((1ul << 56) - PAGE_SIZE));
return ret;
}On a 4-level CPU the small (47-bit) value is patched in; on an LA57 CPU the large (56-bit) value. The accompanying comment is candid about why the top page is excluded: on Intel, a SYSCALL at the highest canonical address returns to a non-canonical address and SYSRET “explodes dangerously”; on some AMD Ryzen parts the CPU speculates off the end of canonical space and “bad things happen.” Reserving the top page sidesteps both.
The hint mechanism also has a subtle correctness rule, enforced by mmap_address_hint_valid(): a non-MAP_FIXED mapping is not allowed to straddle the 47-bit boundary, because an app that cannot handle wide addresses must never get a mapping that begins low but crosses into high addresses (arch/x86/mm/mmap.c, v6.12).
How Every Process Shares the Kernel Half
The kernel half (0xffff800000000000 upward on 4-level) is identical in every process: the same physical kernel, the direct map, vmalloc, vmemmap, etc., mapped at the same virtual addresses in every address space. This is why a system call or interrupt can run in the context of whatever process happened to be executing — the kernel’s code and data are already mapped, no page-table switch (and no expensive CR3 reload / TLB flush) is needed on the user→kernel transition.
Mechanically, each process’s top-level page table (the PGD) has its kernel-half entries copied from a master kernel PGD (init_mm’s page tables) when the mm_struct is created; the user-half entries are private. The constant PGD_KERNEL_START marks the slot where the shared kernel entries begin:
/* arch/x86/include/asm/pgtable_64_types.h, v6.12 */
#define PGD_KERNEL_START ((PAGE_SIZE / 2) / sizeof(pgd_t)) /* slot 256 of 512 */The bottom 256 PGD slots are user-private; the top 256 are the shared kernel map. Because they point at the same lower-level page tables, a change to a kernel mapping (e.g., a new vmalloc allocation) is visible to all processes without per-process fixups — though vmalloc’s lazy PGD synchronization is itself a known source of subtlety (see vmalloc and Virtually Contiguous Memory).
Uncertain
Kernel Page Table Isolation (KPTI / PTI) complicates the “kernel is always mapped” story: as a Meltdown mitigation, PTI gives userspace a shadow PGD in which almost none of the kernel half is mapped, so a user→kernel transition does switch
CR3. TheLDT remap for PTIrow (0xffff880000000000, 0.5 TB) in the memory map exists precisely for this. The detailed PTI mechanism (when it is active, the entry-trampoline) is out of scope here and belongs with Kernel Address Space and the Direct Map or a dedicated PTI note. Verify PTI’s default-on status for the specific CPU/threat model against the v6.12 PTI documentation. uncertain
Inspecting a Live Layout
The layout is directly observable. /proc/<pid>/maps lists every VMA with its address range, permissions, and backing:
$ cat /proc/self/maps
55e3c2a00000-55e3c2a02000 r--p ... /usr/bin/cat # PIE text, ASLR'd base
...
7f9c4e000000-7f9c4e028000 r-xp ... /lib/.../libc.so.6 # shared lib in mmap region
7ffd3b2c0000-7ffd3b2e1000 rw-p ... [stack] # main stack, near top
7ffd3b3aa000-7ffd3b3ae000 r--p ... [vvar]
7ffd3b3ae000-7ffd3b3b0000 r-xp ... [vdso]
ffffffffff600000-ffffffffff601000 --xp ... [vsyscall] # the legacy vsyscall pageRun cat /proc/self/maps twice (with ASLR on): the base addresses change each time. The [vsyscall] line at 0xffffffffff600000 is the only kernel-half address visible to userspace — the legacy vsyscall ABI page (the 4 KB entry at -10 MB in the memory map), a fixed gate page kept for ancient binaries.
To confirm whether a box runs 5-level paging: grep -o 'la57' /proc/cpuinfo (CPU capability) and check CONFIG_X86_5LEVEL in the kernel config; the address-width can be read at boot from dmesg | grep -i 'level paging'.
Failure Modes and Misconceptions
- “The address space is 16 exabytes.” No — it is two ~128 TiB canonical islands (4-level) or ~128 PiB islands (5-level) separated by an unusable hole. Dereferencing a non-canonical address is a hardware
#GP, surfacing asSIGSEGV. - “Enabling 5-level paging gives my program a bigger address space automatically.” No —
DEFAULT_MAP_WINDOWstays at 47 bits; you must pass an mmap hint above the boundary to get wide addresses. A program that never does sees the same 128 TiB it always did. - “ASLR randomizes everything.” It randomizes the mmap base, stack, vDSO, PIE text, and (at level 2) the heap — but a non-PIE binary’s text stays fixed, and the kernel half is randomized by a separate mechanism (KASLR, see Kernel Address Space and the Direct Map). Mixing the two up is a common error.
- Stack/heap collision. Because the stack grows down and the heap grows up, an unbounded stack (no
RLIMIT_STACK) or runaway recursion can in principle approach the mmap region; thestack_guard_gapand the ~128 MB minimum gap exist to make this fault cleanly rather than corrupt the heap. - High-bit pointer tagging breaks under LA57 or wide mmap. The exact failure the 47-bit default was designed to prevent: code that stored a tag in bits 48–56 of a pointer will, on a wide mapping, find those bits are now address bits.
See Also
- Kernel Address Space and the Direct Map — the upper half this note’s user half is paired with: the direct map, vmalloc, vmemmap, fixmap, kernel text, and KASLR.
- Five-Level Paging — the LA57 extra page-table level that turns the 47-bit window into a 56-bit one.
- Virtual Memory Areas — the VMA data structure that records each user region (text/heap/stack/mmap) with its permissions and backing.
- The mm_struct and Process Address Space — the per-process descriptor that owns the user half and points at the page tables.
- mmap brk and Address Space System Calls —
mmap,brk,mremap— the syscalls that shape the user layout at runtime. - Linux Page Table Hierarchy — PGD/P4D/PUD/PMD/PTE, the translation machinery beneath these addresses.
- Memory Management Unit — the hardware MMU/TLB that walks these page tables (architecture background).
- Sv32 Virtual Memory — a compact two-level (RISC-V) address-translation example for contrast.
- MOC: Linux Memory Management MOC — §1 Virtual Memory and the Process Address Space.