Blog

Januscape: how one guest process crashes the entire physical host through KVM

Januscape_CVE-2026-53359
CVE / Linux / Security

Januscape: how one guest process crashes the entire physical host through KVM

You rented an ordinary VPS from a cloud provider. Inside, you have root, because it’s your VM, and you administer it however you want. You even enabled nested virtualization to run Docker containers with their own VMs inside for testing. Everything is isolated by the hypervisor — what could go wrong? The problem is that a few lines of code inside your own VM can crash not just it, but the entire physical server — along with every other tenant on the same machine. Not through a bug in Docker, not through a QEMU vulnerability. Through a bug in the Linux kernel itself, in KVM code that has been traveling across distributions since 2010.

This is CVE-2026-53359, named Januscape — a use-after-free in KVM/x86 shadow MMU (Memory Management Unit). The bug works identically on Intel and AMD, which is unusual in itself: vulnerabilities of this severity are usually tied to a single architecture. The published PoC reliably triggers a host panic. A second, non-public exploit also exists — according to researcher Hyunwoo Kim (@v4bel), it achieves full code execution with root privileges on the host, but its release has been postponed indefinitely. Red Hat rated the vulnerability as Important, CVSSv3 7.0, CWE-825 (use-after-free). NVD hasn’t assigned its own score yet — but that doesn’t mean the bug isn’t serious: since April 15, 2026, NIST has fundamentally limited NVD enrichment for new CVEs to three priority categories (CISA KEV, US federal software, critical software under EO 14028), and for everything else, NVD simply doesn’t calculate its own CVSS score if a CNA (Red Hat, in this case) has already provided one. Waiting for an official NVD score in this situation is pointless — it won’t come.

WHAT SHADOW MMU IN KVM IS

To make a VM run fast, modern Intel and AMD processors can hardware-translate guest memory addresses into host physical addresses — this is Intel EPT and AMD NPT, a second translation layer built directly into silicon. A typical KVM host on modern hardware relies on exactly this: guest page tables aren’t touched at all, and the hardware EPT/NPT does all the work through what’s called the TDP MMU.

But there’s a catch: nested virtualization. If inside your VM (L1) you run another VM (L2) — which is exactly how Docker works with nested containers, CI/CD test labs, any VM-in-VM scenario — the hardware EPT/NPT can only handle one level of translation. The second level, the one L1 built for L2, has to be emulated in software by the physical host (L0). This is exactly what shadow MMU exists for in KVM — legacy code that manually mirrors the guest’s page tables into a separate struct kvm_mmu_page structure. This is old, complex code that’s barely used on modern hosts — except in nested virtualization scenarios, where it’s unavoidable.

HOW THE BUG WORKS

Inside shadow MMU there’s a function called kvm_mmu_get_child_sp() — it decides whether an existing shadow page can be reused for a new table node, or whether a new one needs to be created. Before the patch, the decision was made using a single criterion: does the gfn (guest frame number) of the already-linked shadow page match the gfn currently needed. If it matches, the function returns -EEXIST and reuses the existing page.

The problem is that a shadow page has another attribute besides gfn — role, the page’s type. There are direct pages (direct=1) — the ones KVM creates when it splits a large 2MB page into 4KB pages directly. And there are indirect pages (direct=0) — the ones that actually mirror the guest’s page table. In the normal shadow MMU fetch path, both situations arise for the same gfn: first an indirect page might be needed (a guest table), then a direct page (a split large page), or vice versa. The old code in kvm_mmu_get_child_sp() compared only gfn and ignored role. That means if a direct page was already linked at a given sptep, and the fetch requests an indirect page for the same gfn, the function still returns a “match” and hands back a page with the wrong role.

This reuse breaks the lifetime and parent-pointer tracking of the shadow page. A state arises where one shadow page is already freed while another still holds a pointer into it — an orphaned parent pointer. When shadow MMU later cleans up this pointer via drop_parent_pte()mmu_spte_clear_no_track(), it writes a fixed constant (SHADOW_NONPRESENT_VALUE, which on x86-64 is 0x8000000000000000) into memory that may already have been reused for a different kernel object by this point. This is a classic use-after-free write: the attacker doesn’t control the value being written, but does control which slot (offset) within the freed page gets the write.

HOW IT’S EXPLOITED

The published PoC is a kernel module loaded inside the guest VM (L1). The module directly, by hand, builds a nested guest system L2 through raw VMX (Intel) or SVM (AMD), bypassing QEMU entirely — the whole trick happens inside the host’s in-kernel KVM, without a single request to the userspace VMM. The module constructs a specific memory geometry: the same physical page is used simultaneously as a leaf of a large 2MB page and as a page table page — this is the key precondition for triggering the bug.

Next, a race condition kicks off. One thread continuously flips a PDE entry between “this is a large 2MB page” and “this is a page table” for the same gfn. In parallel, other threads continuously run L2 code, triggering page faults that go through this same PDE. There’s a window between the moment L0 commits the new PDE value and the moment the old shadow link is actually torn down via kvm_page_track_write. If a fault lands exactly in that window, kvm_mmu_get_child_sp() gets called with the new role while the sptep still has a child of the old role linked — and the bug fires. Time to win the race ranges from seconds to minutes, but the outcome is deterministic: sooner or later, the window opens.

WHAT HAPPENS NEXT — DEPENDS ON CONFIGURATION

From here there’s a fork. The public PoC takes the simpler, more reliable path — not a write into someone else’s memory, but self-detected data corruption. The reused direct page has no shadowed_translation field, so instead of the real leaf gfn, it computes one as sp->gfn + index. When a leaf with a different, real gfn gets installed through this page, the mismatch first shows up as a WARN_ONCE (“gfn mismatch under direct page”) — still a harmless warning in the log at this point. The real hit comes later: when that same leaf is removed, the kernel looks for the corresponding entry in rmap using the computed gfn, doesn’t find it (because it was installed under a different, real gfn), and the built-in KVM_BUG_ON_DATA_CORRUPTION check inside pte_list_remove() treats this as data corruption. On systems where CONFIG_BUG_ON_DATA_CORRUPTION=y is set — the default on distributions like RHEL — the matching BUG_ON immediately crashes the kernel. This path doesn’t even require the freed memory to be reused by any specific object — the kernel’s own internal integrity check does the work, not actual exploitation.

The second, heavier path is that same non-public full host-takeover exploit. It requires the freed shadow page to be reused as a guest kernel object where the fixed write of 0x8000000000000000 lands in a meaningful field — a classic cross-cache use-after-free exploitation, just applied to KVM MMU structures. The bug itself is identical on Intel and AMD, since it lives in shared KVM logic rather than in hardware differences between the platforms — but the final exploitation steps in the public PoC differ between vendors, visible in the separate code paths for amd=1 in the module itself.

THE REAL ATTACK SCENARIO

Take a typical multi-tenant cloud host: GCP, AWS, any VPS provider that gives customers root inside their VM and doesn’t block nested virtualization. An attacker rents a single ordinary instance — no special privileges required, beyond root inside their own VM, which any customer of such a hosting already has. They load the kernel module (which first requires unloading the stock kvm_intel or kvm_amd, since the PoC works directly with raw VMX/SVM state), run the race, and within seconds to minutes, the physical host running their VM panics. Every other VM on that same physical machine goes down with it — neighbors on the hosting the attacker may not even know exist.

Here’s what that looks like in practice — an actual panic log obtained by the researcher on RHEL running 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

First comes that gfn mismatch warning on one CPU and PID, then a moment later — an already-fatal kernel BUG in pte_list_remove on a different CPU and PID, and the host goes down.

There’s also a less interesting, secondary vector — local privilege escalation via /dev/kvm. On distributions like RHEL, this device is world-writable by default (mode 0666), meaning any unprivileged local user can theoretically turn this vulnerability into reliable LPE to root on the host itself, without any VM involved at all. The researcher himself rates this scenario as secondary compared to guest-to-host escape on public clouds — a far less interesting target. Still, tightening permissions on /dev/kvm is worth doing regardless, it’s a separate, easy fix.

TIMELINE

The vulnerable code appeared in commit 2032a93d66fa, dated August 1, 2010 — the kernel 2.6.36 era. It survived unnoticed for roughly 16 years of KVM development since then. This isn’t the first time a similar problem was found in this same code: commit 0cb2af2ea66ad (“KVM: x86: Fix shadow paging use-after-free due to unexpected GFN”, CVE-2026-46113, published May 28, 2026) already closed a similar shadow paging mismatch — that bug was triggered by a PDE mapping change from outside the guest followed by memslot removal, where rmap_remove() couldn’t find the entry created after that change. That patch fixed the gfn mismatch case. But, as the official changelog for commit 81ccda30b4e8 explicitly acknowledges, a similar hole remained where the gfn matches but the role doesn’t — a case kvm_mmu_get_child_sp() never checked. The May patch closed one half of the gfn/role mismatch problem, and Januscape closes the other half of that same logical gap.

On June 12, 2026, researcher Hyunwoo Kim (@v4bel) sent detailed vulnerability information and a working exploit to [email protected]. The very next day, June 13, KVM maintainers Paolo Bonzini and Sean Christopherson discussed how to handle the patch, and Paolo wrote the fix. On June 17, the patch was posted on lore.kernel.org for testing. On June 19, commit 81ccda30b4e8 was merged into mainline. On July 1, the information was sent to the linux-distros mailing list with a five-day embargo. On July 4, CVE-2026-53359 was officially assigned, and that same day, fixed stable kernel versions were released: 7.1.3, 6.18.38, 6.12.95, 6.6.144, 6.1.177, 5.15.211, and 5.10.260. After the embargo ended, on July 6, the information was published on the oss-security mailing list along with the full technical writeup.

Notice the gap between June 19 (patch already in mainline) and July 4 (only then did a CVE number appear) — nearly two weeks during which the fix was already publicly visible in the kernel’s git history, but without a CVE tied to it, it was practically impossible to find through ordinary vulnerability tracking by number.

HOW IT SURVIVED FOR 16 YEARS

It’s not that anyone was looking for this bug and missed it — almost no one was specifically looking in this exact spot. Shadow MMU in KVM is a legacy path that’s barely exercised on most modern hosts at all: hardware EPT/NPT covers the vast majority of workloads, and the shadow code quietly sits idle. It only triggers under one very specific condition — nested virtualization, where L1 itself becomes a hypervisor for L2. That’s not the most common scenario on “ordinary” production servers, but it’s entirely ordinary on cloud platforms, where customers run CI/CD, test environments, or their own nested VMs.

On top of that, the bug itself isn’t a trivial bounds check — it’s a subtle logic error: comparing on one struct field (gfn) instead of two (gfn and role). No static analyzer catches something like this unless it specifically understands the semantics of KVM MMU role fields. It took someone who understood the fine details of shadow paging and deliberately fuzzed or manually audited this exact, rarely-exercised branch of code.

WHY IT’S BEING FOUND RIGHT NOW

Januscape isn’t an isolated case, but part of a series: this is already the third major kernel vulnerability disclosure from Hyunwoo Kim in about two months. In May, he published Dirty Frag (CVE-2026-43284 / CVE-2026-43500) — a page-cache write bug chain that gives deterministic root on most distributions and continues the same class of vulnerabilities as Dirty Pipe and Copy Fail. In June — ITScape (CVE-2026-46316), the first publicly demonstrated guest-to-host escape on KVM/arm64 via a race condition in the virtual interrupt controller. Januscape closes out the x86 side of the same research effort.

One of the drivers behind this wave is Google’s kvmCTF, a bug bounty program specifically for guest-to-host escapes in KVM, with a prize of up to $250,000 for a full escape. Januscape was originally submitted as a zero-day through kvmCTF. The program deliberately incentivizes exactly this kind of research — deep audits of rarely-exercised but high-impact code paths like shadow MMU, rather than surface-level fuzzing of popular APIs.

WHY THIS MATTERS

The scale of the impact depends on who you are. If you’re a cloud provider or hosting company that gives customers root inside their VM and doesn’t block nested virtualization by default, you’re exposed to one customer taking down the physical host along with every neighbor on it. That’s a reputation risk and an SLA risk at the same time: customers who did nothing wrong lose their services because of someone else’s VPS on the same machine. If you’re a customer of such a hosting provider and rely on hypervisor isolation as a security boundary, that boundary is leaky exactly in configurations where nested virtualization is enabled, regardless of how well your own VM is secured internally.

Worth noting separately: the bug lives in pure KVM kernel code, not in QEMU or any userspace VMM. That means it’s equally relevant to open-stack clouds (KVM + QEMU + libvirt) and to proprietary hyperscaler solutions built on their own virtualization layered over in-kernel KVM — GCP and AWS both use KVM at their core. The bug’s universality across Intel and AMD further widens the attack surface — usually you only have to close one architecture, here it’s both at once.

THE FIX

The patch is essentially a single line in kvm_mmu_get_child_sp(): a check was added to the shadow page reuse condition — spte_to_child_sp(*sptep)->role.word == role.word — meaning the page is now only reused if both gfn and role match. Commit 81ccda30b4e8 landed in mainline on June 19, 2026, and made it into stable branches 7.1.3, 6.18.38, 6.12.95, 6.6.144, 6.1.177, 5.15.211, and 5.10.260, released July 4, 2026. Red Hat tracked the issue as CVE-2026-53359 (Bugzilla 2497033), Important, CVSSv3 7.0, with fixes for RHEL 6-10; AlmaLinux shipped test builds for the 8, 9, and 10 branches.

Updating the kernel is standard for your distribution:

# Debian/Ubuntu
sudo apt update && sudo apt upgrade && sudo reboot

# RHEL/AlmaLinux/Rocky
sudo dnf update kernel && sudo reboot

Don’t rely on uname -r alone — distribution backports often change the version number differently than upstream. It’s more reliable to check your kernel package’s changelog for a mention of CVE-2026-53359 or commit 81ccda30b4e8:

# Debian / Ubuntu
apt-get changelog linux-image-$(uname -r)

# RHEL / AlmaLinux
rpm -q --changelog kernel-$(uname -r)

If an immediate kernel update isn’t possible, there’s a temporary measure that completely removes the attack vector for untrusted guests: disable nested virtualization on the host.

# Intel
echo "options kvm_intel nested=0" | sudo tee /etc/modprobe.d/kvm-nested.conf
sudo rmmod kvm_intel
sudo modprobe kvm_intel

# AMD
echo "options kvm_amd nested=0" | sudo tee /etc/modprobe.d/kvm-nested.conf
sudo rmmod kvm_amd
sudo modprobe kvm_amd

Note: rmmod will refuse to unload the module if VMs are currently running on the host — they keep the module busy. In that case, either stop the guest machines before unloading, or schedule this as maintenance with a full host reboot, which in practice is more reliable than manually unloading the module under load.

This is strictly a temporary measure — it only protects against guests that don’t need nested virtualization. If your infrastructure requires it (for example, you sell customers the ability to run VMs inside VMs), the only real solution is the kernel patch. Additionally, on distributions where /dev/kvm is world-writable (0666) by default, it’s worth tightening permissions to reduce local escalation risk:

ls -l /dev/kvm
sudo chmod 0660 /dev/kvm

BOTTOM LINE

If you’re a cloud provider or run multi-tenant KVM hosting with nested virtualization enabled, updating the kernel to 7.1.3 / 6.18.38 / 6.12.95 / 6.6.144 / 6.1.177 / 5.15.211 / 5.10.260 (or the equivalent distribution backport carrying commit 81ccda30b4e8) isn’t optional: the cost of delay is every VM on affected physical hosts going down at once. If patching right now isn’t possible, disable nested virtualization for untrusted guests today — it takes five minutes.

If you simply rent a VPS from a large provider, no direct action is required from you — responsibility for patching the hypervisor lies with the provider, but it’s worth asking them about patch status, especially if you use nested virtualization yourself for CI/CD or test environments. And in any case: /dev/kvm with 0666 permissions is a separate issue, unrelated to the kernel update, that’s worth closing on any server where you run KVM locally.

Leave your thought here

Your email address will not be published. Required fields are marked *