Shadow Page Tables

Shadow page tables are KVM’s software technique for virtualizing the memory management unit (MMU) on processors that lack hardware nested paging. The guest runs its own page tables that map guest-virtual addresses (GVA) to guest-physical addresses (GPA), but guest-physical is itself a fiction the host invents — it must be remapped to host-physical addresses (HPA). With no hardware to do that second translation, KVM cannot let the real CPU MMU walk the guest’s page tables, because those tables contain guest-physical addresses that mean nothing to the hardware. Instead KVM builds a parallel set of tables — the shadow — that the hardware MMU actually loads into CR3, and whose entries encode the fully-composed translation GVA → HPA directly (KVM MMU doc, v6.12). To keep the shadow faithful to the guest’s intent, KVM write-protects the guest’s real page tables and traps every modification, resynchronizing the shadow on each guest write. It is correct, general, and slow — and it is the technique two-dimensional paging (Intel EPT, AMD NPT) was invented to replace. KVM still falls back to shadow paging when hardware nested paging is unavailable and uses it as a building block for some nested-virtualization cases.

This note is the pre-EPT software story of memory virtualization and the engineering scar it left in KVM’s MMU code. Its hardware successor — the second hardware-walked page table that made all of this unnecessary in the common case — lives in Two-Dimensional Paging (EPT and NPT). The registration of guest RAM that both techniques sit on top of is Guest Physical Memory and Memory Slots. The host-side memory machinery (reclaim, huge pages, NUMA) is the Linux Memory Management MOC.

The Core Problem: Two Layers of Translation, One MMU

A normal (non-virtualized) MMU performs one translation: virtual address → physical address, walking the page tables rooted at the CR3 control register. Virtualization introduces a second layer. The guest believes it owns physical memory, so it builds page tables mapping GVA → GPA and points its own (virtual) CR3 at them. But the guest’s “physical” address space is just a host abstraction; a guest-physical page at GPA 0x1000 might live anywhere in real host RAM, or might not be resident at all (it could be swapped out, not yet faulted in, or deduplicated). The mapping GPA → HPA is owned entirely by the host’s memory manager and recorded in KVM’s memory slots.

So a correct address resolution needs two walks: GVA → GPA (using the guest’s tables) and GPA → HPA (using KVM’s slot mapping). On a pre-Nehalem Intel CPU or a pre-Barcelona AMD CPU there is no hardware that can do the second walk — the MMU knows how to do exactly one translation through CR3. If KVM naively let the hardware load the guest’s CR3, the MMU would walk the guest’s tables and treat the GPAs in them as real host-physical addresses, reading and writing arbitrary host memory. That is both wrong and a catastrophic isolation failure.

The shadow-paging answer is to pre-compose the two translations into a single set of tables. KVM constructs its own page-table tree — the shadow — where a leaf entry for a given GVA holds the host-physical frame number directly. The real CR3 points at this shadow tree, so the one hardware walk the MMU performs resolves GVA → HPA in one shot, exactly as if there were no virtualization. The guest’s own tables are never loaded into the hardware CR3; they are data that KVM reads to know what the shadow should contain (KVM MMU doc, v6.12).

Mental Model

flowchart LR
  subgraph GUEST["Guest's own page tables (data, never in CR3)"]
    GPT["GVA to GPA<br/>mapping"]
  end
  subgraph KVM["KVM memory slots"]
    SLOT["GPA to HPA<br/>mapping"]
  end
  subgraph SHADOW["Shadow page tables (loaded into real CR3)"]
    SPT["GVA to HPA<br/>composed in one tree"]
  end
  GPT -->|"KVM reads, composes"| SPT
  SLOT -->|"KVM reads, composes"| SPT
  HW["Hardware MMU walks ONE table"]
  SPT --> HW
  GPT -.->|"write-protected;<br/>guest write traps"| KVM
  KVM -.->|"resync shadow"| SPT

Shadow paging composes two logical translations into one physical table. What it shows: the guest’s tables (GVA→GPA) and KVM’s slot map (GPA→HPA) are two separate pieces of information that KVM folds together into the shadow (GVA→HPA), which is the only table the hardware MMU ever walks. The dashed arrows are the synchronization burden: because the guest can edit its own tables at any time, KVM write-protects them so every guest write traps back into KVM, which then rebuilds the affected shadow entries. The insight to take: the hardware does one cheap walk per access, but every change to the guest’s mappings costs an expensive trap-and-resynchronize — that asymmetry is the entire performance story of shadow paging.

The Shadow Page and the SPTE

KVM’s central data structure is the shadow page, struct kvm_mmu_page, which holds 512 shadow page-table entries (sptes) — exactly the size of one x86-64 paging-structure page (KVM MMU doc). An spte is either a non-leaf entry (it points at another shadow page, mirroring a guest PML4/PDPT/PD entry) or a leaf entry (it holds an actual host-physical frame number plus permission bits, mirroring a guest PTE). The shadow tree therefore has the same shape as the guest’s tree but with HPAs substituted at the leaves.

A shadow page carries a role describing what it represents. The decisive field is role.direct, documented as: “If set, leaf sptes reachable from this page are for a linear range … starts at (gfn << PAGE_SHIFT). If clear, this page corresponds to a guest page table denoted by the gfn field.” A direct shadow page maps a contiguous run of guest-physical memory with no guest page table behind it — this is the mode used for two-dimensional paging, where the EPT/NPT tables are just a flat GPA → HPA map. An indirect shadow page (role.direct == 0) is the genuine shadow-paging case: it shadows a specific guest page table, identified by the guest frame number (gfn) of that table. The whole synchronization machinery below exists only for indirect pages.

In arch/x86/kvm/mmu/mmu.c the routing happens at MMU initialization. When two-dimensional paging is available, init_kvm_tdp_mmu() sets context->page_fault = kvm_tdp_page_fault and builds direct mappings. When it is not — or when the number of translations does not match the hardware — init_kvm_softmmu() calls kvm_init_shadow_mmu(), installing paging64_page_fault (for 64-bit guests) or paging32_page_fault, the genuine shadow handlers that walk the guest’s tables (the FNAME(walk_addr) / FNAME(fetch) templated paging code) (mmu.c, v6.12).

Mechanical Walk-through: Building and Keeping the Shadow

Step 1 — a guest page fault triggers a VM exit. Initially the shadow tree is empty. The guest touches GVA X; the hardware finds no shadow mapping and faults, causing a VM exit. KVM’s shadow page-fault handler walks the guest’s page tables in software (FNAME(walk_addr)) to resolve GVA X → GPA G, checking the guest’s own permission bits along the way (if the guest’s tables say the page is not present or the access is illegal, KVM injects the fault back into the guest rather than fixing the shadow).

Step 2 — translate GPA to HPA and install the leaf. KVM looks up GPA G in the memory slot to get the host-virtual address, faults in (or finds) the host page, and obtains the host-physical frame H. It then writes a leaf spte mapping X → H with permissions that are the intersection of the guest’s requested permissions and what the host page allows. mmu_set_spte() performs this; if the GPA has no backing slot it returns RET_PF_EMULATE, treating the access as MMIO instead of building a mapping (mmu.c, v6.12).

Step 3 — write-protect the shadowed guest page table. This is the crux. When KVM creates an indirect shadow page for a guest page-table page, account_shadowed() runs. It increments kvm->arch.indirect_shadow_pages and then calls kvm_mmu_slot_gfn_write_protect() on the guest frame holding that page table, which clears the writable bit in the spte(s) mapping the guest’s table page (rmap_write_protect()/spte_write_protect() for the legacy MMU; non-leaf table pages are guarded through the page-track machinery, __kvm_write_track_add_gfn()). From this moment, the guest cannot modify its own page tables without faulting (mmu.c account_shadowed, v6.12). Why is this necessary? The guest expects to edit its page tables freely (to map new memory, change permissions, swap pages). If it could do so silently, the shadow would drift out of sync and the hardware would walk stale GVA→HPA mappings. Write-protection converts every guest page-table edit into a trap that KVM can observe.

Step 4 — absorb the write and resynchronize. When the guest writes a now-read-only page-table page, the hardware faults; KVM’s emulator decodes the instruction, performs the write to the guest’s table, and then calls kvm_mmu_track_write() (historically kvm_mmu_pte_write). That function first checks kvm->arch.indirect_shadow_pages — if KVM is shadowing nothing, it returns immediately. Otherwise it locates every shadow page that shadows this guest frame (for_each_gfn_valid_sp_with_gptes) and, for each affected entry, zaps the stale spte (mmu_page_zap_pte) so it will be rebuilt lazily on the next fault. It also feeds two heuristics: detect_write_misaligned() (a write that doesn’t look like a clean PTE update) and detect_write_flooding() (mmu.c kvm_mmu_track_write, v6.12).

Step 5 — the write-flooding escape hatch. Emulating a single instruction is expensive — KVM must decode and execute it in software. A guest that writes the same page-table page repeatedly (for example, freeing a process’s address space) would generate a storm of these emulations. KVM tracks a write_flooding_count per shadow page; the MMU doc states the policy plainly: “if emulation is triggered too frequently on this page, KVM will unmap the page to avoid emulation in the future.” When flooding is detected, kvm_mmu_prepare_zap_page() tears down the shadow page entirely (incrementing kvm->stat.mmu_flooded), un-write-protecting the guest table, so subsequent guest writes proceed natively and the shadow is rebuilt from scratch on the next access (mmu.c, v6.12; KVM MMU doc).

The Unsync Optimization

Write-protecting every shadowed page table and trapping every write is brutally expensive for hot tables. KVM softens this with the unsync mechanism. Instead of keeping a leaf-level guest page table permanently write-protected, KVM can mark its shadow page unsync and let the guest write to that table freely, accepting that the shadow’s leaf sptes may temporarily disagree with the guest’s PTEs. The MMU doc: “If true, then the translations in this page may not match the guest’s translation … unsync ptes are synchronized when the guest executes invlpg” (or any TLB-flushing event) (KVM MMU doc).

The relevant code is mmu_try_to_unsync_pages(). When KVM is about to create a writable mapping for a guest frame that is itself a shadowed page table, it tries to unsync the shadowing pages rather than force write-protection. The function returns 0 if all reachable shadow pages were marked unsync (the write may proceed), or -EPERM if the page must stay write-protected — for instance because the page is write-tracked for another reason (kvm_gfn_is_write_tracked), which guards the assertion that only 4 KiB leaf tables are ever unsynced (mmu.c mmu_try_to_unsync_pages / kvm_unsync_page, v6.12). The trade-off: unsync trades the cost of trapping writes for the cost of resynchronizing the whole table at the next TLB flush (kvm_sync_page walks the guest table and rebuilds the sptes). This is profitable because x86 guests batch many PTE updates between flushes, so one resync amortizes over many writes. The correctness hinge is that x86 requires a TLB flush (invlpg, a CR3 reload, etc.) before a stale translation may be relied upon, so KVM is allowed to defer synchronization exactly until that flush.

Why It Was Correct But Slow and Complex

Shadow paging is correct — it never lets the hardware walk a guest-controlled table — and general: it works on any CPU with no virtualization-specific MMU hardware at all. But it pays in three currencies. First, VM-exit storms: every guest page-table modification is a trap-and-emulate, and page-table churn is constant in a busy OS (process creation/teardown, mmap/munmap, copy-on-write fork, demand paging). Second, memory overhead and bookkeeping: KVM maintains a separate shadow tree per guest CR3 (per address space), plus a reverse map (rmap) so that when a host page is reclaimed or write-protected KVM can find and zap every spte pointing at it — an entire auxiliary data structure that two-dimensional paging eliminates. Third, code complexity: the LWN write-up of the TDP MMU rework notes that shadow paging must “program the x86 page tables to encode the full translation of guest virtual addresses (GVA) to HPA” by building “a composite x86 paging structure,” which is “complicated,” whereas with TDP “KVM lets the guest control CR3 and programs the EPT/NPT paging structures with the GPA → HPA mapping,” eliminating the rmap and needing “only one version of the paging structure … per L1 paging mode” (Davydov/Stevens, LWN 2020).

The scalability cost was measured and severe. The same LWN article reports that on a 416-vCPU guest with 4 GiB per vCPU, “98% of the time was spent waiting for the MMU lock,” and the new TDP MMU cut the test duration by 89% — a direct consequence of shadow paging’s heavyweight, write-lock-bound synchronization (LWN 2020). This is why Intel added EPT and AMD added NPT, and why those are the default everywhere modern: they move the GPA → HPA walk into hardware, so the guest can own its CR3 and edit its tables natively with no traps at all.

Where KVM Still Uses Shadow Paging

Shadow paging is not dead code; it is the fallback and a nesting primitive.

  • No hardware nested paging. On a CPU without EPT/NPT (or with it disabled), tdp_enabled is false and KVM uses the shadow MMU for all guests. In mmu.c the module parameter tdp_mmu_enabled (default true on x86-64) selects the modern TDP MMU implementation, but it “falls back to the existing shadow paging implementation when TDP is not available” (LWN 2020; mmu.c, v6.12).

  • Nested virtualization without nested EPT. When a guest hypervisor (L1) runs its own guest (L2), there are potentially three translations to compose (ngVA → ngPA → GPA → HPA), but hardware does at most two. The MMU doc states the rule: “to encode between 1 and 3 translations into hardware that support only 1 (traditional) and 2 (tdp) translations. When the number of required translations matches the hardware, the mmu operates in direct mode; otherwise it operates in shadow mode.” If the host provides nested EPT to L1, hardware does ngVA→ngPA (L1’s EPT, shadowed) and GPA→HPA in two walks; without nested EPT, KVM shadows the combined translation. kvm_init_shadow_ept_mmu() builds an indirect (role.direct = false) MMU precisely for shadowing L1’s EPT structures (mmu.c kvm_init_shadow_ept_mmu, v6.12). See Nested Virtualization.

Uncertain

Verify: the precise statement that on a non-nested guest with EPT enabled the legacy shadow MMU (indirect) path is never taken — i.e. that direct-mode TDP fully replaces shadow paging in the common case. Reason: confirmed for the common path from init_kvm_tdp_mmu() vs init_kvm_softmmu() routing and the LWN description, but the full matrix of fallbacks (e.g. shadow paging forced for certain guest paging modes, SMM, or when allow_smaller_maxphyaddr is set) was not exhaustively traced in v6.12 source. To resolve: read kvm_init_mmu() / init_kvm_softmmu() decision logic end-to-end and the tdp_enabled gate in arch/x86/kvm/mmu/mmu.c and vmx.c. uncertain

The Legacy Shadow MMU vs the tdp_mmu

A second, orthogonal distinction confuses readers: shadow paging vs TDP (the algorithm — software-composed vs hardware-walked second table) is not the same as legacy MMU vs tdp_mmu (two C implementations of KVM’s page-table management in arch/x86/kvm/mmu/). The original “legacy” MMU code (mmu.c) implements both shadow paging and direct TDP mappings, using a global write-lock and the rmap. The newer tdp_mmu.c, enabled by tdp_mmu_enabled (default on for x86-64 since Linux 5.10), is a reimplementation of the direct/TDP case only using lockless, RCU-protected page-table walks (read-mostly mmu_lock) to fix the scalability collapse measured above (LWN 2020; mmu.c module param, v6.12). The tdp_mmu deliberately does not handle indirect/shadow pages — when shadow paging is needed (no hardware TDP, or nested without nested-EPT), KVM routes through the legacy MMU. So the two distinctions compose: TDP-the-algorithm can be served by either the legacy MMU (older code) or the tdp_mmu (newer, faster); shadow-paging-the-algorithm is served only by the legacy MMU.

Failure Modes and Common Misunderstandings

  • “Shadow paging is the same as EPT, just in software.” Close but importantly wrong about what the table contains. EPT/NPT add a second hardware-walked table that maps GPA → HPA, leaving the guest’s GVA → GPA table in the guest’s own CR3. Shadow paging produces one table holding GVA → HPA, and the guest’s CR3 never reaches the hardware. The guest’s tables are read as data, not walked by silicon.
  • Forgetting that the rmap is mandatory for shadow paging. Because a single host page can be mapped by many sptes (multiple guest GVAs, multiple guest CR3s), reclaiming or migrating that host page requires finding every spte — the reverse map. The TDP MMU could drop the rmap; the shadow MMU cannot. Bugs here surface as stale mappings after page migration or swap.
  • Assuming write-protection is permanent. The unsync optimization and write-flooding teardown both remove write-protection from hot guest tables. A mental model that says “all guest page tables are always read-only under shadow paging” mispredicts both performance (resyncs happen at TLB flushes, not at writes) and the code paths (kvm_sync_page, mmu_flooded).
  • Diagnosis. Shadow-paging activity is visible in KVM’s stats: kvm_stat / the /sys/kernel/debug/kvm/* counters expose mmu_pte_write, mmu_flooded, mmu_unsync, and mmu_pde_zapped. A guest pegged on these counters with tdp_enabled false is paying the shadow-paging tax; the fix is to enable EPT/NPT or, for nested workloads, nested EPT.

Alternatives and When to Choose Them

There is essentially no choice to make at the operator level today: if the hardware has EPT (Intel, since Nehalem ~2008) or NPT (AMD, since Barcelona ~2007) you use two-dimensional paging, because it is faster on every axis. Shadow paging is what you get when (a) the CPU lacks nested paging — vanishingly rare on server hardware in 2026; (b) EPT/NPT is explicitly disabled (kvm-intel.ept=0 / kvm-amd.npt=0), which is essentially only done for debugging or to reproduce historical behavior; or (c) you are nesting and the L1 hypervisor was not given nested EPT, so KVM must shadow the combined translation. The historical alternative shadow paging itself replaced was binary translation (early VMware), which rewrote sensitive guest instructions because pre-VT-x x86 could not even trap-and-emulate cleanly; KVM never used binary translation because it requires the hardware virtualization extensions that postdate that era. See Why x86 Needed Hardware Virtualization.

Production Notes

In practice, on any cloud or modern data-center host, shadow paging for L1 guests is off the table — EPT/NPT is the norm, and the visible KVM MMU code path is the tdp_mmu. Shadow paging resurfaces almost exclusively in nested scenarios: running KVM-in-KVM, Windows nested Hyper-V, or CI runners that boot VMs inside VMs. There the choice is whether the host exposes nested EPT to L1 (kvm-intel.nested=1 with EPT, the default on capable hardware) — with it, L1 gets hardware-assisted two-dimensional paging for its own L2 guests and avoids the shadow tax; without it, every L2 page-table edit cascades into L1 traps that the host shadows, and nested performance falls off a cliff. This is the concrete reason “always enable nested EPT” is the standing advice for nested KVM. The deeper history — Avi Kivity’s original KVM MMU and the long road to the scalable tdp_mmu — is documented in the OLS 2007 KVM paper (Kivity et al., OLS 2007) and the 2020 TDP MMU LWN coverage (LWN 2020).

See Also