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.

KVM translates guest virtual addresses to host physical addresses through two fundamentally different mechanisms. Understanding which mechanism is active — and why nested virtualization forces a switch to the legacy software path — is essential to understanding where Januscape lives and why it is triggerable on both Intel and AMD hosts despite being pure in-kernel KVM code.

Two Address-Translation Modes in x86 KVM

Hardware Two-Stage Paging (TDP MMU)

Intel EPT and AMD NPT let the hardware handle both GVA→GPA and GPA→HPA translation. This is the default on modern hosts (kvm-intel.ept=1, tdp_mmu=Y). KVM uses the TDP MMU for this path, which bypasses the legacy shadow MMU entirely.

Shadow Paging (Shadow MMU)

KVM shadows guest page tables in software. Each level of the guest PT is represented by a struct kvm_mmu_page (a shadow page), whose backing 4KB page is sp->spt. The shadow MMU walks the guest-controlled PT tree and creates or links a shadow page for each level.
On a default host with EPT enabled, the TDP MMU handles direct GPA translation for the guest and the shadow MMU is not involved. The critical exception is nested virtualization.

Why Nested Virtualization Forces Shadow Paging

Level notation: L0 is the bare-metal host kernel running KVM. L1 is the first-level guest — the VM the attacker controls. L2 is the nested guest that L1 (acting as a hypervisor) launches inside itself. The bug fires on L0 while L0 processes faults generated by L2 execution.
When L1 acts as a hypervisor and runs L2 with its own EPT or NPT, the hardware has only one stage of address translation available at any point in time — it cannot simultaneously walk L1’s guest page tables and apply L0’s EPT. L0 must therefore shadow L1’s nested EPT/NPT in software, maintaining a parallel set of shadow pages that combine L1’s GPA→L2 GPA mapping with L0’s own GPA→HPA mapping. Because this nested EPT/NPT shadowing follows page tables constructed and controlled by the guest (L1), it goes through the legacy shadow MMU rather than the TDP MMU — regardless of whether ept=1 is set on the host. This is the code path that Januscape exploits. The relevant initialization confirms this clearly:
void kvm_init_shadow_ept_mmu(struct kvm_vcpu *vcpu, bool execonly,
                             int huge_page_level, bool accessed_dirty,
                             gpa_t new_eptp)
{
    struct kvm_mmu *context = &vcpu->arch.guest_mmu;   // [16]
    ...
    context->page_fault = ept_page_fault;              // [17]
    ...
}

static inline bool is_tdp_mmu_active(struct kvm_vcpu *vcpu)
{
    return tdp_mmu_enabled && vcpu->arch.mmu->root_role.direct;  // [18]
}
The nested-EPT MMU (guest_mmu) at [16] shadows guest-controlled page tables, so its root_role.direct is false. KVM therefore installs the legacy ept_page_fault handler at [17], not kvm_tdp_page_fault. is_tdp_mmu_active at [18] also returns false because the root role is non-direct — the TDP MMU lockless fast path is bypassed entirely. On AMD, kvm_init_shadow_npt_mmu applies the same logic with the same result.
This means the bug path is active on any default ept=1 host — as long as the guest has nested virtualization exposed and uses it to run L2. The host never needs to have shadow paging explicitly configured.

This Is Pure In-Kernel KVM

Unlike many published VM-escape vulnerabilities that involve QEMU device emulation, Januscape lives entirely inside in-kernel KVM. The faults that occur as L1 builds its nested EPT and runs L2 are handled entirely by the host kernel’s KVM module and never reach userspace. QEMU is not involved and is not a prerequisite. This also means the vulnerability threatens cloud providers that have replaced QEMU with their own VMM.

struct kvm_mmu_page and Shadow Page Roles

Each level of the shadowed page table tree is represented by a struct kvm_mmu_page. The key field for understanding this bug is role, a packed bitfield of type union kvm_mmu_page_role that encodes what kind of shadow page this is:
union kvm_mmu_page_role {
    u32 word;
    struct {
        unsigned level:4;
        unsigned has_4_byte_gpte:1;
        unsigned quadrant:2;
        unsigned direct:1;       /* distinguishes indirect vs. direct split */
        unsigned access:3;
        unsigned invalid:1;
        unsigned efer_nx:1;
        unsigned cr0_wp:1;
        unsigned smep_andnot_wp:1;
        unsigned smap_andnot_wp:1;
        unsigned ad_disabled:1;
        unsigned guest_mode:1;
        unsigned passthrough:1;
        ...
    };
};
The role.direct bit is the critical field for this vulnerability:
role.directMeaning
0 (indirect)This shadow page shadows an actual guest page table. The guest controls the contents of the page it maps.
1 (direct split)This shadow page was split by KVM from a large page (2MB) into 4KB mappings. KVM controls it; there is no corresponding guest PT.
Both types of shadow page can exist at the same gfn simultaneously under certain conditions — and the bug arises precisely when the kernel fails to distinguish them during reuse decisions.

The rmap Reverse Map

KVM maintains a reverse map (rmap) per memslot, indexed by guest frame number (gfn), to track which leaf SPTEs currently map a given physical page. The invariant of this data structure is straightforward: the gfn key used when a leaf SPTE is installed into the rmap must be the same gfn key used when that leaf SPTE is removed.
Install leaf SPTE → rmap_add(gfn, spte)
Remove  leaf SPTE → rmap_remove(gfn, spte)
                              ^^^
                    must match the key used at install time
When role confusion causes a shadow page of the wrong type to be reused, the gfn key at install time diverges from the gfn key at removal time. This rmap invariant violation is what causes both the DoS (caught by KVM’s own integrity check) and the use-after-free (orphaned parent pointers that are later cleared into potentially reallocated memory).

Build docs developers (and LLMs) love