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.

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

struct virt_ops {
	const char *name;
	int (*cpu_on)(int cpu);
	void (*cpu_off)(void);
	u64 (*huge_pte)(u64 pa);
	u64 (*tbl_pte)(u64 pa);
	u64 (*leaf4k)(u64 pa);
	u64 (*mk_root)(u64 pml4_pa);
	int (*vcpu_run)(int cpu);
};
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

static u64 vmx_huge_pte(u64 pa){ return pa | EPT_LEAF | EPT_PS; }   // [13] Intel
static u64 svm_huge_pte(u64 pa){ return pa | PF_P|PF_RW|PF_US|PF_PS; }   // [14] AMD
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:
ops = amd ? &svm_ops : &vmx_ops;   // [15]

Full Backend Structs

Intel (VMX/EPT):
static struct virt_ops vmx_ops = {
	.name     = "VMX/EPT",
	.cpu_on   = vmx_cpu_on,
	.cpu_off  = vmx_cpu_off,
	.huge_pte = vmx_huge_pte,
	.tbl_pte  = vmx_tbl_pte,
	.leaf4k   = vmx_leaf4k,
	.mk_root  = vmx_mk_root,
	.vcpu_run = vmx_vcpu_run,
};
AMD (SVM/NPT):
static struct virt_ops svm_ops = {
	.name     = "SVM/NPT",
	.cpu_on   = svm_cpu_on,
	.cpu_off  = svm_cpu_off,
	.huge_pte = svm_huge_pte,
	.tbl_pte  = svm_tbl_pte,
	.leaf4k   = svm_leaf4k,
	.mk_root  = svm_mk_root,
	.vcpu_run = svm_vcpu_run,
};

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_mmuept_page_faultept_fetchkvm_mmu_get_child_sp.Load command:
sudo rmmod kvm_intel; sudo insmod poc.ko

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:
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]
	...
}
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.

Build docs developers (and LLMs) love