Vghost: Beyond the Privilege Boundary
TL;DR: Corvus AI discovered a high-severity memory corruption vulnerability in Linux's vhost virtualization acceleration subsystem (CVE-2026-74580, CVSS 8.8). The defect spans nearly a decade of kernel releases from Linux 4.11 through 7.2, impacting distributions including Ubuntu, RHEL, Amazon Linux, Azure Linux, and Oracle Linux.
The vulnerability provides dual exploitation vectors: host local privilege escalation and virtual machine escape. On the host, it yields deterministic, race-free arbitrary kernel read and write primitives without heap grooming or kernel structure layout dependencies, enabling unprivileged users to bypass KASLR and escalate to root. In virtualized environments with a virtual IOMMU, an untrusted guest can trigger the same stale-cache condition via
VIRTIO_F_RING_RESET, inducing the host kernel to write out-of-bounds into the host QEMU process heap.
The issue is tracked as CVE-2026-74580. The kernel.org CNA rates it 8.8 High (CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H). The upstream fix is de845981da67.
We previously published our writeup on SCTPhantom, which demonstrated a container escape attack vector. This post presents another attack vector, virtual machine escape, and it reminds us that trusted security isolation boundaries may need to be reconsidered.
Demo — Vghost: an unprivileged user escalates to root.
1. The bug
vq->meta_iotlb[] caches the vhost_iotlb_map backing each vring metadata region (desc / avail / used). iotlb_access_ok() short-circuits on a cache hit:
if (vhost_vq_meta_fetch(vq, addr, len, type))
return true;
and vhost_vq_meta_fetch() performs no range check:
return (void __user *)(uintptr_t)(map->addr + addr - map->start);
addr and map->start are both 64-bit and the difference is unconstrained, so the pointer is only meaningful while addr really is inside map.
The cache is reset on VHOST_IOTLB_UPDATE / VHOST_IOTLB_INVALIDATE, on device-IOTLB init, and on vq reset — but not by VHOST_SET_VRING_ADDR / VHOST_SET_VRING_NUM. With a device IOTLB attached those ioctls are accepted on a live vq and deliberately defer address validation to prefetch time (vq_access_ok() returns true early because the addresses are GIOVAs). Once the cache is populated, that deferred validation never runs: vq_meta_prefetch() hits the stale entry and returns true, and vhost_vq_meta_fetch() keeps translating the new address through the old mapping.
The bug is the composition of two commits that are each individually reasonable:
f88949138058(v4.11, 2017-02-28) addedvq->meta_iotlb[]and the early return iniotlb_access_ok();0210a8db2aec(v5.9, 2020-10-03) moved GIOVA validation out of the ioctl and into prefetch.
On the VHOST_SET_VRING_ADDR path, the second commit removed the ioctl-time check — while the cache keeps the prefetch-time check from ever running.
The translated pointer reaches vhost_put_used() → vhost_copy_to_user() → __copy_to_user() (read direction: vhost_get_desc() → __copy_from_user()). On x86-64 these bottom out in raw_copy_to_user() / raw_copy_from_user() (stac(); rep movsb; clac()), which do no access_ok() and no user-pointer masking. The access happens exactly at the address the stale translation produced.
2. Exploitation
The stale cache covers desc, avail, and used alike, so the single bug yields two independent primitives on the same primed device.
2.1 Arbitrary write
One completed descriptor makes vhost write an 8-byte vring_used_elem { .id = <head>, .len = 0 } — two attacker-chosen bytes (head < vq->num ≤ 0x8000) followed by six zero bytes. Re-pointing the used ring between kicks (VHOST_SET_VRING_ADDR again) places each store at an arbitrary address. used_user_addr must be 4-byte aligned, but the effective store alignment is (map->addr - map->start) mod 4, and both terms are attacker-chosen — pick the IOTLB map's uaddr and any byte alignment is reachable. Writing a string front-to-back gives full arbitrary-write control.
2.2 Arbitrary read
Re-pointing desc_user_addr instead makes vhost fetch desc[0] from an arbitrary kernel address. desc[0].addr — the target's first 8 bytes — is then treated as a data-buffer IOVA. It misses the IOTLB, and vhost reports the miss with an IOTLB message whose iova field carries those 8 bytes straight back to userspace:
repoint desc_user_addr = <target> (stale cache -> no re-validation)
→ kick -> vhost reads desc[0] from <target>
→ desc[0].addr = *(u64 *)<target>
→ that value is used as an IOVA, misses the IOTLB
→ VHOST_IOTLB_MISS message returns the 8 bytes
This is an 8-byte arbitrary kernel read.
2.3 KASLR bypass
The read primitive recovers the KASLR slide from gate 0 of the IDT, which lives at the fixed CPU entry area address 0xfffffe0000000000. Gate 0 is the entry for asm_exc_divide_error. Reading its first 8 bytes yields the low and middle offset fields plus the code-segment selector; the handler is reconstructed by combining them with the canonical high bits, and the slide is the difference from the link-time symbol:
handler = reconstruct(IDT[0])
KASLR_slide = handler - link_time_asm_exc_divide_error
The selector is checked to equal the kernel code segment (0x0010), the reconstructed handler is checked against its expected alignment, and the slide is applied to link-time symbols such as modprobe_path.
This vulnerability affects the vhost-net and vhost-vsock frontends. No privilege is required beyond opening a device node: several distributions ship /dev/vhost-vsock and /dev/vhost-net mode 0666 (the systemd default udev rules), while others use 0660 root:kvm. In addition to the upstream Linux 7.2.0-rc4 build, we reproduced the complete local-user-to-root chain on multiple distribution kernels.
3. Cross-distribution validation
We used the same generic x86-64 exploit on all five targets. The exploit resolved asm_exc_divide_error and modprobe_path from a matching, pre-provided System.map, recovered the live KASLR slide from IDT gate 0, obtained kernel read/write, and restored the original /proc/sys/kernel/modprobe value before handing over a real UID-zero shell.
| Distribution | Tested kernel | /dev/vhost-vsock | Result |
|---|---|---|---|
| Ubuntu 26.04 LTS | 7.0.0-28-generic | 0660 root:kvm | KVM group user uid=1000 → root |
| RHEL 9.8 | 5.14.0-687.5.3.el9_8.x86_64 | 0666 root:kvm | unprivileged user → root |
| Amazon Linux 2023.12.20260817 | 6.1.180-225.360.amzn2023.x86_64 | 0666 root:kvm | unprivileged user → root |
| Microsoft Azure Linux 3.0 | 6.6.150.1-1.azl3 | 0666 root:kvm | unprivileged user → root |
| Oracle Linux 10.2 | 6.12.0-204.92.4.2.el10uek | 0666 root:kvm | unprivileged user → root |
Representative success output was consistent across the five systems:

=== vhost stale IOTLB metadata cache -> root ===
[*] Step 1: KASLR bypass via IDT[0]
[+] runtime modprobe_path @ <KASLR-adjusted address>
[*] Step 3: overwriting modprobe_path
[*] Step 4: triggering
[+] root code execution confirmed: uid=0(root) gid=0(root) groups=0(root)
[+] dropping real-root shell (uid 0)...
After each run, the exploit exited successfully, /proc/sys/kernel/modprobe was back to /sbin/modprobe, and the temporary root helper was absent. The distribution-specific difference was therefore exposure, not primitive quality: Ubuntu limited the node to kvm, while the tested RHEL 9.8, Amazon Linux 2023, Azure Linux, and Oracle Linux 10.2 images exposed it by default as mode 0666 with world access to an ordinary user.
4. Exploitability
The primitives follow from the cache invariant, not from a race or a particular heap layout. After the IOTLB cache is populated, each VHOST_SET_VRING_ADDR followed by a kick produces exactly one kernel read or one kernel write at the translated address. A single process and a short ioctl sequence are sufficient; there is no grooming stage and no retry loop.
The primitives do not walk a kernel object by offsetof(), so they do not have to be retuned when structure layouts change. They only need long-lived symbols (asm_exc_divide_error, modprobe_path). The vulnerable path itself is unchanged from v5.9 through 7.2-rc4; moving to another build is mostly resolving those symbols in a different System.map.
5. VM escape: triggering from inside a guest
The exploit in Section 2 assumes an attacker running locally on the host who can open /dev/vhost-* device nodes directly. However, vhost is designed specifically to provide datapath acceleration for virtual machine devices by exchanging descriptors directly over shared virtual queues (vrings). Because it directly serves the guest, the same stale-cache flaw can also be triggered from within a guest using standard virtio operations, yielding a VM-escape vector.
demo -- Vghost: Guest in a virtual machine controls host RIP.
5.1 Trigger path
When a virtio device is placed behind a virtual IOMMU and iommu_platform is enabled (negotiating VIRTIO_F_ACCESS_PLATFORM), the vring contains GIOVAs rather than guest physical addresses (GPAs). Before accessing them, vhost must translate each GIOVA into a host virtual address (HVA) via the device IOTLB. Reads and writes are executed by a vhost kernel thread borrowing the QEMU process's address space (kthread_use_mm): after resolving the HVA from the IOTLB, it calls __copy_from_user() to read the descriptor table and avail ring, and __copy_to_user() to write the used ring. In the QEMU process, guest RAM is backed by a large mmap region; normally, used-ring writes land entirely within this interval.
A guest cannot directly invoke VHOST_SET_VRING_ADDR, but it has a legitimate mechanism to make QEMU issue the ioctl: VIRTIO_F_RING_RESET allows a guest to reset and re-enable an individual virtqueue:
- A normal packet transmission or reception populates the mapping into
meta_iotlb[USED]; - The guest writes
queue_reset, repoints the used ring (queue_device) to an unmapped new GIOVAR2, and writesqueue_enable. In response, QEMU entersvhost_virtqueue_restart()and issuesVHOST_SET_VRING_ADDRon the existing file descriptor without clearing the metadata cache; - The guest kicks the queue. Because the cache is not invalidated,
vq_meta_prefetch()hits the stale entry and returnstrue, causingvhost_vq_meta_fetch()to extrapolate linearly using the old mapping:
HVA = old_map->addr + (R2 − old_map->start) = rambase + gpa + (R2 − R)
When R2 − R is sufficiently large, the resulting HVA exceeds the guest RAM mapping and lands in the QEMU process heap. The subsequent __copy_to_user() writes used-ring elements into host memory that was never authorized for the guest via the IOMMU.
5.2 Overwriting host QEMU memory and hijacking its control flow
The out-of-bounds write targets the host QEMU (VMM) process's own memory, with the destination selected by the guest through R2. Guest RAM is just one region of QEMU's address space; a sufficiently large R2 − R takes the write beyond that region and into QEMU's own heap. The written values come from used-ring metadata and are sufficient to overwrite an 8-byte pointer with a guest-chosen value.
QEMU's QOM object model maintains a method table (class) for each device type, containing function pointers invoked during device operations. After the guest overwrites one of these function pointers with a chosen value, QEMU's next indirect call through it transfers the instruction pointer (RIP) to that value. In testing, the host QEMU reached RIP=0x41, and targets could also be constructed within QEMU's own image address range, such as 0x555555fe0000. At this point, an ordinary process inside a VM has gained control of the host VMM's control flow; the same primitive can also reliably crash the host QEMU process.
This vector requires vhost to operate in device-IOTLB mode, with the virtio device positioned behind a vIOMMU and iommu_platform enabled.
6. Fix and timeline
The fix resets the metadata cache in vhost_vring_set_num_addr(), pushing reconfigured addresses back through iotlb_access_ok()'s slow path:
BUG();
}
+ /*
+ * The metadata cache holds the IOTLB mapping that backed the previous
+ * desc/avail/used addresses and vring size, both of which are being
+ * replaced here. iotlb_access_ok() takes a cache hit as proof that the
+ * region was validated, so the stale entries have to go.
+ */
+ __vhost_vq_meta_reset(vq);
+
mutex_unlock(&vq->mutex);
The change is scoped to drivers/vhost/vhost.c and matches the three existing reset sites. Two smaller staleness windows (2 bytes and ~256 KB, both staying inside the caller's own mapping) are deliberately left for later non-security patches.
The CVE record marks the issue affected from Linux 4.11 and identifies 5.10.265, 5.15.216, 6.1.183, 6.6.152, 6.12.104, 6.18.45, 7.1.9, and 7.2 as the first fixed releases in those stable series. The tested Amazon Linux 6.1.180 and Azure Linux 6.6.150 kernels therefore predate the corresponding upstream stable fixes; a vendor backport can, of course, fix a distribution kernel without changing its upstream-version base. Currently, both Amazon Linux and Oracle Linux have published security advisories and patched the vulnerability in their latest releases (see the Amazon Linux Security Advisory and Oracle Linux Errata).
Mitigation
The definitive remediation is to install a kernel containing the upstream fix (de845981da67) or the corresponding vendor backport, then reboot into that kernel. The first fixed upstream stable releases are 5.10.265, 5.15.216, 6.1.183, 6.6.152, 6.12.104, 6.18.45, 7.1.9, and 7.2.
Until a fixed kernel can be deployed, restrict access to every vhost device, including /dev/vhost-net and /dev/vhost-vsock. Remove world access, limit the owning group to trusted virtualization services, and prevent untrusted containers from receiving these devices through device-cgroup or runtime configuration. Membership in kvm or another device-owning group must be treated as privileged while the vulnerable kernel is running.
| Date | Milestone |
|---|---|
| 2017-02-28 / v4.11 | f88949138058 introduced the O(1) metadata cache |
| 2020-10-03 / v5.9 | 0210a8db2aec deferred GIOVA validation to prefetch |
| 2026-07-31 | Missing VHOST_SET_VRING_ADDR invalidation identified; arbitrary read/write reproduced |
| 2026-08-03 | The fix entered mainline (de845981da67, vhost_vring_set_num_addr() reset) |
| 2026-08-21 | The Linux kernel CVE team announced CVE-2026-74580 |
| 2026-08-24 | Full LPE verified on Ubuntu 26.04, RHEL 9.8, Amazon Linux 2023, Azure Linux 3.0, and Oracle Linux 10.2 |
| 2026-08-25 | kernel.org CNA CVSS v3.1 score published: 8.8 High |
References
- CVE-2026-74580 record
- CVE-2026-74580 — NVD
- Linux kernel CVE announcement
- Amazon Linux Security Advisory — CVE-2026-74580
- Oracle Linux Errata — ELSA-2026-500248
- Mainline fix:
de845981da67 f88949138058— vhost: introduce O(1) vq metadata cache0210a8db2aec— vhost: Don't call access_ok() when using IOTLB
Tencent Zhuque Lab
Author
