https://bugs.dpdk.org/show_bug.cgi?id=2003
Bug ID: 2003
Summary: vhost double-fetch leading to overflow in
virtio_net_ctrl_pop
Product: DPDK
Version: unspecified
Hardware: All
OS: All
Status: UNCONFIRMED
Severity: normal
Priority: Normal
Component: vhost/virtio
Assignee: [email protected]
Reporter: [email protected]
Target Milestone: ---
Group: security
Report date: 2026-04-24
Reported by: Don Salvatore Nero <[email protected]>
Dear Thomas, Maxime, and the DPDK Security Team,
1. Executive Summary
A double-fetch vulnerability exists in virtio_net_ctrl_pop() in the DPDK
vhost library. The function reads descs[desc_idx].len twice from
guest-controlled shared memory — once to calculate allocation size
(data_len), and once during the actual memcpy() loop — with a malloc() call
in between. A malicious guest can modify the descriptor length between the
two reads, causing memcpy() to write far beyond the allocated buffer,
resulting in a heap buffer overflow with attacker-controlled length and
content.
2. Vulnerability Analysis
Step 1 — Guest-controlled shared memory
>From vhost.c lines 482-488 (confirmed from real code):
vq->desc = (struct vring_desc *)(uintptr_t)vhost_iova_to_vva(dev, vq,
vq->ring_addrs.desc_user_addr, // ← address provided by guest &size,
VHOST_ACCESS_RO);
vq->desc is a direct pointer into guest-mapped shared memory. The guest
owns and can modify this memory at any time.
When VRING_DESC_F_INDIRECT is set, the indirect table also comes from guest
memory:
descs = (struct vring_desc *)(uintptr_t)vhost_iova_to_vva(dev, cvq,
desc_iova, &desc_len, VHOST_ACCESS_RO);
No local copy is made of the descriptor table. descs points directly to
guest memory throughout the entire function.
Step 2 — Phase 2: First read of len to compute data_len
>From virtio_net_ctrl.c lines 70-105 (confirmed from real code):
while (1) { desc_len = descs[desc_idx].len; // ← READ #1 from guest memory
desc_iova = descs[desc_idx].addr; n_descs++; if (descs[desc_idx].flags &
VRING_DESC_F_WRITE) { // handle ack descriptor ... } else { if
(ctrl_elem->desc_ack) { goto err; } data_len += desc_len; // ← accumulate
size using READ #1 } if (!(descs[desc_idx].flags & VRING_DESC_F_NEXT))
break; desc_idx = descs[desc_idx].next; }
At this point, data_len = sum of all readable descriptor lengths from READ
#1.
Step 3 — malloc() using READ #1 value
>From virtio_net_ctrl.c line 124 (confirmed from real code):
ctrl_elem->ctrl_req = malloc(data_len); // ← allocated based on READ #1 if
(!ctrl_elem->ctrl_req) { goto err; } ctrl_req = (uint8_t
*)ctrl_elem->ctrl_req;
The buffer is allocated with the size computed from READ #1.
malloc() is an external function call with side effects — the compiler
cannot cache descs[].len across this call. It must re-read from memory in
Phase 3.
Step 4 — Phase 3: Second read of len with NO bounds check
>From virtio_net_ctrl.c lines 144-168 (confirmed from real code):
while (!(descs[desc_idx].flags & VRING_DESC_F_WRITE)) { desc_len =
descs[desc_idx].len; // ← READ #2 from guest memory desc_iova =
descs[desc_idx].addr; desc_addr = vhost_iova_to_vva(dev, cvq, desc_iova,
&desc_len, VHOST_ACCESS_RO); if (!desc_addr || desc_len <
descs[desc_idx].len) { goto free_err; } memcpy(ctrl_req, (void
*)(uintptr_t)desc_addr, desc_len); // ↑ copies desc_len bytes — from READ
#2, not READ #1 // NO check: copied_so_far + desc_len <= data_len ctrl_req
+= desc_len; // ↑ advances pointer by READ #2 value — no upper bound if
(!(descs[desc_idx].flags & VRING_DESC_F_NEXT)) break; desc_idx =
descs[desc_idx].next; }
There is NO check of total bytes copied against data_len.
The only check present is:
if (!desc_addr || desc_len < descs[desc_idx].len)
This only verifies that the IOVA translation succeeded — it does NOT check
that the copy stays within the allocated buffer.
Step 5 — The Race Window
[Thread: DPDK host] [Thread: Malicious Guest] Phase 2: READ #1:
descs[0].len = 4 data_len = 4 malloc(4) ← buffer of 4 bytes allocated
WRITE: descs[0].len = 65535 Phase 3: READ #2: descs[0].len = 65535
memcpy(ctrl_req, src, 65535) ← writes 65535 into 4-byte buffer ctrl_req +=
65535 ← pointer far past end of buffer
The race window is the time between data_len += desc_len (Phase 2) and
memcpy(..., desc_len) (Phase 3), which is widened by the malloc() call
between them.
4. Why the Existing Locks Do NOT Prevent This
>From virtio_net_ctrl_handle():
rte_rwlock_read_lock(&dev->cvq->access_lock);
vhost_user_iotlb_rd_lock(dev->cvq);
These locks are intra-process locks — they only prevent concurrent access
between host threads. They provide zero protection against the guest
modifying the shared memory pages, because:
The guest runs in a separate address space (VM or VDUSE consumer process)
The shared memory is mapped via mmap(MAP_SHARED) — the guest has direct
write access
No lock can prevent the guest from writing to its own memory
5. Exploit Scenario
Prerequisites:
DPDK vhost enabled with VDUSE backend (rte_vduse_device_create())
VIRTIO_NET_F_CTRL_VQ enabled (default in VIRTIO_NET_SUPPORTED_FEATURES)
Guest has standard virtio-net driver
Steps:
1. Guest sets up descriptor chain with small len:
desc[0]: addr=X, len=4, flags=NEXT, next=1 desc[1]: addr=Y, len=1,
flags=WRITE (ack descriptor)
2. Host enters Phase 2:
Reads desc[0].len = 4 → data_len = 4
Calls malloc(4) → allocates 4-byte buffer
3. Guest immediately overwrites desc[0].len:
desc[0].len = 0x10000; // 65536 bytes
4. Host enters Phase 3:
Reads desc[0].len = 65536
vhost_iova_to_vva() succeeds (guest controls the memory mapping)
memcpy(ctrl_req, src, 65536) — writes 65536 bytes into 4-byte heap buffer
Heap corruption with attacker-controlled length and content
Impact of Heap Overflow:
Since the DPDK process typically runs with elevated privileges (direct NIC
access, hugepages, DMA), a successful heap overflow leads to:
ImpactDescriptionRCE in host processControl heap layout → overwrite
function pointers or vtablesHost escapeDPDK process has privileged access —
compromise extends to host OSMemory corruptionCorrupt adjacent heap objects
affecting other guest connectionsPrivilege escalationDPDK process
capabilities grant access to hardware resources
6. Code Proof Summary
LocationCodeSignificancevhost.c:482vq->desc = vhost_iova_to_vva(...)descs
points to guest shared memoryvirtio_net_ctrl.c:~75desc_len =
descs[desc_idx].lenREAD #1 from guest memoryvirtio_net_ctrl.c:~80data_len
+= desc_lenAllocation size from READ
#1virtio_net_ctrl.c:~124malloc(data_len)Buffer allocated from READ
#1virtio_net_ctrl.c:~148desc_len = descs[desc_idx].lenREAD #2 from guest
memoryvirtio_net_ctrl.c:~154memcpy(ctrl_req, ..., desc_len)Copy using READ
#2 — no bounds checkvirtio_net_ctrl.c:~156ctrl_req += desc_lenPointer
advance — no upper bound
7. Note on Proof of Concept
I was unable to produce a live PoC due to environment constraints — my
analysis VPS (Ubuntu 22.04) encountered build issues when attempting to
configure a VDUSE server environment for live exploitation testing.
However, the vulnerability is demonstrated entirely through source code
analysis:
The double-fetch pattern is unambiguous in the code
The absence of a cumulative bounds check in Phase 3 is confirmed
The guest-controlled nature of descs[] memory is confirmed via
vhost_iova_to_vva() chain
The race window is real and is widened by the malloc() call between phases
8. Recommended Fix
Option 1 — Copy descriptors locally before processing:
// Make a local copy of the entire descriptor chain ONCE struct vring_desc
local_descs[VIRTIO_MAX_INDIRECT_DESCS]; memcpy(local_descs, descs, n_descs
* sizeof(struct vring_desc)); // Use local_descs in BOTH Phase 2 and Phase
3
Option 2 — Add cumulative bounds check in Phase 3:
uint64_t copied = 0; while (!(descs[desc_idx].flags & VRING_DESC_F_WRITE))
{ desc_len = descs[desc_idx].len; // ADD THIS CHECK: if (copied + desc_len
> data_len) { VHOST_CONFIG_LOG(dev->ifname, ERR, "Descriptor overflow
detected"); goto free_err; } desc_addr = vhost_iova_to_vva(...);
memcpy(ctrl_req, desc_addr, desc_len); ctrl_req += desc_len; copied +=
desc_len; // track total ... }
Option 3 — Use atomic reads:
// Read len once atomically and reuse uint32_t cached_len =
__atomic_load_n(&descs[desc_idx].len, __ATOMIC_ACQUIRE);
--
You are receiving this mail because:
You are the assignee for the bug.