# Inside the Sandbox: How pwn Escaped Google kvmCTF (Before the Watershed)

Stale rmap entry in KVM shadow MMU survives page free, dereferenced via VMFUNC recycle pressure, yielding guest-to-host OOB read on Google kvmCTF.

- Payout: Research
- Severity: high
- Program: Linux kernel
- Credited: PWNAI Research
- Disclosed: 2026-09-28
- CVE: CVE-2026-46113
- Write-up quality: 9/10
- Original: https://pwn.ai/blog/kvmescape
- Library page: https://bugbountylibrary.com/r/blog%3Apwn-ai-blog-kvmescape
- Tags: domain:oss impact:data-leak impact:privesc kind:research platform:research sev:high tech:linux technique:chained technique:incomplete-fix technique:source-review vuln:memory-corruption vuln:race-condition

**Target:** KVM shadow MMU (arch/x86/kvm/mmu/mmu.c) on Linux 6.1.74 host
**Root cause:** mmu_set_spte() handles noslot/MMIO PFN before dropping an existing shadow-present SPTE, so the old SPTE's rmap entry is never removed; the shadow page is later freed leaving a dangling rmap pointer.
**Steps:**
1. Build nested VMX L1 hypervisor with EPT12 leaf for GPA X mapping RAM page A read-only (STALE_INITIAL_RW=0).
2. L2 executes `0f 78 07` (VMREAD [rdi], rax) with rax=VMCS field 0x2006 (VM_EXIT_MSR_STORE_ADDR) holding MMIO EPTE 0xfebf0033, rdi aliasing EPT12 PT[64]; KVM's kvm_write_guest_virt_system skips page-track write.
3. L2 stores through old read-only translation; fault re-walks mutated EPT12, hits noslot early-return in mmu_set_spte(), installs MMIO SPTE over present one, leaves rmap[A] stale.
4. Rotate 8 EPTP roots via INVEPT/VMFUNC to retire poisoned tree; MMIO zap path clears slot without removing rmap entry, shadow page freed.
5. L2 touches 49,152 GPAs at 2 MiB stride (base 0x1000000000) to pressure KVM shadow-MMU allocations; HLT, L1 grooms 8,192 accounted pages.
6. VMFUNC selector 1 switches to recycle view; map 1,002 aliases of page A to exceed RMAP_RECYCLE_THRESHOLD=1000, triggering kvm_zap_all_rmap_sptes walk into stale pointer.
7. Call hypercall 102; KASAN oracle returns per-boot token.
**How found:** Source audit of mmu_set_spte() branch ordering noticed noslot early-return precedes shadow-present SPTE drop; predicted mmu_spte_clear_track_bits() reads *sptep before clearing, so first KASAN effect is a read. After 20 failed allocator models, instrumented mmu_spte_clear_track_bits() (50,175 calls) and saw a 1,001-entry rmap clear near MMU zap, revealing the target was KVM-owned shadow memory, not guest pages.
**Exploitation:** L1 nested hypervisor uses emulated VMREAD memory-destination write to silently retarget its own EPT12 leaf to MMIO, poisons KVM's shadow rmap via the noslot early-return, retires roots to free the shadow page, then forces KVM's rmap recycle walk (1,002 aliases > threshold 1000) to dereference the freed SPTE pointer, producing a host OOB read that unlocks the kvmCTF flag.
**Escalation:** Replaced DMA/host-userspace writer precondition with a guest-reachable emulated VMREAD writeback; used VMFUNC EPTP switching to rotate roots and enter recycle view without rewriting the leaf.
**Insight:** When a UAF target is KVM/host-owned shadow memory, guest-page sprays won't reclaim it; instrument the consumer function to count invocations and watch rmap/zap traces to identify the real allocator class, then drive allocation pressure through sparse GPA strides that force per-region page tables.
