Kernel Address Space and the Direct Map
The kernel occupies the upper, canonical-high half of every x86-64 address space — the same physical kernel mapped at the same virtual addresses in every process, so a syscall or interrupt never has to switch page tables. That upper half is not one region but a carefully laid-out series: a guard hole, the direct (linear) map that mirrors all of physical RAM 1:1 at a fixed offset (
page_offset_base, the anchor of the__va/__paconversions every driver relies on), thevmalloc/ioremaparea for virtually-contiguous allocations, thevmemmapregion that holds thestruct pagearray, the KASAN shadow, then a tail of small fixed regions —cpu_entry_area, ESP-fixup stacks, the EFI window, the kernel text mapping, the module area, and the fixmap. On a randomized kernel, the three big regions (direct map, vmalloc, vmemmap) have their bases slid by KASLR at boot (kernel mm.rst, v6.12; arch/x86/mm/kaslr.c, v6.12).
This note is the kernel-half companion to Linux Virtual Address Space Layout (which covers the user half and the user/kernel split). The hardware translation machinery beneath these addresses is Memory Management Unit; the vmalloc allocator and the vmemmap/physical-memory model each have their own deep-dive siblings (vmalloc and Virtually Contiguous Memory, The Physical Memory Model SPARSEMEM and vmemmap).
Uncertain
All addresses are pinned to the v6.12 LTS
Documentation/arch/x86/x86_64/mm.rsttable and cross-checked against v6.18 LTS. The kernel half is byte-for-byte identical between 6.12 and 6.18 (only the user-space top changed; see Linux Virtual Address Space Layout). The 6.18 table additionally annotates the non-canonical hole with LAM_SUP (Linear Address Masking for supervisor addresses); that does not change any kernel-region base. Reason: doc table revised between LTS releases, but the kernel-half rows are unchanged. To resolve: the v6.18mm.rstis authoritative for 6.18. uncertain
Mental Model — The Kernel Half Is a Map of Maps
The single most important idea: the direct map is not where the kernel’s data structures necessarily live — it is a window through which the kernel can name any physical page by a simple offset. Physical address P is always reachable at virtual address P + PAGE_OFFSET. Everything else in the kernel half (vmalloc, vmemmap, modules) is a separate mapping serving a need the direct map cannot.
flowchart LR subgraph PHYS["Physical RAM (one byte = one address)"] P0["phys 0x0"] PN["phys max"] end subgraph KVA["Kernel virtual half (4-level, top 128 TiB)"] direction TB DM["direct map (page_offset_base)<br/>64 TiB window, ALL of RAM 1:1<br/>__va(P) = P + PAGE_OFFSET"] VM["vmalloc / ioremap (vmalloc_base)<br/>32 TiB, virtually contiguous"] VME["vmemmap (vmemmap_base)<br/>1 TiB, the struct page array"] TXT["kernel text @ -2 GiB<br/>+ modules @ -1.5 GiB"] DM --> VM --> VME --> TXT end PHYS -->|"1:1, fixed offset"| DM VME -.->|"one struct page per pfn"| PHYS
The kernel half as a set of distinct mappings over the same physical RAM. What it shows: the direct map is a contiguous 1:1 window onto all physical memory; vmalloc, vmemmap, and the text/module regions are independent mappings with their own purposes. The insight: __va/__pa are pure arithmetic only for the direct map — a vmalloc or ioremap address is not phys + PAGE_OFFSET and must be translated by walking page tables. Confusing the two is a classic kernel bug.
The Direct (Linear) Map — page_offset_base
The direct map (also “linear map,” “lowmem map,” or “physmap”) maps all of physical memory into kernel virtual space at a single constant offset, PAGE_OFFSET. As the v6.12 doc puts it, the row at 0xffff888000000000 (-119.5 TB, 64 TB on 4-level) is the “direct mapping of all physical memory (page_offset_base)”, and: “The direct mapping covers all memory in the system up to the highest memory address (this means in some cases it can also include PCI memory holes).” (mm.rst, v6.12).
This is the kernel’s everyday way of touching RAM: when the page allocator hands out a physical page, the kernel addresses its contents through the direct map. The two conversion macros are the foundation of almost all kernel memory code:
/* arch/x86/include/asm/page.h, v6.12 */
#ifndef __va
#define __va(x) ((void *)((unsigned long)(x) + PAGE_OFFSET))
#endif
/* arch/x86/include/asm/page_types.h, v6.12 */
#define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET)
/* arch/x86/include/asm/page_64_types.h, v6.12 */
#define __PAGE_OFFSET_BASE_L4 _AC(0xffff888000000000, UL) /* 4-level base */
#define __PAGE_OFFSET_BASE_L5 _AC(0xff11000000000000, UL) /* 5-level base */
#ifdef CONFIG_DYNAMIC_MEMORY_LAYOUT
#define __PAGE_OFFSET page_offset_base /* runtime variable (KASLR) */
#else
#define __PAGE_OFFSET __PAGE_OFFSET_BASE_L4
#endifSo __va(P) is just P + PAGE_OFFSET — given a physical address, return the kernel virtual address that aliases it. PAGE_OFFSET is __PAGE_OFFSET, which on a CONFIG_DYNAMIC_MEMORY_LAYOUT kernel (the common case, needed for KASLR) is the runtime variable page_offset_base chosen at boot, not a compile-time constant. On 4-level paging its un-randomized value is 0xffff888000000000; on 5-level, 0xff11000000000000. The peculiar base — the most-negative address plus PGDIR_SIZE × 17 (PGD slot 273) — leaves room for the LDT-remap PTI slot and 16 slots a hypervisor (Xen) wants, per the comment in page_64_types.h.
The reverse, __pa(x), must handle the fact that the kernel image itself is not in the direct map (it lives in its own text mapping at -2 GiB, below). So it cannot be a single subtraction:
/* arch/x86/include/asm/page_64.h, v6.12 */
static __always_inline unsigned long __phys_addr_nodebug(unsigned long x)
{
unsigned long y = x - __START_KERNEL_map;
/* use the carry flag to determine if x was < __START_KERNEL_map */
x = y + ((x > y) ? phys_base : (__START_KERNEL_map - PAGE_OFFSET));
return x;
}Reading it: __START_KERNEL_map is 0xffffffff80000000 (the kernel text base, -2 GiB). If x is above that (a kernel-text address), the physical address is x − __START_KERNEL_map + phys_base. Otherwise (x is a direct-map address) the physical address is x − PAGE_OFFSET — which the code computes as y + (__START_KERNEL_map − PAGE_OFFSET), i.e. x − __START_KERNEL_map + __START_KERNEL_map − PAGE_OFFSET = x − PAGE_OFFSET. The branchless (x > y) test detects which region x fell in by whether the subtraction wrapped. The takeaway: __pa/__va are valid only for direct-map and kernel-text addresses — feed them a vmalloc or ioremap pointer and you get garbage, which is why those regions get their own walk-the-page-tables translators (vmalloc_to_page, etc.).
The direct map also lets the kernel translate between a physical page frame and its descriptor: virt_to_page(kaddr) is pfn_to_page(__pa(kaddr) >> PAGE_SHIFT) (arch/x86/include/asm/page.h, v6.12) — direct-map virtual address → physical → page-frame number → struct page in the vmemmap array.
Uncertain
The direct map is normally a coarse mapping (large 2 MiB / 1 GiB pages where alignment allows) for TLB efficiency, but the kernel can split it down to 4 KiB pages and mark individual pages not-present (e.g., for memory hot-unplug,
set_memory_*permission changes, or page-poisoning hardening). Whether and when the direct map is fully 1 GiB-mapped vs split depends on configuration and runtime events. Verify the exact direct-map page-size policy and any 6.12/6.18-era hardening defaults against the kernel’sarch/x86/mm/init.c/pat/set_memory.cand the relevant docs before stating specifics. Reason: not directly addressed by the mm.rst table; implementation detail. uncertain
The vmalloc / ioremap Area — vmalloc_base
Below the direct map sits the vmalloc/ioremap region: 0xffffc90000000000 (-55 TB, 32 TB on 4-level), labelled “vmalloc/ioremap space (vmalloc_base)” in the v6.12 table. Where the direct map gives physically-contiguous memory at a fixed offset, vmalloc() allocates memory that is virtually contiguous but physically scattered — it grabs individual physical pages from the page allocator and stitches them together with fresh page-table entries in this region. That makes large allocations possible even when physical memory is fragmented, at the cost of needing real page-table walks to translate (no __va shortcut) and consuming TLB entries.
ioremap() shares the region but maps device MMIO (memory-mapped I/O) physical ranges — registers on a PCI device, framebuffers — into kernel virtual space with appropriate cache attributes. The size constants differ sharply by paging level:
/* arch/x86/include/asm/pgtable_64_types.h, v6.12 */
#define __VMALLOC_BASE_L4 0xffffc90000000000UL
#define __VMALLOC_BASE_L5 0xffa0000000000000UL
#define VMALLOC_SIZE_TB_L4 32UL /* 32 TiB on 4-level */
#define VMALLOC_SIZE_TB_L5 12800UL /* 12.5 PiB on 5-level */
#ifdef CONFIG_DYNAMIC_MEMORY_LAYOUT
# define VMALLOC_START vmalloc_base
# define VMALLOC_SIZE_TB (pgtable_l5_enabled() ? VMALLOC_SIZE_TB_L5 : VMALLOC_SIZE_TB_L4)
#endifVMALLOC_START is the runtime vmalloc_base (KASLR-randomized) and VMALLOC_END is derived from it plus the size. The full allocator mechanics — the red-black tree of free areas, lazy TLB flushing of freed ranges, the guard pages between allocations — live in vmalloc and Virtually Contiguous Memory; this note only places the region in the map and contrasts it with the direct map.
The vmemmap Region — vmemmap_base
Higher still: 0xffffea0000000000 (-22 TB, 1 TB on 4-level), “virtual memory map (vmemmap_base)”. This region holds the struct page array — one descriptor per physical page frame, the kernel’s model of physical RAM. On the SPARSEMEM_VMEMMAP model (the default on x86-64), struct page for page-frame-number pfn lives at vmemmap_base + pfn * sizeof(struct page), so pfn_to_page()/page_to_pfn() are simple array indexing — a constant-time conversion with no lookup. This is why memory-hotplug and sparse physical layouts are cheap: the array is virtually contiguous and dense even when physical RAM has holes; only the populated stretches get backing pages.
/* arch/x86/include/asm/pgtable_64_types.h, v6.12 */
#define __VMEMMAP_BASE_L4 0xffffea0000000000UL
#define __VMEMMAP_BASE_L5 0xffd4000000000000UL
#ifdef CONFIG_DYNAMIC_MEMORY_LAYOUT
# define VMEMMAP_START vmemmap_base
#endifKASLR sizes this region precisely to the system’s physical memory. In kaslr.c, after fixing the direct-map size, the kernel computes the vmemmap size as (number of direct-map pages) × sizeof(struct page), rounded up to a terabyte:
/* arch/x86/mm/kaslr.c, v6.12 (abridged) */
vmemmap_size = (kaslr_regions[0].size_tb << (TB_SHIFT - PAGE_SHIFT)) *
sizeof(struct page);
kaslr_regions[2].size_tb = DIV_ROUND_UP(vmemmap_size, 1UL << TB_SHIFT);The full SPARSEMEM/vmemmap story (sections, sub-section hotplug, the mem_map) is in The Physical Memory Model SPARSEMEM and vmemmap; here it suffices that this 1-TiB region (on 4-level) is where every struct page is addressable.
The Tail Regions — Text, Modules, Fixmap, and the Rest
The very top of the address space (the last few terabytes/gigabytes) holds a fixed sequence of small, non-randomized regions. From the v6.12 table, the part that is “Identical layout to the 56-bit one” — i.e., the same on 4-level and 5-level — runs:
| Start (4-level) | Offset | Size | Region | Purpose |
|---|---|---|---|---|
0xfffffc0000000000 | -4 TB | 2 TB | unused hole | also vaddr_end for KASLR |
0xfffffe0000000000 | -2 TB | 0.5 TB | cpu_entry_area | per-CPU entry trampolines/stacks (needed under PTI) |
0xffffff0000000000 | -1 TB | 0.5 TB | %esp fixup stacks | 16-bit stack-segment fixup for IRET edge cases |
0xffffffef00000000 | -68 GB | 64 GB | EFI region | EFI runtime services (mapped in efi_pgd) |
0xffffffff80000000 | -2 GB | 512 MB | kernel text | the kernel image, mapped to physical 0 |
0xffffffffa0000000 | -1.5 GB | 1520 MB | module mapping | where loadable .ko modules land |
FIXADDR_START | ~-11 MB | ~0.5 MB | fixmap | compile-time-fixed virtual slots |
0xffffffffff600000 | -10 MB | 4 KB | legacy vsyscall | the one kernel page userspace can see |
A few of these warrant explanation:
- Kernel text mapping (
__START_KERNEL_map = 0xffffffff80000000, -2 GiB). The kernel image (code + data) is mapped here, separately from the direct map. This 2-GiB-from-the-top placement is what lets the compiler use the small/kernel code model (-mcmodel=kernel), where all symbol references fit in a signed 32-bit displacement.KERNEL_IMAGE_SIZEcaps it at 1 GiB when KASLR is on (CONFIG_RANDOMIZE_BASE), leaving the other 1 GiB for modules; without KASLR it shrinks to 512 MiB, giving modules 1.5 GiB (page_64_types.h, v6.12). - Module mapping space.
MODULES_VADDR = __START_KERNEL_map + KERNEL_IMAGE_SIZE, ending atMODULES_END = 0xffffffffff000000— loadable modules must live within ±2 GiB of the kernel text so module code can call kernel symbols with 32-bit-relative branches. - Fixmap. A set of compile-time-fixed virtual addresses (
FIXADDR_STARTup to the top), each a permanent virtual slot the kernel can bind to a chosen physical page very early in boot (before the normal allocators exist) or for special purposes (e.g., the APIC, earlyioremap). The address of each slot is a constant, so it is usable before page tables are fully built. cpu_entry_area. Per-CPU read-only structures (the entry trampoline code, the IST/entry stacks) that must remain mapped even in the PTI user shadow page tables, so the CPU has somewhere to land on a user→kernel transition. Its base,CPU_ENTRY_AREA_BASE, is also thevaddr_endceiling for KASLR randomization (kaslr.c, v6.12).- Legacy vsyscall page (
0xffffffffff600000). A single fixed 4-KiB gate page (the[vsyscall]line in/proc/self/maps) kept for ancient statically-linked binaries that hardcoded the address ofgettimeofday. It is the only kernel-half address userspace can name.
The STACKLEAK_POISON value (0xffffffffffff4111) lives in the very last 2 MiB hole; in v6.18 this is renamed KSTACK_ERASE_POISON (a stack-erasure hardening marker — purely a rename, same address).
KASLR of the Kernel Memory Regions
Kernel Address Space Layout Randomization randomizes the kernel half just as ASLR randomizes the user half — to deny an attacker fixed kernel addresses. There are actually two kernel randomizations: the kernel image base (CONFIG_RANDOMIZE_BASE, slides the text/module mapping) and the kernel memory regions (CONFIG_RANDOMIZE_MEMORY, slides the direct map, vmalloc, and vmemmap). The v6.12 mm.rst is explicit:
“Note that if CONFIG_RANDOMIZE_MEMORY is enabled, the direct mapping of all physical memory, vmalloc/ioremap space and virtual memory map are randomized. Their order is preserved but their base will be offset early at boot time.” (mm.rst, v6.12).
The mechanism is kernel_randomize_memory() in arch/x86/mm/kaslr.c. It treats the three big regions as an ordered list:
/* arch/x86/mm/kaslr.c, v6.12 (abridged) */
static __initdata struct kaslr_memory_region {
unsigned long *base;
unsigned long *end;
unsigned long size_tb;
} kaslr_regions[] = {
{ .base = &page_offset_base, .end = &physmem_end },
{ .base = &vmalloc_base },
{ .base = &vmemmap_base },
};The algorithm: start at the un-randomized base (__PAGE_OFFSET_BASE_L4/L5), end at vaddr_end = CPU_ENTRY_AREA_BASE. Sum each region’s required size, leaving remain_entropy = (vaddr_end − vaddr_start) − Σ sizes of slack to distribute as random padding. Then walk the regions in fixed order, prepending each a random slice of the remaining entropy (PUD-aligned), and recording the chosen base into the live variable (page_offset_base, vmalloc_base, vmemmap_base) that all the macros above resolve to. The header comment notes this gives, on the best configuration, “30,000 possible virtual addresses in average for each memory region.”
Two design points fall out:
- Order is preserved, only bases slide. Direct map is still below vmalloc, still below vmemmap — code that assumes the ordering is safe; code that hardcodes a base is not (hence the
*_basevariables). - KASLR must not overlap anything except KASAN. The mm.rst warns: “The KASLR address range must not overlap with anything except the KASAN shadow area, which is correct as KASAN disables KASLR.” The KASAN shadow (
0xffffec0000000000, 16 TiB) and KASLR are mutually exclusive — a debug build with KASAN turns KASLR off.
The randomization happens once at boot (it is __init code); the bases are then __ro_after_init constants for the life of the kernel.
Why Map All of RAM, and the Trade-offs
Mapping every physical page into the kernel half permanently is a deliberate design choice with clear costs and benefits.
Benefit: any physical page is instantly addressable by the kernel with zero setup — no per-access mapping, no TLB flush to “bring in” a page. The page allocator can hand back a struct page, and page_address() / __va gives an immediately-usable pointer. This is what makes kernel memory access cheap.
Cost / why it was once a problem: on 32-bit x86 the kernel half was only ~1 GiB, far smaller than installed RAM, so not all RAM could fit in the direct map — the origin of the painful highmem mechanism (temporary kmap of high pages). On x86-64 the 64-TiB direct-map window dwarfs any real machine’s RAM, so highmem is gone and the direct map simply covers everything. The phrase “up to the highest memory address (… can also include PCI memory holes)” in the doc reflects that the map spans the whole physical range, holes included.
Security cost: a permanent 1:1 map of all RAM is also a giant attack surface — an attacker with a kernel write primitive can reach any physical page through it. Mitigations include marking the direct map non-executable (it never holds code), and hardening features that restrict or unmap portions; KASLR of page_offset_base is part of the same defensive picture.
Failure Modes and Misconceptions
- Calling
__pa()/__va()on a vmalloc or ioremap pointer. These arithmetic macros are valid only for direct-map and kernel-text addresses. A vmalloc address has no fixed relationship to its physical pages; usevmalloc_to_page(). This is a recurring real-world bug (e.g., handing a vmalloc buffer to DMA that expects direct-map/virt_to_physsemantics). - Assuming
page_offset_baseis a compile-time constant. WithCONFIG_DYNAMIC_MEMORY_LAYOUT(needed for KASLR) it is a runtime variable. Out-of-tree code that hardcodes0xffff888000000000breaks on a randomized kernel. - Confusing kernel KASLR with user ASLR. They are independent mechanisms with independent sysctls/configs; disabling one does not affect the other (Linux Virtual Address Space Layout).
- “vmalloc is just like kmalloc but bigger.” No —
kmalloc/the direct map give physically-contiguous memory;vmallocgives only virtually contiguous memory and is unsuitable for DMA that needs physical contiguity. See vmalloc and Virtually Contiguous Memory. - Expecting the kernel text to be in the direct map. It is mapped separately at
-2 GiB(__START_KERNEL_map); this is exactly why__pa()needs its two-case logic.
See Also
- Linux Virtual Address Space Layout — the user half and the user/kernel split this note’s upper half pairs with; the canonical-address hole and ASLR.
- vmalloc and Virtually Contiguous Memory — the allocator that fills the
vmalloc_baseregion with virtually-contiguous, physically-scattered memory. - The Physical Memory Model SPARSEMEM and vmemmap — the
struct pagearray that lives in thevmemmap_baseregion;pfn↔pagetranslation. - Five-Level Paging — LA57, which shifts every kernel base down to the
-64 PBstarting offset and enlarges these regions. - Linux Page Table Hierarchy — PGD/P4D/PUD/PMD/PTE; the shared kernel-half PGD entries copied into every process.
- The Translation Lookaside Buffer and TLB Shootdowns — why a coarse, large-page direct map matters for TLB reach.
- Memory Management Unit — the hardware MMU/TLB that walks these mappings (architecture background).
- MOC: Linux Memory Management MOC — §2 Page Tables and Address Translation.