The root cause of Januscape is a one-field omission in a single reuse guard insideDocumentation 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.
kvm_mmu_get_child_sp(). The function decides whether to reuse an already-linked child shadow page based solely on the guest frame number, without verifying that the existing page’s role matches what the caller is requesting. When a direct-split shadow page (role.direct=1) and an indirect shadow page (role.direct=0) are both needed for the same gfn — a situation that arises naturally in the shadow MMU fetch path — the guard fires prematurely, returning the wrong-role page as if it were valid. Everything downstream breaks from that point forward.
The Vulnerable Function
kvm_mmu_get_child_sp() is defined in arch/x86/kvm/mmu/mmu.c. The vulnerable code before patch 81ccda30b4e8 is:
role.direct bit encodes a fundamental distinction:
direct=0(indirect): The shadow page mirrors a guest-controlled page table. Itsshadowed_translationarray records the gfn→access mapping for each installed leaf, because the guest can map arbitrary gfns through it.direct=1(direct split): The shadow page was synthesized by KVM when a 2MB large page was demoted to 4KB. KVM computes the leaf gfn assp->gfn + indexrather than consulting ashadowed_translationarray (which isNULLfor direct pages).
The same gfn legitimately requires shadow pages of different roles because the same physical page can appear as both the data leaf of a 2MB mapping and as a guest page table page. When the guest constructs a nested page table such that
ptg_pa == greg_pa (the page table page and the first page of a large-mapped region share the same physical address), L0 may need a direct=1 split page for the large-page mapping of that gfn and a direct=0 indirect page for when the same gfn is used as a page table. These two roles are semantically incompatible and must never be confused.The FNAME(fetch) Context
The two conflicting roles are both produced inFNAME(fetch), the shadow MMU’s page-fault handler that walks the guest page table and builds shadow pages for each level:
| Call | direct arg | Shadow page role | Purpose |
|---|---|---|---|
[2] | false | indirect (direct=0) | Shadows a guest page table pointed to by an upper PDE entry |
[3] | true | direct split (direct=1) | 4KB split created because the 2MB large page cannot be mapped as-is |
table_gfn == base_gfn — achieved in the PoC by making ptg_pa == greg_pa — both call sites operate on the same gfn at the same sptep.
How the Bug Fires
The race proceeds as follows:- The writer vCPU toggles
nest_pd[PDE_IDX]from a huge-page PTE (direct=1path active) to a table PTE (direct=0path active). - During the huge-page phase,
[3]runs first and links adirect=1shadow page atsptepfor gfn G. - L0 has not yet zapped the old shadow link at
kvm_page_track_write. A faulter vCPU wins themmu_lockin this window. - The faulter reaches
[2]requesting an indirect (direct=0) shadow page for the same gfn G. - The guard at
[1]fires:spte_to_child_sp(*sptep)->gfn == gfnis true (both are G). Role is not checked.ERR_PTR(-EEXIST)is returned. FNAME(fetch)treats this as “page already exists” and continues using thedirect=1page for thedirect=0role. The wrong-role page is now in active use.
nvcpu=8 faulter threads contending over mmu_lock, they delay the writer’s kvm_page_track_write zap and widen the race window. The PoC module demonstrates that this window is wide enough to win deterministically given sufficient time.
The Fix
Patch81ccda30b4e8 adds a role.word comparison to the reuse guard, so that an existing child page is reused only when both the gfn and the role match:
kvm_mmu_child_role() call before the guard, so the role is available for comparison without restructuring the control flow. With this change, a request for direct=0 on a gfn that already has a direct=1 page linked at sptep will not match the guard, and kvm_mmu_get_shadow_page() will be called to create or fetch the correctly-typed shadow page.