gfn in both cases — yet the two shadow pages it must create have opposite role.direct values. This is the precondition that makes the role-mismatch reuse in kvm_mmu_get_child_sp reachable. The build_world() function inside poc.c constructs this geometry entirely from L1 physical memory with no host cooperation.
Memory Allocations
Three regions are central to the exploit:low_va/low_pa— An order-9 (2 MB) allocation that provides the identity-mapped guest physical space for the L2 guest image, GDT, TSS, page tables, and stack. The L2 page tables insidelow_vamap the first 2 MB of L2 guest virtual space (npd[0]) and theHVwindow (npd[PDE_IDX]) using standard 2 MB PTEs withPF_P|PF_RW|PF_US|PF_PS.greg_va/greg_pa— A second order-9 allocation. This page serves the double role: whennest_pd[PDE_IDX]holds a huge-page entry,greg_pais the physical frame being mapped as 2 MB; whennest_pd[PDE_IDX]holds a table entry, the first page ofgreg_va(ptg) acts as the 4 KB page table that L0 must shadow indirectly.q_va/q_pa— A single 4 KB probe page filled entirely with0x4141414141414141. Its only role is to appear atptg[PRIME_IDX]so that when the reused direct-split shadow page installs a leaf for it, the real gfn (Q) diverges from the direct assumption (sp->gfn + PRIME_IDX), causing theWARN_ONCEat[6]and subsequently the fatalpte_list_removermap mismatch.
The Key Geometry
[10], ptg (the nested PT page) is taken as the first page of greg — so ptg_pa == greg_pa. The consequences are:
- When
nest_pd[PDE_IDX]mapsgregas a 2 MB large page (as at[11]), thegfnof the mapping isgreg_pa >> 12. L0’s shadow MMU also uses this value as thetable_gfnit will pass tokvm_mmu_get_child_spwhen processing the PDE. - When
nest_pd[PDE_IDX]instead points toptgas a page table,table_gfn = ptg_pa >> 12 = greg_pa >> 12. Identical.
direct=1), while the table-path demands an indirect shadow page (direct=0). This is the mismatch that the buggy reuse check at [1] in kvm_mmu_get_child_sp fails to catch.
At [12], ptg[PRIME_IDX] is pointed at q_pa. When the reused direct-split page later installs a leaf SPTE for gfn Q through slot PRIME_IDX, the shadow page believes the leaf gfn should be sp->gfn + PRIME_IDX — not the actual gfn of Q. This divergence is what breaks the rmap invariant and triggers the host panic.
gfn Match / Role Mismatch Table
Same gfn, opposite role — the exact condition that makes
[1] return -EEXIST when it should not.
L2 Guest Code
The L2 guest image (ncode) placed inside low_va at offset N_CODE is a minimal stub:
movabs rax, GVA; mov rax,[rax]; vmcall; hlt. The mov rax,[rax] dereferences GVA (set to HV for the normal fault path, or GVA_PRIME for the race path), inducing a nested EPT/NPT violation that forces L0 into ept_page_fault → FNAME(fetch) → kvm_mmu_get_child_sp. The vmcall (Intel) or vmmcall (AMD) signals normal exit so the faulter kthread can loop again immediately.
The race path uses
N_CODE_RACE which loads GVA_PRIME = HV + (0x100UL << 12), i.e. the virtual address that maps through ptg[PRIME_IDX] — the slot that points at the q probe page. This is what drives the leaf installation for gfn Q through the reused direct-split shadow page.Nested Page Table Structure
The full nested page table tree built bybuild_world() for L2 is:
the_root) is constructed by ops->mk_root(nest_pml4_pa) — for Intel this ORs in the EPT memory-type and walk-length fields (0x1e); for AMD the PML4 physical address is used as-is as nested_cr3.