@samitouri / QOSamiQemu / commits / 4913ae36f9

virtio-blk: fix zone report buffer out-of-memory (CVE-2026-5761)

An internal buffer is used when processing VIRTIO_BLK_T_ZONE_REPORT requests. The buffer's size is controlled by the guest. A large value can result in g_malloc() failure and the QEMU process aborts, resulting in a Denial of Service (DoS) (most likely in cases where an untrusted guest application or a nested guest with virtio-blk passthrough is able to abort QEMU). Modify the zone report implementation to work incrementally with a bounded buffer size. This is purely a QEMU implementation issue and no VIRTIO spec changes are needed. Mingyuan Luo found this bug and provided a reproducer which I haven't put into tests/qtest/ because it requires a zoned storage device (e.g. root and modprobe null_blk): 1) Prepare a zoned nullblk backend (/dev/nullb0): sudo modprobe -r null_blk || true sudo modprobe null_blk nr_devices=1 zoned=1 sudo chmod 0666 /dev/nullb0 cat /sys/block/nullb0/queue/zoned 2) Create qtest input: cat >/tmp/vblk-zone-report-oom.qtest <<'EOF' outl 0xcf8 0x80002004 outw 0xcfc 0x0007 outl 0xcf8 0x80002010 outl 0xcfc 0x0000c001 outb 0xc012 0x00 outb 0xc012 0x01 outb 0xc012 0x03 outl 0xc004 0x00000000 outw 0xc00e 0x0000 outl 0xc008 0x00000100 outb 0xc012 0x07 writel 0x00020000 0x00000010 writel 0x00020004 0x00000000 writeq 0x00020008 0x0000000000000000 writeq 0x00100000 0x0000000000020000 writel 0x00100008 0x00000010 writew 0x0010000c 0x0001 writew 0x0010000e 0x0001 EOF for i in $(seq 1 1022); do d=$((0x00100000 + i * 16)) n=$((i + 1)) printf 'writeq 0x%08x 0x0000000000200000\n' "$d" >> /tmp/vblk-zone-report-oom.qtest printf 'writel 0x%08x 0x1fe00000\n' $((d + 8)) >> /tmp/vblk-zone-report-oom.qtest printf 'writew 0x%08x 0x0003\n' $((d + 12)) >> /tmp/vblk-zone-report-oom.qtest printf 'writew 0x%08x 0x%04x\n' $((d + 14)) "$n" >> /tmp/vblk-zone-report-oom.qtest done d=$((0x00100000 + 1023 * 16)) printf 'writeq 0x%08x 0x0000000000200000\n' "$d" >> /tmp/vblk-zone-report-oom.qtest printf 'writel 0x%08x 0x1fe00000\n' $((d + 8)) >> /tmp/vblk-zone-report-oom.qtest printf 'writew 0x%08x 0x0002\n' $((d + 12)) >> /tmp/vblk-zone-report-oom.qtest printf 'writew 0x%08x 0x0000\n' $((d + 14)) >> /tmp/vblk-zone-report-oom.qtest cat >> /tmp/vblk-zone-report-oom.qtest <<'EOF' writew 0x00104000 0x0000 writew 0x00104002 0x0001 writew 0x00104004 0x0000 outw 0xc010 0x0000 EOF 3) Run the qtest input with ASAN build (compile qemu with --enable-asan): build/qemu-system-x86_64 -display none \ -accel qtest -qtest stdio \ -machine pc -nodefaults -m 512M -monitor none -serial none \ -blockdev driver=host_device,node-name=disk0,filename=/dev/nullb0 \ -device virtio-blk-pci-transitional,drive=disk0,addr=04.0,queue-size=1024 \ < /tmp/vblk-zone-report-oom.qtest Cc: Sam Li <faithilikerun@gmail.com> Cc: Damien Le Moal <dlemoal@kernel.org> Cc: Dmitry Fomichev <dmitry.fomichev@wdc.com> Fixes: CVE-2026-5761 Fixes: 4f7366506a9 ("virtio-blk: add zoned storage emulation for zoned devices") Reported-by: Mingyuan Luo <myluo24@m.fudan.edu.cn> Reviewed-by: Damien Le Moal <dlemoal@kernel.org> Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>

Stefan Hajnoczi committed Apr 10, 2026 at 08:11 UTC 4913ae36f9796c55d434dcbfa6bdb9ebb3e5e4b1
1 file changed +67 -33
hw/block/virtio-blk.c
+67 -33
@@ -38,6 +38,9 @@
38 #include "hw/virtio/virtio-blk-common.h"
39 #include "qemu/coroutine.h"
40
41 +/* Internal buffer size limit for zone report */
42 +#define VIRTIO_BLK_MAX_ZONES_PER_BATCH 4096
43 +
44 static void virtio_blk_ioeventfd_attach(VirtIOBlock *s);
45
46 static void virtio_blk_init_request(VirtIOBlock *s, VirtQueue *vq,
@@ -447,15 +450,22 @@ err:
450 return err_status;
451 }
452
453 +typedef struct {
454 + unsigned int total_nr_zones; /* max zones to fill in this request */
455 + unsigned int nr_zones_done; /* how many zones have been filled in */
456 + int64_t iov_offset; /* current byte position in in_iov[] */
457 + int64_t offset; /* current zone report disk offset */
458 + unsigned int nr_zones; /* for zone report calls */
459 + unsigned int zones_per_batch; /* size of zone report buffer */
460 + BlockZoneDescriptor *zones; /* zone report buffer */
461 +} ZoneReportData;
462 +
463 typedef struct ZoneCmdData {
464 VirtIOBlockReq *req;
465 struct iovec *in_iov;
466 unsigned in_num;
467 union {
455 - struct {
456 - unsigned int nr_zones;
457 - BlockZoneDescriptor *zones;
458 - } zone_report_data;
468 + ZoneReportData zone_report_data;
469 struct {
470 int64_t offset;
471 } zone_append_data;
@@ -512,16 +522,15 @@ static bool check_zoned_request(VirtIOBlock *s, int64_t offset, int64_t len,
522 static void virtio_blk_zone_report_complete(void *opaque, int ret)
523 {
524 ZoneCmdData *data = opaque;
525 + ZoneReportData *zrd = &data->zone_report_data;
526 VirtIOBlockReq *req = data->req;
527 VirtIODevice *vdev = VIRTIO_DEVICE(req->dev);
528 struct iovec *in_iov = data->in_iov;
529 unsigned in_num = data->in_num;
519 - int64_t zrp_size, n, j = 0;
520 - int64_t nz = data->zone_report_data.nr_zones;
530 + int64_t n;
531 + unsigned nz = zrd->nr_zones;
532 int8_t err_status = VIRTIO_BLK_S_OK;
522 - struct virtio_blk_zone_report zrp_hdr = (struct virtio_blk_zone_report) {
523 - .nr_zones = cpu_to_le64(nz),
524 - };
533 + struct virtio_blk_zone_report zrp_hdr = {};
534
535 trace_virtio_blk_zone_report_complete(vdev, req, nz, ret);
536 if (ret) {
@@ -529,28 +538,18 @@ static void virtio_blk_zone_report_complete(void *opaque, int ret)
538 goto out;
539 }
540
532 - zrp_size = sizeof(struct virtio_blk_zone_report)
533 - + sizeof(struct virtio_blk_zone_descriptor) * nz;
534 - n = iov_from_buf(in_iov, in_num, 0, &zrp_hdr, sizeof(zrp_hdr));
535 - if (n != sizeof(zrp_hdr)) {
536 - virtio_error(vdev, "Driver provided input buffer that is too small!");
537 - err_status = VIRTIO_BLK_S_ZONE_INVALID_CMD;
538 - goto out;
539 - }
540 -
541 - for (size_t i = sizeof(zrp_hdr); i < zrp_size;
542 - i += sizeof(struct virtio_blk_zone_descriptor), ++j) {
541 + for (unsigned j = 0; j < nz; j++) {
542 struct virtio_blk_zone_descriptor desc =
543 (struct virtio_blk_zone_descriptor) {
545 - .z_start = cpu_to_le64(data->zone_report_data.zones[j].start
544 + .z_start = cpu_to_le64(zrd->zones[j].start
545 >> BDRV_SECTOR_BITS),
547 - .z_cap = cpu_to_le64(data->zone_report_data.zones[j].cap
546 + .z_cap = cpu_to_le64(zrd->zones[j].cap
547 >> BDRV_SECTOR_BITS),
549 - .z_wp = cpu_to_le64(data->zone_report_data.zones[j].wp
548 + .z_wp = cpu_to_le64(zrd->zones[j].wp
549 >> BDRV_SECTOR_BITS),
550 };
551
553 - switch (data->zone_report_data.zones[j].type) {
552 + switch (zrd->zones[j].type) {
553 case BLK_ZT_CONV:
554 desc.z_type = VIRTIO_BLK_ZT_CONV;
555 break;
@@ -564,7 +563,7 @@ static void virtio_blk_zone_report_complete(void *opaque, int ret)
563 g_assert_not_reached();
564 }
565
567 - switch (data->zone_report_data.zones[j].state) {
566 + switch (zrd->zones[j].state) {
567 case BLK_ZS_RDONLY:
568 desc.z_state = VIRTIO_BLK_ZS_RDONLY;
569 break;
@@ -594,18 +593,47 @@ static void virtio_blk_zone_report_complete(void *opaque, int ret)
593 }
594
595 /* TODO: it takes O(n^2) time complexity. Optimizations required. */
597 - n = iov_from_buf(in_iov, in_num, i, &desc, sizeof(desc));
596 + n = iov_from_buf(in_iov, in_num, zrd->iov_offset, &desc, sizeof(desc));
597 if (n != sizeof(desc)) {
598 virtio_error(vdev, "Driver provided input buffer "
599 "for descriptors that is too small!");
600 err_status = VIRTIO_BLK_S_ZONE_INVALID_CMD;
601 + goto out;
602 }
603 +
604 + zrd->iov_offset += sizeof(desc);
605 + }
606 +
607 + if (nz > 0) {
608 + BlockZoneDescriptor *zone = &zrd->zones[nz - 1];
609 + zrd->offset = zone->start + zone->length;
610 + }
611 +
612 + zrd->nr_zones_done += nz;
613 +
614 + /* Call zone report again if the end hasn't been reached yet */
615 + if (nz == zrd->zones_per_batch &&
616 + zrd->nr_zones_done < zrd->total_nr_zones) {
617 + zrd->nr_zones = MIN(zrd->zones_per_batch,
618 + zrd->total_nr_zones - zrd->nr_zones_done);
619 + blk_aio_zone_report(req->dev->blk, zrd->offset, &zrd->nr_zones,
620 + zrd->zones, virtio_blk_zone_report_complete, data);
621 + return;
622 + }
623 +
624 + /* Fill in header now that all zones have been reported */
625 + zrp_hdr.nr_zones = cpu_to_le64(zrd->nr_zones_done);
626 + n = iov_from_buf(in_iov, in_num, 0, &zrp_hdr, sizeof(zrp_hdr));
627 + if (n != sizeof(zrp_hdr)) {
628 + virtio_error(vdev, "Driver provided input buffer that is too small!");
629 + err_status = VIRTIO_BLK_S_ZONE_INVALID_CMD;
630 + goto out;
631 }
632
633 out:
634 virtio_blk_req_complete(req, err_status);
635 g_free(req);
608 - g_free(data->zone_report_data.zones);
636 + g_free(zrd->zones);
637 g_free(data);
638 }
639
@@ -617,7 +645,8 @@ static void virtio_blk_handle_zone_report(VirtIOBlockReq *req,
645 VirtIODevice *vdev = VIRTIO_DEVICE(s);
646 unsigned int nr_zones;
647 ZoneCmdData *data;
620 - int64_t zone_size, offset;
648 + ZoneReportData *zrd;
649 + int64_t offset;
650 uint8_t err_status;
651
652 if (req->in_len < sizeof(struct virtio_blk_inhdr) +
@@ -639,16 +668,21 @@ static void virtio_blk_handle_zone_report(VirtIOBlockReq *req,
668 trace_virtio_blk_handle_zone_report(vdev, req,
669 offset >> BDRV_SECTOR_BITS, nr_zones);
670
642 - zone_size = sizeof(BlockZoneDescriptor) * nr_zones;
671 data = g_malloc(sizeof(ZoneCmdData));
672 data->req = req;
673 data->in_iov = in_iov;
674 data->in_num = in_num;
647 - data->zone_report_data.nr_zones = nr_zones;
648 - data->zone_report_data.zones = g_malloc(zone_size),
675
650 - blk_aio_zone_report(s->blk, offset, &data->zone_report_data.nr_zones,
651 - data->zone_report_data.zones,
676 + zrd = &data->zone_report_data;
677 + zrd->total_nr_zones = nr_zones;
678 + zrd->nr_zones_done = 0;
679 + zrd->iov_offset = sizeof(struct virtio_blk_zone_report);
680 + zrd->offset = offset;
681 + zrd->zones_per_batch = MIN(nr_zones, VIRTIO_BLK_MAX_ZONES_PER_BATCH);
682 + zrd->zones = g_malloc(zrd->zones_per_batch * sizeof(BlockZoneDescriptor));
683 +
684 + zrd->nr_zones = zrd->zones_per_batch;
685 + blk_aio_zone_report(s->blk, offset, &zrd->nr_zones, zrd->zones,
686 virtio_blk_zone_report_complete, data);
687 return;
688 out: