Skip to main content
To the best of public knowledge, Januscape is the first guest-to-host KVM escape demonstrated on both Intel and AMD. This is not a coincidence of implementation — it is a direct consequence of where the bug lives. kvm_mmu_get_child_sp(), the function that performs the role-blind child shadow page reuse at [1], resides in arch/x86/kvm/mmu/mmu.c. That file is compiled once and shared by both the Intel VMX (kvm_intel.ko) and AMD SVM (kvm_amd.ko) paths. There is no per-vendor copy. Once nested virtualization is active, both architectures reach FNAME(fetch) → kvm_mmu_get_child_sp through the same ept_page_fault / npf_page_fault dispatch — the only difference is the page-table entry bit layout used to describe the nested page tables. poc.c isolates that difference behind a single virt_ops struct, selected at module load time by the amd parameter.

The virt_ops Abstraction

Every operation that touches architecture-specific state — enabling virtualization on a CPU, formatting page-table entries, encoding the nested page table root, and running the L2 guest — is dispatched through this interface. The shared exploit logic (build_world, the writer loop, the fault loop) calls only ops->huge_pte(), ops->tbl_pte(), and ops->leaf4k(), and is completely architecture-agnostic.

Page-Table Entry Bit Differences

Intel EPT entries use a combined RWX access field (EPT_RWX = 0x7), a memory type field (EPT_MT_WB = 6 << 3), and the page-size bit (EPT_PS = 1 << 7). AMD NPT entries use the standard x86 paging bits: Present (PF_P), Read/Write (PF_RW), User/Supervisor (PF_US), and Page Size (PF_PS = 0x80). The svm_tbl_pte and svm_leaf4k functions follow the same pattern, omitting PF_PS for non-leaf entries. The backend is selected at module init:

Full Backend Structs

Intel (VMX/EPT):
AMD (SVM/NPT):

Intel vs AMD Execution Path

The Intel path calls vmx_cpu_on() to execute vmxon with a per-CPU VMXON region, then vmx_vcpu_run() to set up a VMCS with EPT enabled (SEC_EPT in the secondary processor-based controls) and call vmlaunch. The EPT_POINTER VMCS field is set to the_root, which is nest_pml4_pa | 0x1e (walk length 4, memory type WB). L2 faults surface as EPT violations, dispatched by L0 through kvm_init_shadow_ept_mmu → ept_page_fault → ept_fetch → kvm_mmu_get_child_sp.Load command:

Why Both Architectures Hit the Same Bug

When L1 runs a nested guest with EPT (Intel) or NPT (AMD), L0 must shadow L1’s nested page tables in software. For this, KVM initializes a non-direct guest_mmu context:
Because guest_mmu’s root_role.direct is false at [16], is_tdp_mmu_active() returns false and the TDP MMU fast path is bypassed. The fault handler at [17] routes all nested faults into FNAME(fetch), which calls kvm_mmu_get_child_sp — the vulnerable function. The AMD counterpart kvm_init_shadow_npt_mmu sets up an identical non-direct guest_mmu. Neither path bypasses the shared shadow MMU code, which is exactly why the bug is architecture-neutral.
The nflood EPT-pointer flood optimization (R_FLOOD role) is Intel-only because it exploits the VMCS EPT_POINTER field. On AMD the flood role is suppressed (cpu <= nflood && !amd), but the AMD path is still reliably exploitable with faulter threads alone.