Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/V4bel/Januscape/llms.txt

Use this file to discover all available pages before exploring further.

The entire exploit hinges on a specific memory geometry: a single physical page that is simultaneously the leaf destination of a 2 MB large-page PDE entry and the page table page that a second configuration of the same PDE points into. Because both configurations resolve through the same PDE index to the same guest frame number, L0’s shadow MMU sees the same 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 inside low_va map the first 2 MB of L2 guest virtual space (npd[0]) and the HV window (npd[PDE_IDX]) using standard 2 MB PTEs with PF_P|PF_RW|PF_US|PF_PS.
  • greg_va / greg_pa — A second order-9 allocation. This page serves the double role: when nest_pd[PDE_IDX] holds a huge-page entry, greg_pa is the physical frame being mapped as 2 MB; when nest_pd[PDE_IDX] holds a table entry, the first page of greg_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 with 0x4141414141414141. Its only role is to appear at ptg[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 the WARN_ONCE at [6] and subsequently the fatal pte_list_remove rmap mismatch.

The Key Geometry

ptg = (u64 *)greg_va;   // [10]
ptg_pa = greg_pa;
...
nest_pd[PDE_IDX] = ops->huge_pte(greg_pa);   // [11]
...
ptg[0]         = ops->leaf4k(greg_pa);
ptg[PRIME_IDX] = ops->leaf4k(q_pa);   // [12] probe page filled with 0x4141...
At [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] maps greg as a 2 MB large page (as at [11]), the gfn of the mapping is greg_pa >> 12. L0’s shadow MMU also uses this value as the table_gfn it will pass to kvm_mmu_get_child_sp when processing the PDE.
  • When nest_pd[PDE_IDX] instead points to ptg as a page table, table_gfn = ptg_pa >> 12 = greg_pa >> 12. Identical.
The gfn is therefore the same in both configurations, but the required shadow page role differs: the huge-path demands a direct-split page (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

PDE configurationPhysical targettable_gfn passed to shadow MMURequired role.direct
huge_pte(greg_pa)greg_pa mapped as 2 MB leafgreg_pa >> 121 (direct split)
tbl_pte(ptg_pa)ptg_pa as page tableptg_pa >> 12 = greg_pa >> 120 (indirect shadow)
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:
static u8 ncode[] = { 0x48, 0xb8, 0,    0,    0,    0,    0,    0,   0,
                      0,    0x48, 0x8b, 0x00, 0x0f, 0x01, 0xc1, 0xf4 };
Decoded: 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_faultFNAME(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 by build_world() for L2 is:
nest_pml4[0]     → nest_pdpt          (tbl_pte)
nest_pdpt[0]     → nest_pd            (tbl_pte)
nest_pd[0]       → nest_pt0           (tbl_pte)  ; identity maps low_pa
nest_pt0[i]      → low_pa + i*0x1000  (leaf4k, i=0..511)
nest_pd[PDE_IDX] → greg_pa            (huge_pte OR tbl_pte — toggled by race writer)
ptg[0]           → greg_pa            (leaf4k)
ptg[PRIME_IDX]   → q_pa               (leaf4k)  ; 0x4141... probe
The root pointer (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.

Build docs developers (and LLMs) love