The role-mismatch reuse inDocumentation 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 can only fire if two conditions hold simultaneously: the stale child shadow page (wrong role) is still linked at sptep, and a faulter is calling kvm_mmu_get_child_sp with the new role. L0’s shadow MMU does not update these atomically — there is a window between when L0 commits a new PDE value observed by a faulter and when it calls kvm_page_track_write to zap the old shadow link. The race in poc.c is designed to land a faulter precisely inside that window. With multiple faulters contending over mmu_lock, the window is widened enough that the race wins deterministically on any unpatched kernel given enough time.
Thread Roles
Each kthread launched by the module is assigned one of three roles:R_WRITER(CPU 0): The sole writer. Continuously togglesnest_pd[PDE_IDX]between a huge-page entry and a table entry with a configurable spin delay (dwell, default 256cpu_relax()iterations) between each write.R_FLOOD(Intel only, CPUs 1..nflood): EPT-pointer flood threads. On each VMEXIT they switch the VMCSEPT_POINTERto a different pre-built EPT root from thepress_root[]array. This forces L0 to process many different EPT contexts, increasing TLB and shadow MMU pressure to widen the race window. The AMD path suppresses this role (!amd).R_FAULT(remaining CPUs, default 7): Faulter threads. Each runs L2 in a tight loop, generating a constant stream of nested EPT/NPT violations through thePDE_IDXentry to feed L0’s shadow MMU fetch path.
The Writer Loop
[19]: The PDE tries to map greg as a 2 MB page. However, because ptg (which shares greg_pa) is already tracked as a shadowed page table, account_shadowed() has registered a disallow_lpage for that gfn. When a faulter takes a nested fault through this PDE, kvm_mmu_hugepage_adjust() observes the disallow and lowers the fault to a 4 KB resolution. L0 then creates a direct-split shadow page (direct=1) at greg_pa >> 12 and links it at sptep.
Table write [20]: The PDE now points to ptg as a page table. When a faulter takes a nested fault through this PDE, L0’s FNAME(fetch) needs an indirect shadow page (direct=0) at the same gfn (ptg_pa >> 12 == greg_pa >> 12).
The Race Window
The non-atomic window that the exploit targets:- The writer stores the new PDE value (
nest_pd[PDE_IDX] = ops->tbl_pte(ptg_pa)at[20]). - L0 begins processing a page-track write notification for the previous (huge) PDE.
- Race window opens: L0 has not yet called
kvm_page_track_writeto zap the old direct-split shadow link atsptep. - A faulter faults through the PDE and calls
kvm_mmu_get_child_sp(vcpu, sptep, gfn, /*direct=*/false, access). - The check at
[1]seesspte_to_child_sp(*sptep)->gfn == gfn(true — samegreg_pa >> 12) and returns-EEXISTwithout checkingrole.word. - The fetch path reuses the direct-split page as if it were the indirect shadow page it requested.
- Race window closes when
kvm_page_track_writefinally zapssptep— but the damage is already done.
How Contention Widens the Window
Withnvcpu=8 (default), seven faulter kthreads hammer the mmu_lock spinlock simultaneously. Each time the writer calls kvm_page_track_write, it must acquire mmu_lock to zap old shadow entries. The competing faulters hold mmu_lock for their own fault processing, delaying the writer’s track_write and systematically lengthening the window. The more faulters contend, the wider the window, and the shorter the expected time to win.
With
CONFIG_BUG_ON_DATA_CORRUPTION=y (the default on RHEL and many distributions), the panic fires immediately the first time the rmap invariant is broken — no need for repeated hits. Even a single successful race window entry is sufficient.Time to Win
The time from module load to host panic is non-deterministic and depends on CPU count, scheduler latency, andmmu_lock contention patterns. In practice, on vulnerable kernels, the race triggers within seconds to minutes. The run_ms module parameter (default 600,000 ms) sets a deadline after which the writer self-terminates without winning, but on real hardware the race almost always fires well within the default window.
Diagnostic Output
The writer periodically prints progress and the faulters log their first VMEXIT details. After the race triggers, the sequence visible in the host kernel log is:gfn mismatch line is the first visible symptom — a WARN_ONCE at [6] — followed immediately by the fatal BUG from pte_list_remove at [7].