[PATCH 13/14] vhost: fix indirect descriptor table double fetch

Maxime Coquelin maxime.coquelin at redhat.com
Wed Oct 14 17:00:48 CEST 2026


The virtio descriptor length field was read multiple times from
guest-controlled memory, creating a TOCTOU (time-of-check-time-of-use)
vulnerability. A malicious guest could exploit this by modifying
desc->len between reads, causing the host to allocate a buffer of
one size but iterate over a different number of descriptors, leading
to an out-of-bounds read.

Affected functions:
- fill_vec_buf_packed_indirect(): desc->len read for allocation at
  one point, then re-read for loop bound calculation
- fill_vec_buf_split(): vq->desc[idx].len re-read for allocation
  after being captured in a local variable
- vdpa.c indirect descriptor handling: same pattern

Fix by capturing the descriptor length once in a local variable
and using that for all subsequent operations.

Also fix a memory leak in fill_vec_buf_packed_indirect() where
free_ind_table() was not called on the map_one_desc() error path.

CVE: CVE-2026-93683
Bugzilla ID: 2046

Fixes: d0c24508e1f ("vhost: add support for non-contiguous indirect descs tables")
Fixes: 2f3225a7d69 ("vhost: add vector filling support for packed ring")

Reported-by: Naseeb Dangi <naseebdangi100 at gmail.com>
Acked-by: Kevin Traynor <ktraynor at redhat.com>
Signed-off-by: Maxime Coquelin <maxime.coquelin at redhat.com>
---
 lib/vhost/vdpa.c       |  8 +++++---
 lib/vhost/virtio_net.c | 22 +++++++++++++---------
 2 files changed, 18 insertions(+), 12 deletions(-)

diff --git a/lib/vhost/vdpa.c b/lib/vhost/vdpa.c
index bc2dd8d2e1..ff3b4379aa 100644
--- a/lib/vhost/vdpa.c
+++ b/lib/vhost/vdpa.c
@@ -195,7 +195,9 @@ rte_vdpa_relay_vring_used(int vid, uint16_t qid, void *vring_m)
 			return -1;
 
 		if (vq->desc[desc_id].flags & VRING_DESC_F_INDIRECT) {
-			dlen = vq->desc[desc_id].len;
+			uint64_t desc_len = vq->desc[desc_id].len;
+
+			dlen = desc_len;
 			nr_descs = dlen / sizeof(struct vring_desc);
 			if (unlikely(nr_descs > vq->size))
 				return -1;
@@ -209,11 +211,11 @@ rte_vdpa_relay_vring_used(int vid, uint16_t qid, void *vring_m)
 			if (unlikely(!desc_ring))
 				return -1;
 
-			if (unlikely(dlen < vq->desc[desc_id].len)) {
+			if (unlikely(dlen < desc_len)) {
 				vhost_user_iotlb_rd_lock(vq);
 				idesc = vhost_alloc_copy_ind_table(dev, vq,
 						vq->desc[desc_id].addr,
-						vq->desc[desc_id].len);
+						desc_len);
 				vhost_user_iotlb_rd_unlock(vq);
 				if (unlikely(!idesc))
 					return -1;
diff --git a/lib/vhost/virtio_net.c b/lib/vhost/virtio_net.c
index ff4613df68..8bdddedccf 100644
--- a/lib/vhost/virtio_net.c
+++ b/lib/vhost/virtio_net.c
@@ -775,7 +775,9 @@ fill_vec_buf_split(struct virtio_net *dev, struct vhost_virtqueue *vq,
 	*desc_chain_head = idx;
 
 	if (vq->desc[idx].flags & VRING_DESC_F_INDIRECT) {
-		dlen = vq->desc[idx].len;
+		uint64_t desc_len = vq->desc[idx].len;
+
+		dlen = desc_len;
 		nr_descs = dlen / sizeof(struct vring_desc);
 		if (unlikely(nr_descs > vq->size))
 			return -1;
@@ -787,13 +789,13 @@ fill_vec_buf_split(struct virtio_net *dev, struct vhost_virtqueue *vq,
 		if (unlikely(!descs))
 			return -1;
 
-		if (unlikely(dlen < vq->desc[idx].len)) {
+		if (unlikely(dlen < desc_len)) {
 			/*
 			 * The indirect desc table is not contiguous
 			 * in process VA space, we have to copy it.
 			 */
 			idesc = vhost_alloc_copy_ind_table(dev, vq,
-					vq->desc[idx].addr, vq->desc[idx].len);
+					vq->desc[idx].addr, desc_len);
 			if (unlikely(!idesc))
 				return -1;
 
@@ -898,29 +900,29 @@ fill_vec_buf_packed_indirect(struct virtio_net *dev,
 	uint16_t i;
 	uint32_t nr_descs;
 	uint16_t vec_id = *vec_idx;
-	uint64_t dlen;
+	uint64_t desc_len = desc->len;
+	uint64_t dlen = desc_len;
 	struct vring_packed_desc *descs, *idescs = NULL;
 
-	dlen = desc->len;
 	descs = (struct vring_packed_desc *)(uintptr_t)
 		vhost_iova_to_vva(dev, vq, desc->addr, &dlen, VHOST_ACCESS_RO);
 	if (unlikely(!descs))
 		return -1;
 
-	if (unlikely(dlen < desc->len)) {
+	if (unlikely(dlen < desc_len)) {
 		/*
 		 * The indirect desc table is not contiguous
 		 * in process VA space, we have to copy it.
 		 */
 		idescs = vhost_alloc_copy_ind_table(dev,
-				vq, desc->addr, desc->len);
+				vq, desc->addr, desc_len);
 		if (unlikely(!idescs))
 			return -1;
 
 		descs = idescs;
 	}
 
-	nr_descs =  desc->len / sizeof(struct vring_packed_desc);
+	nr_descs = desc_len / sizeof(struct vring_packed_desc);
 	if (unlikely(nr_descs >= vq->size)) {
 		free_ind_table(idescs);
 		return -1;
@@ -936,8 +938,10 @@ fill_vec_buf_packed_indirect(struct virtio_net *dev,
 		*len += dlen;
 		if (unlikely(map_one_desc(dev, vq, buf_vec, &vec_id,
 						descs[i].addr, dlen,
-						perm)))
+						perm))) {
+			free_ind_table(idescs);
 			return -1;
+		}
 	}
 	*vec_idx = vec_id;
 
-- 
2.56.0



More information about the dev mailing list