QEMU packed-ring indirect zero-length descriptor reaches a host assertion and crashes the VM process

Host environment

Operating system: OS/kernel version: Architecture: QEMU flavor: QEMU version: QEMU command line:

Severity: Medium

Advisory Details

Title: QEMU packed-ring indirect zero-length descriptor reaches a host assertion and crashes the VM process

Description:

Summary

A guest that can directly program a packed virtio queue can submit an indirect head descriptor with len = 0 to virtio-rng-pci, causing QEMU to accept the zero-length indirect table on the packed-ring path and then abort in address_space_cache_init() on assert(len > 0). On the highest currently affected upstream release tag, v11.0.2, this is a guest-to-host denial of service against the qemu-system-x86_64 process backing the VM.

Details

The bug is in the packed-ring indirect descriptor parsing logic in hw/virtio/virtio.c.

At the highest affected upstream release tag, v11.0.2, which peels to the canonical GitHub commit e545d8bb9d63e9dd61542b88463183314cff9482, the packed indirect paths validate only divisibility by sizeof(VRingPackedDesc):

if (desc.flags & VRING_DESC_F_INDIRECT) {
    if (desc.len % sizeof(VRingPackedDesc)) {
        virtio_error(vdev, "Invalid size for indirect buffer table");
        goto err;
    }

    len = address_space_cache_init(&indirect_desc_cache,
                                   vdev->dma_as,
                                   desc.addr, desc.len, false);

The equivalent split-ring path in the same file already rejects zero length explicitly:

if (!desc.len || (desc.len % sizeof(VRingDesc))) {
    virtio_error(vdev, "Invalid size for indirect buffer table");
    goto err;
}

That difference matters because 0 % sizeof(VRingPackedDesc) == 0, so a guest-supplied desc.len = 0 is treated as structurally valid on the packed path and is immediately forwarded into:

assert(len > 0);

in system/physmem.c:address_space_cache_init().

The reproduced call chain for the end-to-end exploit is:

  1. A malicious guest negotiates VIRTIO_F_VERSION_1, VIRTIO_F_RING_PACKED, and VIRTIO_RING_F_INDIRECT_DESC on virtio-rng-pci.
  2. The guest submits a packed indirect head descriptor with VRING_DESC_F_INDIRECT set and len = 0.
  3. hw/virtio/virtio-rng.c:get_request_size() reaches virtqueue_get_avail_bytes().
  4. virtqueue_packed_get_avail_bytes() accepts the zero-length indirect table and calls address_space_cache_init(..., len=0, ...).
  5. system/physmem.c aborts on assert(len > 0).
  6. The host QEMU process exits, dropping the VM.

This is not a static source-only claim and does not require local patching or internal hooks. The PoC launches the real build/qemu-system-x86_64 binary, boots a minimal guest, and interacts with the standard guest-visible virtio-rng-pci interface end to end. A matched control changes only the indirect head descriptor length from 0 to sizeof(indirect_table) and exits cleanly through isa-debug-exit, which rules out a generic packed-ring stability issue.

PoC

Prerequisites

  • A checkout of qemu/qemu at any vulnerable version up to and including v11.0.2, or an equivalent tree that still contains the same packed-ring indirect parsing logic.
  • Standard QEMU build prerequisites sufficient to build qemu-system-x86_64.
  • A local environment that can run the built binary with -accel tcg.
  • If the local build links against the repo-bundled Rutabaga library, set LD_LIBRARY_PATH so qemu-system-x86_64 can resolve librutabaga_gfx_ffi.so.0.
  • The target VM must expose virtio-rng-pci with packed rings enabled. The reproduced setup uses: -device virtio-rng-pci,disable-legacy=on,disable-modern=off,packed=on,addr=04.0,rng=rng0

Reproduction Steps

  1. Download the support files needed to build the minimal guest payload:
  2. Download the harness files:
  3. Place all downloaded files in the same directory inside the vulnerable QEMU checkout.
  4. If needed for the local build, export the Rutabaga library path:
    export LD_LIBRARY_PATH=/path/to/qemu/llm-enhance/local/rutabaga/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}
  5. Run the exploit script:
    python3 verification_test.py
  6. Observe that the script reports [DEFECT-CONFIRMED], the guest reaches virtio-rng queue setup, and QEMU aborts on address_space_cache_init: Assertion 'len > 0' failed.
  7. Run the control for comparison:
    python3 control-nonzero-indirect-len.py
  8. Observe that the control reports [CONTROL-PASSED], the guest reaches post-notify, and the same QEMU configuration exits cleanly through isa-debug-exit.

Log of Evidence

$ python3 verification_test.py
[DEFECT-CONFIRMED] guest-controlled packed indirect head descriptor with len=0 reached the packed indirect parser and terminated QEMU (rc=-6)

$ sed -n '1,20p' verification_result.log
[Verification Mode] End-to-End
Command: /root/project/zgc-project/qemu/build/qemu-system-x86_64 -accel tcg,tb-size=64 -machine pc -m 128M -nodefaults -no-reboot -display none -monitor none -serial none -parallel none -kernel /root/project/zgc-project/qemu/llm-enhance/cve-finding/DoS/CVE-2016-6490-packed-indirect-zero-len-exp/virtio_packed_zero_indirect.elf -chardev stdio,id=dbg,signal=off -device isa-debugcon,chardev=dbg -device isa-debug-exit,iobase=0xf4,iosize=0x4 -object rng-random,id=rng0,filename=/dev/urandom -device virtio-rng-pci,disable-legacy=on,disable-modern=off,packed=on,addr=04.0,rng=rng0
[Test Input] Guest-crafted packed indirect head descriptor with len=0 on virtio-rng queue 0
[Interface] Official qemu-system-x86_64 CLI launching a real guest against virtio-rng-pci
[Processing] guest notify -> virtio_rng_process() -> get_request_size() -> virtqueue_packed_get_avail_bytes()
[Sink] address_space_cache_init(desc.addr, desc.len=0, false)
Return code: -6
Guest log:
guest: pci scan modern virtio-rng
guest: slot=0x4 common=0xfebf8000 notify=0xfebfb000 mult=0x4
guest: features=0x10530000000
guest: queue size=8 notify_off=0
QEMU stderr:
qemu-system-x86_64: ../system/physmem.c:3840: address_space_cache_init: Assertion `len > 0' failed.

$ python3 control-nonzero-indirect-len.py
[CONTROL-PASSED] changing only the indirect table length from zero to a valid nonzero multiple prevents the abort and QEMU exits through isa-debug-exit

$ sed -n '1,20p' control_result.log
[Verification Mode] End-to-End
Command: /root/project/zgc-project/qemu/build/qemu-system-x86_64 -accel tcg,tb-size=64 -machine pc -m 128M -nodefaults -no-reboot -display none -monitor none -serial none -parallel none -kernel /root/project/zgc-project/qemu/llm-enhance/cve-finding/DoS/CVE-2016-6490-packed-indirect-zero-len-exp/virtio_packed_valid_indirect.elf -chardev stdio,id=dbg,signal=off -device isa-debugcon,chardev=dbg -device isa-debug-exit,iobase=0xf4,iosize=0x4 -object rng-random,id=rng0,filename=/dev/urandom -device virtio-rng-pci,disable-legacy=on,disable-modern=off,packed=on,addr=04.0,rng=rng0
[Test Input] Same guest/device path, but the packed indirect head descriptor len is changed from 0 to sizeof(indirect_table)
[Interface] Official qemu-system-x86_64 CLI launching a real guest against virtio-rng-pci
[Processing] guest notify -> virtio_rng_process() -> get_request_size() -> virtqueue_packed_get_avail_bytes()
[Sink] address_space_cache_init(desc.addr, desc.len=sizeof(indirect_table), false)
Return code: 1
Guest log:
guest: pci scan modern virtio-rng
guest: slot=0x4 common=0xfebf8000 notify=0xfebfb000 mult=0x4
guest: features=0x10530000000
guest: queue size=8 notify_off=0
guest: notify nonzero packed indirect
guest: post-notify

Impact

This is a guest-triggerable denial of service in the QEMU virtio device model. Any deployment that exposes a packed-ring-capable virtio-rng-pci device to an untrusted tenant guest is affected if the guest can directly program the virtqueue instead of being constrained to a conservative in-guest driver behavior. Successful exploitation terminates the host QEMU process backing that VM, immediately interrupting the guest workload and any orchestration around it. The current evidence demonstrates reliable host-process termination; it does not require claiming host code execution.

Affected products

  • Ecosystem: GitHub
  • Package name: qemu/qemu
  • Affected versions: <= 11.0.2
  • Patched versions:

Severity

  • Severity: Medium
  • Vector string: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:N/I:N/A:H

Weaknesses

  • CWE: CWE-617: Reachable Assertion

Occurrences

Permalink Description
https://github.com/qemu/qemu/blob/e545d8bb9d63e9dd61542b88463183314cff9482/hw/virtio/virtio-rng.c#L36-L41 get_request_size() pulls guest-controlled queue sizing information through virtqueue_get_avail_bytes(), making the packed indirect parser reachable from the public virtio-rng-pci data path.
https://github.com/qemu/qemu/blob/e545d8bb9d63e9dd61542b88463183314cff9482/hw/virtio/virtio.c#L1477-L1494 virtqueue_packed_get_avail_bytes() checks only desc.len % sizeof(VRingPackedDesc) and then immediately maps the indirect table, so desc.len == 0 reaches address_space_cache_init().
https://github.com/qemu/qemu/blob/e545d8bb9d63e9dd61542b88463183314cff9482/hw/virtio/virtio.c#L1929-L1940 virtqueue_packed_pop() contains the same missing zero-length guard on packed indirect descriptors, leaving a sibling live parsing path vulnerable to the same root cause.
https://github.com/qemu/qemu/blob/e545d8bb9d63e9dd61542b88463183314cff9482/system/physmem.c#L3842-L3848 address_space_cache_init() aborts on assert(len > 0), which turns the packed-path validation gap into a reliable host QEMU process crash.