vulnerability reports (virtio-net RSC host heap OOB writes and feature-negotiation bypass)

Imported by the "Security Issuer Importer" bot, on behalf of the original reporter who disclosed via qemu-security list:

  • From: Sven <bestswngs@gmail.com>
  • Date: 25 May, 2026
  • Message ID: <CANgPUi34xN6wxMFRzNm4DsE=rkf8bA0Dgf7fLPSpHo_3=-rBRw@mail.gmail.com>

NOTE: the original reporter can not be copied on this issue so cannot view this ticket while it remains marked confidential. If further information is needed about the disclosure, contact the reporter directly.

NOTE: this issue was one of many (potential) security flaws disclosed in a single message to the qemu-security list. During import each distinct flaw has been filed as a separate issue. Attachments on this issue may still have details of other flaws which are unrelated and should be ignored. The title of this issue declares which specific flaw this is tracking.


QEMU vulnerability reports

Report A: virtio-net RSC host heap OOB writes and feature-negotiation bypass

1. Vulnerability root cause: source code and relevant function context

This report covers three related issues: two host heap out-of-bounds writes in the virtio-net RSC data path, and one virtio-core feature-negotiation bypass that can make the RSC path reachable even when the host did not advertise the RSC feature.

Issue A-1 is in the RSC coalescing path in hw/net/virtio-net.c. When the first candidate packet of a flow is received, virtio_net_rsc_cache_buf() caches the packet:

seg->buf = g_malloc(hdr_len + sizeof(struct eth_header)
    + sizeof(struct ip6_header) + VIRTIO_NET_MAX_TCP_PAYLOAD);
memcpy(seg->buf, buf, size);
seg->size = size;

Later, when another packet from the same flow arrives, virtio_net_rsc_do_coalesce() calls virtio_net_rsc_coalesce4() / virtio_net_rsc_coalesce6(), which eventually reaches virtio_net_rsc_coalesce_data(). That function performs its capacity check using the IP length declared in the cached packet header:

o_ip_len = read_unit_ip_len(o_unit);
...
if ((o_ip_len + n_unit->payload) > chain->max_payload) {
    return RSC_FINAL;
}
...
memmove(seg->buf + seg->size, data, n_unit->payload);
seg->size += n_unit->payload;

The root cause is that the bounds check and the actual write offset use two different quantities that are not required to match:

  • The check uses o_ip_len, which is the attacker-controlled length declared in the first packet's IP header.
  • The write offset uses seg->size, which is the actual on-wire length of the first packet.

virtio_net_rsc_sanity_check4() only checks that ip_len <= size - guest_hdr_len - sizeof(struct eth_header). It does not require the declared IP length to match the actual delivered frame size. An attacker can therefore send a first packet with a small declared ip_len but a large on-wire size. When a second packet with a large payload is coalesced, o_ip_len + payload can still pass the chain->max_payload check, while memmove(seg->buf + seg->size, ...) writes at or past the end of the cached buffer, causing a host heap out-of-bounds write.

Issue A-2 is in the same cache function, virtio_net_rsc_cache_buf(). The cache allocation has a fixed size:

guest_hdr_len + sizeof(eth_header) + sizeof(ip6_header) + VIRTIO_NET_MAX_TCP_PAYLOAD
= 12 + 14 + 40 + 65535
= 65601 bytes

However, memcpy(seg->buf, buf, size) copies the actual frame length supplied by the netdev backend. size can reach NET_BUFSIZE = 4096 + 65536 = 69632. A single oversized RSC-candidate packet can therefore copy 69632 bytes into a 65601-byte allocation, producing an out-of-bounds write of about 4031 bytes. The trailing bytes are attacker-controlled.

Issue A-3 is in virtio feature negotiation. hw/virtio/virtio.c computes a masked feature set in virtio_set_features_nocheck():

bad = virtio_features_andnot(tmp, val, vdev->host_features_ex);
virtio_features_and(tmp, val, vdev->host_features_ex);

But if the device implements set_features_ex, the callback receives the raw, unmasked val:

if (k->set_features_ex) {
    k->set_features_ex(vdev, val);
}
...
virtio_features_copy(vdev->guest_features_ex, tmp);

The virtio-pci write path for VIRTIO_PCI_COMMON_GF calls virtio_set_features_ex(vdev, features) and discards the return value. virtio-net registers vdc->set_features_ex = virtio_net_set_features, and virtio_net_set_features() derives the RSC state directly from the feature argument:

n->rsc4_enabled = virtio_has_feature_ex(features, VIRTIO_NET_F_RSC_EXT) &&
    virtio_has_feature_ex(features, VIRTIO_NET_F_GUEST_TSO4);
n->rsc6_enabled = virtio_has_feature_ex(features, VIRTIO_NET_F_RSC_EXT) &&
    virtio_has_feature_ex(features, VIRTIO_NET_F_GUEST_TSO6);

As a result, a guest can write VIRTIO_NET_F_RSC_EXT even if the host did not advertise it. virtio-net then uses the unmasked feature value and enables the RSC path, broadening the reachability of A-1 and A-2.

2. Vulnerability reproduction steps

For A-1, use attachments-A/virtio-net-rsc-oob-qtest-repro.py:

python3 virtio-net-rsc-oob-qtest-repro.py \
    --qemu /path/to/qemu-system-x86_64 \
    --accel qtest \
    --coalesce-overflow

Reproduction flow:

  1. The script creates a Unix socketpair. One end is passed to QEMU through -netdev socket,fd=...; the other end is kept by the script for frame injection.
  2. The qtest channel initializes a modern virtio-net-pci device, sets up the rx and control virtqueues, and negotiates VIRTIO_F_VERSION_1, VIRTIO_NET_F_GUEST_TSO4, and VIRTIO_NET_F_RSC_EXT.
  3. The script injects packet P1 for a TCP flow. P1 declares a small ip_len, such as 40, but has a large on-wire size, such as 65665 bytes. Its declared TCP payload is 0, so it reaches the RSC pure-ACK-to-data branch.
  4. The script injects packet P2 for the same flow and the same TCP sequence number, with a large payload, such as 20000 bytes.
  5. QEMU enters virtio_net_receive() -> virtio_net_rsc_receive() -> virtio_net_rsc_do_coalesce() -> virtio_net_rsc_coalesce_data(), and triggers the heap-buffer-overflow at memmove(seg->buf + seg->size, data, n_unit->payload).

ASan reports a 20000-byte write in virtio_net_rsc_coalesce_data(), with the destination address located immediately after a 65601-byte allocation.

For A-2, use the same attachment with its default parameters:

python3 virtio-net-rsc-oob-qtest-repro.py \
    --qemu /path/to/qemu-system-x86_64 \
    --accel qtest

Reproduction flow:

  1. The script initializes the virtio-net RSC path.
  2. It injects an Ethernet frame of size NET_BUFSIZE, 69632 bytes, as the first segment of a flow.
  3. virtio_net_rsc_do_coalesce() sees an empty chain and calls virtio_net_rsc_cache_buf(chain, nc, buf, 69632).
  4. virtio_net_rsc_cache_buf() allocates a 65601-byte cache buffer, but memcpy(seg->buf, buf, 69632) writes 69632 bytes into it, causing an approximately 4031-byte host heap out-of-bounds write.

A standalone dynamic PoC for A-3 is not included in the original report. The source-level reproduction is to build or configure virtio-net so that host_features_ex does not include VIRTIO_NET_F_RSC_EXT, then have the guest still write VIRTIO_NET_F_RSC_EXT | VIRTIO_NET_F_GUEST_TSO4 into driver_features. virtio_net_set_features() receives the unmasked feature value and sets n->rsc4_enabled to 1.

ASan crash site for A-1:

==1656050==ERROR: AddressSanitizer: heap-buffer-overflow on address
    0x531000024841
WRITE of size 20000 at 0x531000024841 thread T0
    #0 0x59d811ed3318 in __asan_memmove
    #1 0x59d81230135e in memmove
       /usr/include/x86_64-linux-gnu/bits/string_fortified.h:36:10
    #2 0x59d81230135e in virtio_net_rsc_coalesce_data
       hw/net/virtio-net.c:2307:9
    #3 0x59d8122ffbf1 in virtio_net_rsc_coalesce4
       hw/net/virtio-net.c:2331:12
    #4 0x59d8122ffbf1 in virtio_net_rsc_do_coalesce
       hw/net/virtio-net.c:2401:19
    #5 0x59d8122fbec8 in virtio_net_rsc_receive
       hw/net/virtio-net.c
    #6 0x59d8122fbec8 in virtio_net_receive
       hw/net/virtio-net.c:2677:16

0x531000024841 is located 0 bytes after 65601-byte region
    [0x531000014800,0x531000024801)
allocated by thread T0 here:
    #1 g_malloc
    #2 0x59d8123005b9 in virtio_net_rsc_cache_buf
       hw/net/virtio-net.c:2190:16

SUMMARY: AddressSanitizer: heap-buffer-overflow in __asan_memmove

ASan crash site for A-2:

==1655949==ERROR: AddressSanitizer: heap-buffer-overflow on address
    0x531000024841
WRITE of size 69632 at 0x531000024841 thread T0
    #0 0x5f83bc99fdf7 in __asan_memcpy
    #1 0x5f83bcdcd5e5 in memcpy
       /usr/include/x86_64-linux-gnu/bits/string_fortified.h:29:10
    #2 0x5f83bcdcd5e5 in virtio_net_rsc_cache_buf
       hw/net/virtio-net.c:2192:5
    #3 0x5f83bcdccb6e in virtio_net_rsc_do_coalesce
       hw/net/virtio-net.c:2393:9
    #4 0x5f83bcdc8ec8 in virtio_net_rsc_receive
       hw/net/virtio-net.c
    #5 0x5f83bcdc8ec8 in virtio_net_receive
       hw/net/virtio-net.c:2677:16

0x531000024841 is located 0 bytes after 65601-byte region
    [0x531000014800,0x531000024801)
allocated by thread T0 here:
    #2 0x5f83bcdcd5b9 in virtio_net_rsc_cache_buf
       hw/net/virtio-net.c:2190:16

SUMMARY: AddressSanitizer: heap-buffer-overflow in __asan_memcpy

Acknowledgment

Please credit: 章鱼哥@aipy (www.aipyaipy.com) and swing (swing@mail.exp.sh).

qemu-security-submission-verified.tar.gz