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 public PoC implements the DoS path: it triggers a host kernel panic from guest actions alone, without requiring any freed memory to be reclaimed by a victim object. The panic is self-inflicted by the host kernel’s own corruption detection. Once the race window is entered and the direct-split shadow page is reused as an indirect shadow page, a chain of invariant violations unfolds in L0’s shadow MMU that terminates in KVM_BUG_ON_DATA_CORRUPTIONBUG → host panic.
This panic kills the host kernel immediately, terminating every virtual machine running on the same physical host. All tenant workloads are lost. On a public cloud physical node, this is a multi-tenant denial-of-service with no recovery short of a host reboot.

Step-by-Step Panic Sequence

1

Direct split page reused as indirect (role mismatch accepted)

The race lands a faulter inside the window where the stale direct-split shadow page (direct=1) is still linked at sptep. The faulter calls kvm_mmu_get_child_sp(vcpu, sptep, gfn, /*direct=*/false, access). The check at [1] compares only gfn, finds a match, and returns -EEXIST — signaling the caller to reuse the existing page rather than allocating a new one with the correct role. The direct-split page is now being treated as an indirect shadow page.
2

Leaf SPTE for gfn Q installed

The FNAME(fetch) path proceeds to install leaf SPTEs into the reused shadow page. Because ptg[PRIME_IDX] points to q_pa, a leaf SPTE is installed at slot PRIME_IDX that maps the q probe page (gfn Q). The rmap is updated: the SPTE pointer is registered under the real gfn key — the actual gfn of the q page.
3

Real gfn diverges from direct assumption

The reused page has no shadowed_translation array (direct pages use sp->gfn + index to compute leaf gfns). When kvm_mmu_page_set_translation is called for the new leaf, it computes the expected gfn as sp->gfn + PRIME_IDX — which resolves to a gfn in the greg region — but the actual leaf gfn is Q (the q_pa frame). The two values diverge.
4

WARN_ONCE fires at [6]

The divergence is caught by:
WARN_ONCE(gfn != kvm_mmu_page_get_gfn(sp, index),   // [6]
          "gfn mismatch under %s page %llx (expected %llx, got %llx)\n",
          sp->role.passthrough ? "passthrough" : "direct",
          sp->gfn, kvm_mmu_page_get_gfn(sp, index), gfn);
This fires as a warning in the host kernel log — "gfn mismatch under direct page 8a00 (expected 8b00, got 256e4)" — but execution continues. This is a symptom, not the fatal point.
5

Rmap registered under real gfn key

Despite the mismatch warning, the rmap entry for the leaf SPTE is registered under the real gfn key (Q). The shadow MMU now has an inconsistent state: the shadow page’s direct computation says the leaf belongs to one gfn, but the rmap tracks it under another.
6

Another fault overwrites and drops the leaf

On the next pass through the same location — triggered by another faulter or a subsequent writer toggle — mmu_set_spte calls drop_spte to clear the old SPTE before installing the new one.
7

pte_list_remove looks up rmap under computed key — entry not found

drop_sptermap_removepte_list_remove looks up the rmap using the computed gfn key (sp->gfn + PRIME_IDX), not the real gfn key under which the entry was registered. The rmap bucket for the computed key is empty. pte_list_remove detects the inconsistency.
8

KVM_BUG_ON_DATA_CORRUPTION fires → BUG → host panic

if (KVM_BUG_ON_DATA_CORRUPTION(!rmap_val, kvm))   // [7]
    goto out;
With CONFIG_BUG_ON_DATA_CORRUPTION=y, KVM_BUG_ON_DATA_CORRUPTION expands to BUG_ON(!rmap_val). BUG_ON triggers an oops; combined with panic_on_oops=1 (the default on RHEL), this is an immediate, unrecoverable host kernel panic. [8]

Actual RHEL Panic Log

The following panic was triggered on RHEL (kernel 6.12.0-211.26.1.el10_2.x86_64):
gfn mismatch under direct page 8a00 (expected 8b00, got 256e4)
WARNING: CPU: 6 PID: 974281 at arch/x86/kvm/mmu/mmu.c:689 kvm_mmu_page_set_translation.part.0+0xb7/0x130 [kvm]
CPU: 6 PID: 974281 Comm: qemu-kvm  6.12.0-211.26.1.el10_2.x86_64
kernel BUG at arch/x86/kvm/mmu/mmu.c:1091!
RIP: 0010:pte_list_remove.isra.0+0xd9/0xe0 [kvm]
CPU: 3 PID: 974278 Comm: qemu-kvm
The WARNING at mmu.c:689 is the WARN_ONCE at [6] (kvm_mmu_page_set_translation). The kernel BUG at mmu.c:1091 is the KVM_BUG_ON_DATA_CORRUPTION at [7] (pte_list_remove). Both fire on qemu-kvm threads (L0’s QEMU vCPU threads for L1), which confirms the panic originates entirely in L0’s in-kernel KVM, triggered by L1 guest actions.

PoC Output on the Guest Side

From the module’s perspective inside L1, the final output before the host goes down is:
[*] poc step 4/4: race live -- host DoS triggering
...
kernel BUG at arch/x86/kvm/mmu/mmu.c (pte_list_remove)
Comm: qemu-kvm
The guest itself freezes when the host panics and stops servicing VM exits. The attacker’s L1 instance becomes unresponsive along with every other VM on the physical host.

Why No Victim Object Is Needed

Unlike the full RCE path (see Full Escape), the DoS path requires no cross-cache heap manipulation, no reclamation of the freed shadow page by a victim object, and no control over write values. The panic is triggered entirely by the rmap accounting invariant being broken: the shadow MMU’s own integrity check (KVM_BUG_ON_DATA_CORRUPTION) detects the corruption and calls BUG(). This makes the DoS path highly reliable — a single successful race window entry is sufficient.

Build docs developers (and LLMs) love