The same role-mismatch reuse that enables the use-after-free write also has a direct and considerably simpler outcome: it causes KVM’s own internal data-structure integrity checks to fire, triggering an immediate host kernel panic. No freed memory needs to be reclaimed, no victim object spray is required, and the written value does not matter. The public PoC implements this DoS path exclusively.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.
This is the path that
poc.c exercises. The full use-after-free escape described in the UAF Write page requires significantly more exploit engineering. The DoS path is sufficient to bring down every guest on the affected physical host.Why the DoS Path Is Simpler
The use-after-free write requires the freed shadow pagespt to be reclaimed by a suitable victim object before the orphaned parent pointer is cleared — a cross-cache timing attack. The DoS path requires none of this. Instead, it relies on a gfn divergence that is immediately detectable by KVM’s rmap, and on the fact that pte_list_remove() treats such divergence as unrecoverable data corruption.
The two mechanisms share the same trigger: a direct=1 (direct split) shadow page being reused in a context that expects direct=0 (indirect). The divergence manifests differently because of how each shadow page type computes the gfn of an installed leaf SPTE.
gfn Divergence: Install vs. Remove
An indirect shadow page (direct=0) stores per-slot metadata in shadowed_translation[]. When a leaf SPTE is installed, the real guest gfn is written into sp->shadowed_translation[index] and used as the rmap key.
A direct split shadow page (direct=1) has no shadowed_translation — the pointer is NULL. Instead, it computes the leaf gfn mechanically as sp->gfn + index. This is correct when the page was legitimately split from a large page: the large page’s base gfn plus the 4KB index gives the leaf gfn.
When a direct=1 page is reused as direct=0, a leaf installed through it is registered in the rmap under the real guest gfn (e.g., gfn Q, the probe page). But when that leaf is later removed, the kernel calls kvm_mmu_page_get_gfn(sp, index), which computes sp->gfn + index — a completely different value. The rmap lookup uses this computed, wrong key and finds nothing.
The WARN: kvm_mmu_page_set_translation
The gfn mismatch is first observed — though not fatally — inkvm_mmu_page_set_translation:
[6] fires when a leaf is installed through the reused direct=1 page and the caller supplies the real gfn (e.g., Q), but kvm_mmu_page_get_gfn(sp, index) returns sp->gfn + index (a value derived from the large-page base gfn, not from Q). This WARN_ONCE is a diagnostic indicator only — it does not prevent the bad install from proceeding. The rmap entry is written under the real gfn key. The leaf’s removal will later look under the computed key. The divergence is now committed.
The Fatal Check: pte_list_remove
The actual crash occurs later, when a second fault overwrites and drops the leaf at the same SPTE slot. The call chain from that fault to the crash is:pte_list_remove function showing the integrity check at [7]:
pte_list_remove is called with the computed-gfn rmap key. Because the leaf was installed under the real gfn key (Q), the computed-gfn rmap bucket is empty — rmap_val is zero. The first guard fires: KVM_BUG_ON_DATA_CORRUPTION(!rmap_val, kvm).
KVM_BUG_ON_DATA_CORRUPTION and the Panic
[8] depends on kernel configuration:
| Kernel config | panic_on_oops | Outcome |
|---|---|---|
CONFIG_BUG_ON_DATA_CORRUPTION=y | set | BUG_ON → immediate host kernel panic |
CONFIG_BUG_ON_DATA_CORRUPTION=n | — | WARN_ON_ONCE + kvm_vm_bugged() — guest VM is killed, host survives |
Observed Panic Log
The following panic was triggered on RHEL (6.12.0-211.26.1.el10_2.x86_64) using the public PoC:
WARNINGatmmu.c:689— theWARN_ONCEat[6]inkvm_mmu_page_set_translationfires first, reporting the gfn mismatch between the expected value (8b00) and the real leaf gfn (256e4).kernel BUG at mmu.c:1091—pte_list_removehitsBUG_ONat[8]when the rmap lookup under the computed gfn key returns empty.
CPU: 6 and CPU: 3) and different PIDs (974281 and 974278), which is consistent with the multi-vCPU race: the install and the drop happen on separate faulter threads racing over mmu_lock.
PoC Invocation
The public PoC is a kernel module loaded inside the guest VM. It requires nested virtualization to be exposed to the guest, and the in-tree KVM module must be unloaded first because the PoC uses raw VMX/SVM instructions directly:nvcpu=). The writer toggles nest_pd[PDE_IDX] between a huge-page PTE and a table PTE. The faulters continuously run L2, generating faults that drive the shadow MMU fetch path. The race window opens between the PDE write and the subsequent kvm_page_track_write zap on L0. With the default dwell=256 spin count, the module reports progress periodically and the panic typically occurs within seconds to minutes on a vulnerable host.