Zapscape — KVM/x86 Shadow-MMU Recursive-Zap Guest-to-Host Escape (CVE-2026-64561)
Published: 2026-08-09 • Researcher: Hyunwoo Kim (@v4bel)
- Severity
- High
- CVE
- CVE-2026-64561
- Category
- binary
- Affected product
- Linux kernel, KVM/x86 shadow-MMU (nested EPT/NPT shadowing) — arch/x86/kvm/mmu/mmu.c and arch/x86/kvm/mmu/paging_tmpl.h
- Affected versions
- From commit f95eec9bed76 (2020-07-08) through the fix 2abd5287f083 (2026-07-21). Per NVD the stable fixes land in 6.6.148, 6.12.101, 6.18.42, 7.1.6 and 7.2-rc5. Public demo runs against Linux 7.1.3.
- Disclosed
- 2026-08-09
- Patch status
- Patched
Tags
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-64561
- https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=2abd5287f083
- https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=f95eec9bed76
- https://lore.kernel.org/all/20260721102829.313226-1-pbonzini@redhat.com/
- https://www.openwall.com/lists/oss-security/
- https://github.com/V4bel/Zapscape
- https://github.com/V4bel/Januscape
Archive entry
intelseclab/poc-archiveOn this page
Metadata
| Field | Value |
|---|---|
| Date Added | 2026-08-09 |
| Last Updated | 2026-08-06 |
| Author / Researcher | Hyunwoo Kim (@v4bel) |
| CVE / Advisory | CVE-2026-64561 |
| Category | binary |
| Severity | High |
| CVSS Score | 8.8 (CVSSv3.1, kernel.org CNA: AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H) |
| Status | Patched |
| Tags | linux-kernel, kvm, x86, shadow-mmu, nested-virtualization, svm, npt, ept, guest-to-host-escape, vm-escape, use-after-free, CWE-416, cross-cache, kaslr-bypass, usermode-helper, virtualization |
| Related | pocs/binary/2026-07-27_cve-2026-46316-itscape-kvm-arm64-vgic-its-escape/ (same author; ITScape is entry one of the “KVM Escape Trilogy”: ITScape / Januscape CVE-2026-53359 / Zapscape). Zapscape shares the shadow-MMU root cause family with Januscape but is a distinct bug. |
Affected Target
| Field | Value |
|---|---|
| Software / System | Linux kernel, KVM/x86 shadow-MMU (nested EPT/NPT shadowing) — arch/x86/kvm/mmu/mmu.c and arch/x86/kvm/mmu/paging_tmpl.h |
| Versions Affected | From commit f95eec9bed76 (2020-07-08) through the fix 2abd5287f083 (2026-07-21). Per NVD the stable fixes land in 6.6.148, 6.12.101, 6.18.42, 7.1.6 and 7.2-rc5. Public demo runs against Linux 7.1.3. |
| Language / Platform | C, Linux kernel, x86_64, KVM (in-kernel emulation — independent of QEMU userspace). Demo targets AMD SVM/NPT; Intel is affected under an extra EPT constraint (see below). |
| Authentication Required | Yes — requires guest kernel (L1) privilege inside an otherwise unprivileged guest VM. On distros where /dev/kvm is world-writable (e.g. RHEL, 0666) it also serves as a local privilege escalation for an unprivileged host user. |
| Network Access Required | Local only — no network needed; entirely guest-VM-local nested-virtualization activity against the host KVM. The bundled qemu.sh boots with -nic none (no guest networking at all). |
Summary
Zapscape (CVE-2026-64561) is a use-after-free in the KVM/x86 shadow MMU that lets a guest which uses nested virtualization escape to the host and run commands as the host kernel (root). Using guest-side actions alone, an attacker makes KVM recursively zap a shadow page that is still pinned as a root during MMU page-quota reclaim; KVM then keeps servicing the fault under a root that has already become invalid, an invalid child enters the active MMU page list, the same list link is attached to two lists at once and then freed, and a dangling link plus a post-free write result. Because the bug lives in in-kernel KVM (not QEMU), it is triggered independently of the VMM emulation code and threatens multi-tenant x86 clouds — including providers that ship their own virtualization stack — that expose nested virtualization to untrusted guests.
The public PoC is explicitly reproduction / demonstration code, not a weaponised exploit. It reproduces the full chain on top of QEMU TCG with no guest networking; porting it to a real cloud would require moving the L1 actions into a guest kernel module and re-tuning to the host kernel kconfig and VMM memory backend.
Vulnerability Details
Root Cause
x86 KVM shadows nested EPT/NPT with software struct kvm_mmu_page objects. In the vulnerable kernel the shadow page-fault path takes the MMU lock, checks whether the current root has gone stale, secures the shadow-page quota, and then fetches — in that order:
| |
The decisive ordering flaw: is_page_fault_stale() at [5] runs before make_mmu_pages_available() at [6], but quota reclaim at [6] can itself recursively invalidate the current root, and [7] fetches again without re-checking. Compounding it, the recursive zap of a nested-TDP child (while unlinking its parent SPTE) has no child->root_count guard:
| |
The top-level quota walker does skip pinned roots (if (sp->root_count) continue;), so it never picks the pinned root X directly. But once one kvm_mmu_page is simultaneously a child of one nested page table and the root of another (the guest aliases the same GFN/level/role in two page-table hierarchies so mmu_alloc_root() finds the existing header and bumps root_count), reclaim reaches X recursively through its parent. __kvm_mmu_prepare_zap_page() then list_del()s X from the active list and marks sp->role.invalid = 1, but X is not freed because a root reference remains. The still-running fault, having already passed the stale check, keeps fetching under invalid X; kvm_mmu_child_role() copies the parent role without clearing invalid, so the new child C inherits the invalid bit yet is inserted into active_mmu_pages like a normal member. C now has an invalid role while sitting on the active list, so a later rootless prepare uses list_add() (not list_move()) to also place C on the invalid list — double list membership. The commit path frees C via kmem_cache_free(mmu_page_header_cache, sp) while the active list still points at C.link, leaving a dangling link. The next list_add() at the active-list head then dereferences freed C and performs a post-free write of a kernel pointer into the freed object.
Attack Vector
Entirely guest-driven from L1 kernel context, no host/QEMU cooperation, no network:
- Alias one shadow page as both child and pinned root. On AMD, L1 builds a level-2 child X under a long-mode NPT and then makes a PAE NPT root use the same GFN/level/access/guest-mode role;
mmu_alloc_root()finds X and incrementsroot_count. On Intel the same alias requires that both EPT page-walk length 4 and 5 be exposed to L1 (a level-4 PWL5 child reused as a level-4 PWL4 root); AMD has no such constraint. - Drive quota reclaim to invalidate X while a fault fetches under it, producing the invalid child C on the active list, then free C via the two-stage active-list/invalid-list double membership. The PoC carefully fixes fault order, MMU-header slab refill, and write-flooding-triggered prepares so the freed 184-byte header slab is returned to the page allocator on the same vCPU.
- Two guest-driven cross-caches. A 518 MiB
guest_memfdregion (GUEST_MEMFD_FLAG_MMAP | GUEST_MEMFD_FLAG_INIT_SHARED) is faulted so the just-freed SLUB page (from the dedicatedkvm_mmu_page_headercache, 184-byte objects, 22 per order-0 slab) is reallocated as a guest-authored folio. Stage 1 locates freed C and theactive_mmu_pageshead; Stage 2 obtains an independent guest-controlled page N. - KASLR leak. Rebuilding C inside a guest page turns its
list_head/hash_linkfields into a controlled__list_del()write primitive. The chain corruptsstruct kvm.n_max_mmu_pages(to stop further quota reclaim), then interprets thepm_notifierregion as a fake MMU header to leak the address ofrcu_pm_notify_nb, from which the guest computes the KASLR slide and re-deriveslog_wait,srcu_delay_timer,call_usermodehelper_exec_work, etc. - Usermode-helper trigger. In page N the guest builds a
wait_queue_entry(callbacksrcu_delay_timer) and an overlappingsubprocess_infowhose work function iscall_usermodehelper_exec_work, path/bin/sh, modeUMH_WAIT_PROC. Linking the wait entry into the globallog_waitand issuing MONITOR + an x2APIC LOWEST ICR causes the kernel printk wake to walklog_wait, queue the work, and a kworker runs/bin/sh -c "umask 022; : > /Zapscape"aftercommit_creds(prepare_kernel_cred(...)).
Impact
Full guest-to-host escape with host kernel-level code execution, triggerable by a guest-rooted tenant with no host cooperation and no network. On a successful run the host filesystem gains /Zapscape, owned by uid 0, mode 0644 — a benign proof marker standing in for arbitrary root code execution on the host. The same primitive can panic the host (DoS every co-tenant VM) or take over the host and all guests on it (RCE). Because the flaw is in in-kernel KVM, QEMU-side sandboxing (seccomp, privilege separation) does not mitigate it. On distros where /dev/kvm is 0666, an unprivileged host user can use the same bug as a local root escalation (and more stably, since host-side VMM ioctls are then available).
Environment / Lab Setup
L0 (escape target): Linux 7.1.3 + KVM_AMD, x86_64 with AMD SVM/NPT, emulated by QEMU TCG
(-cpu EPYC,+svm,+npt -accel tcg,thread=multi). This is the "host" the
escape lands on. Running under TCG makes the lab safe and disposable.
L1: the guest the PoC creates via /dev/kvm. Aliases one shadow page as both child and
pinned root (long -> PAE), then escalates the UAF into L0 kernel code-exec.
L2: the guest L1 VMRUNs; its memory touches drive L0 quota reclaim -> recursive zap.
Guest config: 518 MiB guest_memfd (MMAP | INIT_SHARED), 2 vCPUs (BSP + AP), KVM irqchip + x2APIC.
Tools: gcc (static, pthread), BusyBox initramfs, QEMU v9.2.0 or later.
Demo kconfig (like Ubuntu): CONFIG_DEBUG_LIST / CONFIG_LIST_HARDENED /
CONFIG_BUG_ON_DATA_CORRUPTION all DISABLED.Setup Steps
Safety: build and read only. Never run this against a host you are not authorised to test. The escape lands on the host running KVM.
| |
The bundled launcher is short and explicit about the safe, no-networking demo posture:
| |
Proof of Concept
See
poc.c(~6,337 lines, a self-contained/dev/kvmVMM + guest code emitter),qemu.sh,kconfig, andassets/write-up.md(~50 KB technical writeup) in this folder — mirrored byte-for-byte from V4bel/Zapscape. The upstream README is preserved asupstream-README.md.
Step-by-Step Reproduction
Boot the vulnerable L0 host kernel under QEMU TCG
Shell script1./qemu.sh bzImage initramfs.cpio.gzRun the PoC from the unprivileged (uid 65534) guest shell — it opens
/dev/kvm, builds the L1 VM and its L2, and drives the whole chain with guest instructions after the firstKVM_RUN:Shell script1./pocVerify the escape — success creates
/Zapscapeon the HOST (L0) filesystem, owned by root:Shell script1ls -la /Zapscape
Exploit Code
The alias step that turns one header into both a child and a pinned root (mmu_alloc_root() re-finds X and bumps root_count):
| |
The controlled write primitive: once C is rebuilt inside a guest_memfd page, KVM deleting its manipulated list_head performs two attacker-chosen pointer writes —
| |
The final payload built in page N (harmless proof marker — creates an empty root-owned file, no shell, no download):
| |
Expected Output
/$$$$$$$$ /$$$$$$ /$$$$$$$
|_____ $$ /$$__ $$| $$__ $$
/$$/ | $$ \ $$| $$ \ $$
/$$/ | $$$$$$$$| $$$$$$$/
/$$/ | $$__ $$| $$____/
/$$/ | $$ | $$| $$
/$$$$$$$$| $$ | $$| $$
|________/|__/ |__/|__/
[+] /Zapscape created by the target KVM host kernel (owner uid=0, mode=0644).
[+] exploit completed - verify with: ls -la /Zapscape
zapscape(uid=65534)$ ls -la /Zapscape
-rw-r--r-- 1 root root 0 Jul 29 05:27 /ZapscapeScreenshots / Evidence
assets/demo.gif— upstream animated demo of the full run ending in/Zapscapeon the host.assets/write-up.md— the complete ~50 KB technical root-cause and exploit-chain writeup (mirrored unmodified).
Detection & Indicators of Compromise
Remediation
| Action | Detail |
|---|---|
| Patch | Apply mainline commit 2abd5287f083 (2026-07-21). It moves the stale-root check to run after make_mmu_pages_available(); if quota reclaim invalidated the current root, the fault restarts with RET_PF_RETRY instead of continuing to map/fetch. Fix is present in Linux 6.6.148, 6.12.101, 6.18.42, 7.1.6, and 7.2-rc5. |
| Workaround | None short of the patch — the bug is purely guest-triggerable in-kernel KVM with no QEMU dependency, so VMM sandboxing does not help. Enabling CONFIG_LIST_HARDENED / CONFIG_BUG_ON_DATA_CORRUPTION converts the exploit into a host DoS rather than a clean escape, but does not close the bug. Until patched, avoid exposing nested virtualization to untrusted guests; on Intel, not exposing both EPT PWL4 and PWL5 to L1 removes the alias precondition. |
| Verification | Confirm the host kernel contains 2abd5287f083 (arch/x86/kvm/mmu/mmu.c + paging_tmpl.h) via git log or the distro changelog / stable branch version. |
References
- CVE Record — CVE-2026-64561 (NVD) — CVSS 8.8 (kernel.org CNA), published 2026-08-04.
- Fix commit — 2abd5287f083 (mainline, 2026-07-21)
- Introducing commit of the affected range — f95eec9bed76 (2020-07-08)
- Patch posting on lore (Paolo Bonzini, 2026-07-21)
- oss-security disclosure thread (posted 2026-08-06 after embargo)
- Public PoC — V4bel/Zapscape
- Related — Januscape (CVE-2026-53359), same shadow-MMU family
- Related — ITScape (CVE-2026-46316), arm64 KVM escape, in this archive
Notes
Verified this session by reading the full source directly (not on the strength of the README): the entire ~6,337-line poc.c VMM/guest-emitter and the ~50 KB assets/write-up.md root-cause writeup were read, and the root cause, patch commit (2abd5287f083), affected range, fixed versions, and disclosure timeline were cross-corroborated against the NVD record (CVSS 8.8, published 2026-08-04) and the upstream write-up. Both mirrored files were verified byte-identical against a second, independent fresh clone via diff; poc.c sha256 5e76ee0ee92f458bf109b469adbf90567c14f1a50e0b48f8daec80cab8378cf8, assets/write-up.md sha256 45a4c2e328dd1d371574d3afb8fdf9b6748f34070e131240078ba4a88c4d788b, qemu.sh sha256 e3a51a513e00607e97952caf5666d54d1fa2a2d75b125282bb8bc1c8042b87bf.
Malware screen — clean. No obfuscated payload that gets executed, no remote downloader, no credential exfiltration, no miner, no unexpected committed binaries (only the expected assets/demo.gif, assets/tux.png, and three sym-*.svg images), and no setup.py/Makefile/install-time side effects. poc.c contains no socket/connect/getaddrinfo/curl/wget calls and no embedded URLs; its only “command execution” is the intended, benign host-side proof /bin/sh -c "umask 022; : > /Zapscape" (creates an empty file). qemu.sh uses -accel tcg and -nic none, i.e. software emulation with no guest networking — consistent with the author’s statement that this is reproduction/demonstration code, not a weaponised or cloud-ready exploit.
Author track record: Hyunwoo Kim (@v4bel on X, V4bel on GitHub since 2018, ~380 followers) disclosed responsibly — reported to security@kernel.org on 2026-07-11, Sean Christopherson wrote the fix on 2026-07-13, Paolo Bonzini posted it to lore and it merged to mainline on 2026-07-21, submitted to linux-distros@vs.openwall.org under a 5-day embargo on 2026-08-01, CVE-2026-64561 assigned 2026-08-04, and the exploit + this writeup were published to oss-security on 2026-08-06 after the embargo expired. Zapscape is the third entry in the author’s “KVM Escape Trilogy” (ITScape / Januscape / Zapscape); it shares the shadow-MMU area with Januscape but is a separate bug with a different root cause.
WARNING — copycat / fake-installer repository. A separate account, aarif450/Zapscape (account created 2026-04-15, 0 followers), hosts a byte-identical poc.c (verified this session: same sha256 5e76ee0ee92f458bf109b469adbf90567c14f1a50e0b48f8daec80cab8378cf8) but replaces the README with a large “⬇️ DOWNLOAD ZAPSCAPE NOW” button linking to an empty /releases page, plus fake-installer social-engineering copy (“a friendly tool … a digital shield … Click the blue download button”) and a matching aarif450.github.io lure page. This is textbook fake-installer staging: legitimate research code used as cover while the download button is the intended payload channel. There is no released artifact behind the button today, but the staging is set up to serve one. Use only the V4bel upstream (github.com/V4bel/Zapscape); do not download anything from the aarif450 repository or its GitHub Pages site.
| |