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 only the DoS path — triggering a host kernel panic via KVM_BUG_ON_DATA_CORRUPTION. A full guest-to-host escape that achieves arbitrary code execution with root privilege on L0 exists and was demonstrated in a controlled environment, but it is not released at this time. The same use-after-free bug that causes the DoS also yields a write primitive against freed kernel memory, but exploiting it for RCE is substantially more difficult than the panic path because the primitive is highly constrained.
Januscape was used as a 0-day exploit in Google kvmCTF — a public competition run by Google’s security team that offers rewards for KVM guest-to-host exploits demonstrated against production kernels. kvmCTF targets in-kernel KVM exclusively; QEMU-level exploits are out of scope. Januscape’s purely in-kernel nature made it directly applicable.

The UAF Write Primitive

After the direct-split shadow page is reused as indirect and eventually freed by __kvm_mmu_prepare_zap_page, its backing 4 KB spt page is returned to the buddy allocator. KVM’s parent-pointer cleanup code still holds a stale pointer into this page. When it later clears the orphaned parent pointer, it executes:
static void drop_parent_pte(struct kvm *kvm, struct kvm_mmu_page *sp,
                             u64 *parent_pte)
{
    mmu_page_remove_parent_pte(kvm, sp, parent_pte);
    mmu_spte_clear_no_track(parent_pte);   // [4]
}

static void mmu_spte_clear_no_track(u64 *sptep)
{
    __update_clear_spte_fast(sptep, SHADOW_NONPRESENT_VALUE);   // [5]
}

#ifdef CONFIG_X86_64
#define SHADOW_NONPRESENT_VALUE  BIT_ULL(63)
#else
#define SHADOW_NONPRESENT_VALUE  0ULL
#endif
At [4], the kernel believes it is clearing a slot in a live shadow page. If that page has been freed and reclaimed by a victim kernel object, WRITE_ONCE(*sptep, SHADOW_NONPRESENT_VALUE) at [5] writes into the victim’s memory. On x86-64, SHADOW_NONPRESENT_VALUE = BIT_ULL(63) = 0x8000000000000000.

What Makes the Primitive Tricky

Several properties of this write primitive make it difficult to weaponize for arbitrary code execution: Fixed write value. The value written is always 0x8000000000000000 on x86-64 — it cannot be chosen by the attacker. There is no way to encode a useful pointer, size, or flag value directly. The attacker must find a kernel object layout where this specific 64-bit constant, written at a specific offset, transitions the object into a state that leads to control flow hijack or privilege escalation. Attacker controls only the slot (offset). The target address is parent_pte, which is a pointer into the freed spt page. The guest controls which SPTE slot within that 4 KB page holds the orphaned parent pointer — i.e., the offset within the reclaimed object. The base address of the reclaimed page is determined by which slab or buddy allocation fills the vacancy. Cross-cache constraint. The freed spt page (a 4 KB order-0 page used as a shadow page table) is returned to the buddy allocator, not to a slab. It must be reclaimed by an allocation from the right slab cache — one whose objects are 4 KB or whose allocation path draws from the buddy directly. This cross-cache dependency makes the heap layout non-trivial and potentially dependent on the guest VM’s memory configuration. RAM quota sensitivity. Depending on the target kernel object and how it is allocated, the exploit strategy may require a specific guest memory size to ensure the desired allocation path is taken. This means a publicly released full escape may need to handle different cloud instance sizes differently. Per-architecture strategy. Because Intel EPT and AMD NPT reach the vulnerable code through different call stacks and have different surrounding object layouts, the cross-cache target selection and the heap manipulation sequence differ between the two architectures. The AMD-specific full escape is empirically somewhat easier to construct.

Cross-Cache Attack Structure

The general exploit structure for the full escape follows the cross-cache use-after-free pattern:
  1. Trigger the role-mismatch reuse as in the DoS path to free the spt page.
  2. Suppress the KVM_BUG_ON_DATA_CORRUPTION path (or time the exploit to race past it) to keep the host alive long enough for the next step.
  3. Perform heap feng shui from the guest to ensure the freed 4 KB page is reclaimed by a chosen victim kernel object.
  4. Trigger drop_parent_pte to execute WRITE_ONCE(*parent_pte, 0x8000000000000000) into the victim.
  5. Use the corrupted victim object state to achieve a further primitive — typically an arbitrary read, a pointer overwrite, or a type confusion — that enables code execution at L0 ring 0.
The full escape exploit is planned for release “in the very distant future” per the researcher (Hyunwoo Kim, @v4bel). The public repository currently contains only the DoS PoC (poc.c). Do not expect a near-term release.

No QEMU Dependency

Because the entire exploit operates in-kernel — the module runs in L1 guest kernel context, and the vulnerability fires in L0’s kvm.ko — the full escape does not require QEMU to be the VMM. Any hypervisor stack that uses in-kernel KVM on x86 is affected, including custom VMM implementations used by major cloud providers. This significantly broadens the attack surface compared to traditional QEMU escape vulnerabilities that exploit userspace emulation code.

Impact Summary

PathRequirementOutcome
DoS (public PoC)Race win — single successful window entryImmediate host kernel panic; all tenant VMs terminated
Full escape (unreleased)Race win + cross-cache heap control + suitable victim objectRoot code execution on L0 host; full compromise of host and all guests

Build docs developers (and LLMs) love