Matryoshka dolls on a dark server rack shelf: the smallest doll, bright red, climbs out through a crack in the outer shell

CVE-2026-53359, "Januscape": a 16-year-old use-after-free in KVM's x86 shadow MMU. Root in a guest, launch a nested guest, corrupt the host kernel. The public PoC panics the host; the researcher claims an unreleased variant achieves host code execution.

Our first reaction was the same as everyone else's: "we don't run nested virt, we're fine." Then we checked.

The CVE, briefly

Januscape (found by Hyunwoo Kim via Google's kvmCTF) is a use-after-free in the shadow paging code KVM uses to emulate a nested guest's MMU. An attacker with root inside a VM loads KVM in the guest, starts an L2 guest, and makes crafted page-directory modifications that leave stale rmap entries pointing at a freed shadow page. The bug has been in the kernel since 2.6.36, roughly sixteen years, and hits Intel and AMD alike.

The public PoC "only" panics the host: every VM on the node down at once, triggered from inside any single guest. If the code-execution variant ever leaks, it's a full guest-to-host escape. Red Hat scored it CVSS 7.0 ("Important"); if you host other people's workloads, that score understates it.

"We don't use nested virt" is not a statement about your config

The exploit needs two preconditions on the host side:

  1. Nested virtualization enabled on the KVM module: kvm_intel/kvm_amd loaded with nested=Y.
  2. The guest's vCPU exposing vmx/svm, so the guest can enter the nested code path at all.

The trap: the first one is a kernel default, and the second comes free with a very common choice. nested=Y has been the module default since v4.20 (December 2018), per the kernel's own nested-guests documentation; kvm_amd had it on years earlier. Unless you explicitly wrote options kvm_intel nested=N into /etc/modprobe.d/, it's on. Proxmox's own CPU-type default (x86-64-v2-AES) does not expose vmx/svm. But on clusters homogeneous enough to allow it, plenty of setups run cpu: host for maximum performance and flags, and that passes vmx/svm straight through to the guest.

We checked every hypervisor across our three datacenters:

  • Every host: nested=Y. Module default, never overridden, never decided.
  • A non-trivial number of VMs: cpu: host, so vmx/svm exposed.
  • Zero of those VMs needing nested virt. No nested consumers, no explicit +vmx/+svm args anywhere.

So "we don't use nested virt" was true as a statement about our workloads and irrelevant as a statement about our attack surface. The path was open on every node, for nothing.

# Check your own hosts in one loop
for h in $HOSTS; do ssh root@$h 'echo "$(hostname): $(uname -r)";
  for m in kvm_intel kvm_amd; do
    [ -f /sys/module/$m/parameters/nested ] && \
      echo "  $m nested: $(cat /sys/module/$m/parameters/nested)";
  done'; done

# VMs exposing vmx/svm (on one node per Proxmox cluster)
grep -H "^cpu:" /etc/pve/nodes/*/qemu-server/*.conf | grep -Ei "host|vmx|svm"

Where all that cpu: host came from

The inventory had a second lesson in it. Those cpu: host entries weren't an accident, and they didn't creep in. We put them there ourselves: our provisioning scripts set cpu: host explicitly, for maximum performance and full CPU flags. A defensible choice at the time, made with performance in mind. Nobody re-examined it as attack surface, because why would you. It's just a CPU type.

We're now walking the flagged VMs back to a named CPU model (x86-64-v3 fits our hardware) one maintenance window at a time, alongside the cluster upgrades already in progress. If you provision from templates instead of scripts, check those too: a template with cpu: host baked in reintroduces it with every clone.

(cpu: host has bitten us before, for a different reason: it's also what makes cross-vendor AMD↔Intel live migration a hazard. Two reasons now to prefer a named CPU model unless a VM demonstrably needs more.)

What we did: make the default a decision

The fix hierarchy for Januscape looks like this:

  1. The real fix: patched kernels. Credit to Proxmox here: fixed kernels (proxmox-kernel-6.17 6.17.13-15, and 7.0.14-4 for the opt-in 7.0 series) were out within three days of disclosure. Requires the reboot dance, one quorum-safe node at a time. One hazard to check before that dance: there are early forum reports of exactly these kernels failing to boot on legacy-BIOS/GRUB hosts (GRUB stops at attempt to read or write outside of disk 'hd0'); UEFI hosts are unaffected. Know each host's boot mode before you reboot into the new kernel ([ -d /sys/firmware/efi ] means UEFI).
  2. The hard off switch: nested=0 on the KVM module. Fully removes the attack path. The catch: it only takes effect when the module reloads, which means stopping every VM on the host. That's the same disruption as rebooting into the fixed kernel, so it's worthless as an interim mitigation…
  3. Per-VM CPU type changes: switching untrusted guests off cpu: host. Works, but needs a stop/start per VM and changes guest-visible CPUID. A scalpel, not a rollout mechanism.

…but option 2 being useless as an interim fix doesn't make it useless. It makes it a permanent policy. We were never going to allow nested virtualization; we'd just never said so to the kernel. So we wrote it down where it counts:

# /etc/modprobe.d/kvm-nested-off.conf - deployed via Ansible to all hypervisors
options kvm_intel nested=N
options kvm_amd nested=0

Deployed to every hypervisor ahead of the kernel upgrade, inert until each node's reboot. That way the patched kernel and the policy activate together, in one maintenance window instead of two. And because the playbook reads back /sys/module/*/parameters/nested, re-running it after the reboot night doubles as the validator: every host prints nested disabled NOW: true/false and REBOOT NEEDED, so a straggler can't hide.

One subtlety worth knowing: the protection is the module parameter, not guest CPUID. A VM live-migrated onto a nested=0 host with a stale vmx flag in its CPUID still can't do anything with it; VMXON just takes a #UD. That's straight from the KVM source: with nested=0, every VMX-instruction VM exit lands in a handler that injects #UD, whatever the guest's CPUID says. So mid-rollout migrations between patched-policy and not-yet-rebooted hosts stay safe, and you don't have to chase down every guest's CPUID before you're protected.

The sibling CVE hiding in the changelog

Small detour that almost caused a false alarm: the 6.17 kernel changelog names two CVEs (Januscape plus CVE-2026-46113, a second shadow-paging use-after-free), while the 7.0 kernel changelog names only Januscape. Missing backport? No. 46113 had already landed upstream in an earlier 7.0 point release, so the new build inherits it via its base version and the changelog only cites what's new.

Check the distro security tracker's fixed-versions before assuming a changelog omission means unpatched. Changelogs record what changed in this build, not the cumulative security state.

Lessons

  • A kernel default is a decision someone else made for you. nested=Y shipped enabled for years; unless you overrode it, you're running a feature you never chose. This isn't the first CVE to teach us that: a lot of things ship enabled that most environments never need. Inventory your module parameters like you inventory your open ports.
  • "We don't use X" is not "X is off." The question is never whether your workloads use a feature. It's whether an attacker inside your guest can.
  • Performance choices are security choices. We wrote cpu: host into our own provisioning for the extra flags. It was also attack surface, and nobody looked at it that way until a CVE forced the question.
  • Turn incident response into standing policy. The mitigation that wasn't worth it as a stopgap was worth it forever: nested=0 now protects us from the next nested-path CVE too, not just this one.
  • A playbook that reads back state is its own validator. Deploy inert, reboot, re-run: the same code that made the change proves it took effect.

Sixteen years is a long time for a bug to sit in a code path most fleets never consciously enabled. Some people genuinely need nested virt. The rest of us had it on without ever making that choice. The patch closes this hole; the policy closes the category. When the next KVM nested-virt CVE drops (and at sixteen years a pop, there's more in there), our answer is already deployed.

References

Your VMs run on hypervisors somebody has to patch. On ours, that's us. Talk to us about managed hosting.

More from the coffee bar →