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/__pa conversions every driver relies on), the vmalloc/ioremap area for virtually-contiguous allocations, the vmemmap region that holds the struct page array, 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.rst table 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.18 mm.rst is 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
#endif

So __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’s arch/x86/mm/init.c / pat/set_memory.c and 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)
#endif

VMALLOC_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
#endif

KASLR 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)OffsetSizeRegionPurpose
0xfffffc0000000000-4 TB2 TBunused holealso vaddr_end for KASLR
0xfffffe0000000000-2 TB0.5 TBcpu_entry_areaper-CPU entry trampolines/stacks (needed under PTI)
0xffffff0000000000-1 TB0.5 TB%esp fixup stacks16-bit stack-segment fixup for IRET edge cases
0xffffffef00000000-68 GB64 GBEFI regionEFI runtime services (mapped in efi_pgd)
0xffffffff80000000-2 GB512 MBkernel textthe kernel image, mapped to physical 0
0xffffffffa0000000-1.5 GB1520 MBmodule mappingwhere loadable .ko modules land
FIXADDR_START~-11 MB~0.5 MBfixmapcompile-time-fixed virtual slots
0xffffffffff600000-10 MB4 KBlegacy vsyscallthe 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_SIZE caps 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 at MODULES_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_START up 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, early ioremap). 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 the vaddr_end ceiling 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 of gettimeofday. 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:

  1. 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 *_base variables).
  2. 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; use vmalloc_to_page(). This is a recurring real-world bug (e.g., handing a vmalloc buffer to DMA that expects direct-map/virt_to_phys semantics).
  • Assuming page_offset_base is a compile-time constant. With CONFIG_DYNAMIC_MEMORY_LAYOUT (needed for KASLR) it is a runtime variable. Out-of-tree code that hardcodes 0xffff888000000000 breaks 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; vmalloc gives 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