VYPR
Medium severity6.7NVD Advisory· Published Jun 12, 2026

CVE-2026-48914

CVE-2026-48914

Description

A heap out-of-bounds write in QEMU's virtio-blk device allows a malicious guest to cause denial of service by submitting a malformed SCSI request.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A heap out-of-bounds write in QEMU's virtio-blk device allows a malicious guest to cause denial of service by submitting a malformed SCSI request.

Vulnerability

A flaw exists in QEMU's virtio-blk device where the size of input descriptors is not properly validated before writing data. Specifically, when handling a crafted VIRTIO_BLK_T_SCSI_CMD request, the device can write past the end of a heap-allocated MMIO bounce buffer. The issue affects QEMU versions prior to the fix (all versions with the virtio-blk device). [1][3]

Exploitation

An attacker must be a malicious guest with high privileges (e.g., root inside the guest) to program virtio-blk request descriptors. The attacker submits a malformed SCSI request where the second-to-last writable input descriptor points to an MMIO guest physical address with a length of only 1 byte. QEMU maps this descriptor through an exact-size heap bounce buffer, then virtio_blk_handle_scsi() writes the 4-byte virtio_scsi_inhdr.errors field without checking that the descriptor is large enough, causing an out-of-bounds write. [3]

Impact

Successful exploitation results in an out-of-bounds write on the host heap memory, leading to a denial of service (DoS) by crashing the QEMU process. The vulnerability could potentially be leveraged for further compromise, but the primary impact described is process termination. [1][3]

Mitigation

As of the publication date, no official fix has been disclosed in the available references. Users are advised to monitor vendor advisories (e.g., Red Hat) for updated QEMU packages. [1]

AI Insight generated on Jun 12, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • QEMU/Qemuinferred2 versions
    (expand)+ 1 more
    • (no CPE)
    • (no CPE)

Patches

2
10a9fa0065a3

Merge tag 'for-upstream' of https://repo.or.cz/qemu/kevin into staging

https://github.com/qemu/qemuStefan HajnocziJun 10, 2026via github-commit-search
16 files changed · +432 52
  • block/export/fuse.c+53 16 modified
    @@ -51,23 +51,16 @@
     #define FUSE_MAX_READ_BYTES (MIN(BDRV_REQUEST_MAX_BYTES, 1 * 1024 * 1024))
     #define FUSE_MAX_WRITE_BYTES (64 * 1024)
     
    -/*
    - * fuse_init_in structure before 7.36.  We don't need the flags2 field added
    - * there, so we can work with the smaller older structure to stay compatible
    - * with older kernels.
    - */
    -struct fuse_init_in_compat {
    -    uint32_t major;
    -    uint32_t minor;
    -    uint32_t max_readahead;
    -    uint32_t flags;
    -};
    -
     typedef struct FuseRequestInHeader {
         struct fuse_in_header common;
         /* All supported requests */
         union {
    -        struct fuse_init_in_compat init;
    +        /*
    +         * When using_old_fuse_init_in() is true, then the smaller older struct
    +         * is used by the kernel. The flags2 member and other new members must
    +         * be treated as absent then.
    +         */
    +        struct fuse_init_in init;
             struct fuse_open_in open;
             struct fuse_setattr_in setattr;
             struct fuse_read_in read;
    @@ -629,6 +622,16 @@ static int clone_fuse_fd(int fd, Error **errp)
         return new_fd;
     }
     
    +/**
    + * Check whether the smaller older fuse_init_in structure from before protocol
    + * version 7.36 is used. The flags2 member and other new members must be treated
    + * as absent then.
    + */
    +static bool using_old_fuse_init_in(const struct fuse_init_in *in)
    +{
    +    return in->major < 7 || (in->major == 7 && in->minor < 36);
    +}
    +
     /**
      * Try to read a single request from the FUSE FD.
      * Takes a FuseQueue pointer in `opaque`.
    @@ -693,6 +696,31 @@ static void coroutine_fn co_read_from_fuse_fd(void *opaque)
             goto no_request;
         }
     
    +    /*
    +     * If the request is of type FUSE_INIT, need to check the version to
    +     * actually determine the length of the fuse_init_in structure used by the
    +     * kernel. In protocol version 7.36, the structure was extended.
    +     */
    +    if (in_hdr->common.opcode == FUSE_INIT) {
    +        /* Length of the fuse_init_in structure before 7.36. */
    +        size_t old_init_hdr_len = 16;
    +
    +        /*
    +         * Expect at least the size of the smaller older structure to ensure the
    +         * version can be checked.
    +         */
    +        if (unlikely(ret < sizeof(in_hdr->common) + old_init_hdr_len)) {
    +            error_report("FUSE_INIT request truncated, read only %zi bytes",
    +                         ret);
    +            fuse_write_err(fuse_fd, &in_hdr->common, -EINVAL);
    +            goto no_request;
    +        }
    +
    +        if (using_old_fuse_init_in(&in_hdr->init)) {
    +            op_hdr_len = old_init_hdr_len;
    +        }
    +    }
    +
         if (unlikely(ret < sizeof(in_hdr->common) + op_hdr_len)) {
             error_report("FUSE request truncated, expected %zu bytes, read %zi "
                          "bytes",
    @@ -826,9 +854,10 @@ static bool is_regular_file(const char *path, Error **errp)
      */
     static ssize_t coroutine_fn GRAPH_RDLOCK
     fuse_co_init(FuseExport *exp, struct fuse_init_out *out,
    -             const struct fuse_init_in_compat *in)
    +             const struct fuse_init_in *in)
     {
    -    const uint32_t supported_flags = FUSE_ASYNC_READ | FUSE_ASYNC_DIO;
    +    uint32_t supported_flags = FUSE_ASYNC_READ | FUSE_ASYNC_DIO;
    +    uint32_t flags2 = 0;
     
         if (in->major != 7) {
             error_report("FUSE major version mismatch: We have 7, but kernel has %"
    @@ -843,13 +872,21 @@ fuse_co_init(FuseExport *exp, struct fuse_init_out *out,
             return -EINVAL;
         }
     
    +    if (!using_old_fuse_init_in(in)) {
    +        /* The flags2 flags must be shifted down by 32 bits. */
    +        const uint32_t supported_flags2 = FUSE_DIRECT_IO_ALLOW_MMAP >> 32;
    +        /* flags2 is only considered if FUSE_INIT_EXT is set. */
    +        supported_flags = supported_flags | FUSE_INIT_EXT;
    +        flags2 = in->flags2 & supported_flags2;
    +    }
    +
         *out = (struct fuse_init_out) {
             .major = 7,
             .minor = MIN(FUSE_KERNEL_MINOR_VERSION, in->minor),
             .max_readahead = in->max_readahead,
             .max_write = FUSE_MAX_WRITE_BYTES,
             .flags = in->flags & supported_flags,
    -        .flags2 = 0,
    +        .flags2 = flags2,
     
             /* libfuse maximum: 2^16 - 1 */
             .max_background = UINT16_MAX,
    
  • block/qcow2.c+7 1 modified
    @@ -4234,10 +4234,16 @@ qcow2_co_pwrite_zeroes(BlockDriverState *bs, int64_t offset, int64_t bytes,
             }
     
             qemu_co_mutex_lock(&s->lock);
    -        /* We can have new write after previous check */
             offset -= head;
             bytes = s->subcluster_size;
             nr = s->subcluster_size;
    +        /*
    +         * Wait for in-flight allocating writes first: otherwise the type
    +         * check below could pass on UNALLOCATED while a yet-to-link_l2 write
    +         * completes during qcow2_subcluster_zeroize()'s own wait, letting the
    +         * resumed MAY_UNMAP discard the just-written data.
    +         */
    +        qcow2_wait_for_dependencies(bs, offset, bytes);
             ret = qcow2_get_host_offset(bs, offset, &nr, &off, &type);
             if (ret < 0 ||
                 (type != QCOW2_SUBCLUSTER_UNALLOCATED_PLAIN &&
    
  • block/qcow2-cluster.c+5 5 modified
    @@ -1474,9 +1474,9 @@ static int coroutine_fn handle_dependencies(BlockDriverState *bs,
         return 0;
     }
     
    -static void coroutine_mixed_fn wait_for_dependencies(BlockDriverState *bs,
    -                                                     uint64_t guest_offset,
    -                                                     uint64_t bytes)
    +void coroutine_mixed_fn qcow2_wait_for_dependencies(BlockDriverState *bs,
    +                                                    uint64_t guest_offset,
    +                                                    uint64_t bytes)
     {
         BDRVQcow2State *s = bs->opaque;
         QCowL2Meta *m = NULL;
    @@ -2035,7 +2035,7 @@ int qcow2_cluster_discard(BlockDriverState *bs, uint64_t offset,
          * We don't need to allocate a QCowL2Meta for the discard operation because
          * s->lock is held for the duration of the whole operation.
          */
    -    wait_for_dependencies(bs, offset, bytes);
    +    qcow2_wait_for_dependencies(bs, offset, bytes);
     
         /* Caller must pass aligned values, except at image end */
         assert(QEMU_IS_ALIGNED(offset, s->cluster_size));
    @@ -2204,7 +2204,7 @@ int coroutine_fn qcow2_subcluster_zeroize(BlockDriverState *bs, uint64_t offset,
          * We don't need to allocate a QCowL2Meta for the zeroize operation because
          * s->lock is held for the duration of the whole operation.
          */
    -    wait_for_dependencies(bs, offset, bytes);
    +    qcow2_wait_for_dependencies(bs, offset, bytes);
     
         /* If we have to stay in sync with an external data file, zero out
          * s->data_file first. */
    
  • block/qcow2.h+4 0 modified
    @@ -966,6 +966,10 @@ int coroutine_fn GRAPH_RDLOCK
     qcow2_subcluster_zeroize(BlockDriverState *bs, uint64_t offset, uint64_t bytes,
                              int flags);
     
    +void coroutine_mixed_fn
    +qcow2_wait_for_dependencies(BlockDriverState *bs, uint64_t guest_offset,
    +                            uint64_t bytes);
    +
     int GRAPH_RDLOCK
     qcow2_expand_zero_clusters(BlockDriverState *bs,
                                BlockDriverAmendStatusCB *status_cb,
    
  • block/qed.c+11 5 modified
    @@ -351,16 +351,22 @@ static void bdrv_qed_detach_aio_context(BlockDriverState *bs)
     {
         BDRVQEDState *s = bs->opaque;
     
    -    qed_cancel_need_check_timer(s);
    -    timer_free(s->need_check_timer);
    -    s->need_check_timer = NULL;
    +    if (s->need_check_timer) {
    +        qed_cancel_need_check_timer(s);
    +        timer_free(s->need_check_timer);
    +        s->need_check_timer = NULL;
    +    }
     }
     
    -static void bdrv_qed_attach_aio_context(BlockDriverState *bs,
    -                                        AioContext *new_context)
    +static void GRAPH_RDLOCK bdrv_qed_attach_aio_context(BlockDriverState *bs,
    +                                                     AioContext *new_context)
     {
         BDRVQEDState *s = bs->opaque;
     
    +    if (bdrv_is_inactive(bs)) {
    +        return;
    +    }
    +
         s->need_check_timer = aio_timer_new(new_context,
                                             QEMU_CLOCK_VIRTUAL, SCALE_NS,
                                             qed_need_check_timer_cb, s);
    
  • docs/tools/qemu-img.rst+7 3 modified
    @@ -301,15 +301,19 @@ Command description:
       For write tests, by default a buffer filled with zeros is written. This can be
       overridden with a pattern byte specified by *PATTERN*.
     
    -.. option:: bitmap (--merge SOURCE | --add | --remove | --clear | --enable | --disable)... [-b SOURCE_FILE [-F SOURCE_FMT]] [-g GRANULARITY] [--object OBJECTDEF] [--image-opts | -f FMT] FILENAME BITMAP
    +.. option:: bitmap (--merge SOURCE | --add | --remove | --remove-all | --clear | --enable | --disable)... [-b SOURCE_FILE [-F SOURCE_FMT]] [-g GRANULARITY] [--object OBJECTDEF] [--image-opts | -f FMT] FILENAME [BITMAP]
     
    -  Perform one or more modifications of the persistent bitmap *BITMAP*
    -  in the disk image *FILENAME*.  The various modifications are:
    +  Perform one or more modifications of persistent bitmaps in the disk
    +  image *FILENAME*.  Most operations require *BITMAP* to be specified;
    +  ``--remove-all`` operates on all bitmaps and does not take *BITMAP*.
    +  The various modifications are:
     
       ``--add`` to create *BITMAP*, enabled to record future edits.
     
       ``--remove`` to remove *BITMAP*.
     
    +  ``--remove-all`` to remove all bitmaps.
    +
       ``--clear`` to clear *BITMAP*.
     
       ``--enable`` to change *BITMAP* to start recording future edits.
    
  • hw/block/virtio-blk.c+7 1 modified
    @@ -199,10 +199,16 @@ static void virtio_blk_handle_scsi(VirtIOBlockReq *req)
     
         /*
          * The scsi inhdr is placed in the second-to-last input segment, just
    -     * before the regular inhdr.
    +     * before the regular inhdr. VIRTIO implementations normally do not rely on
    +     * the precise message framing, but legacy implementations did and so we do
    +     * too for the legacy virtio-blk SCSI request type.
          *
          * Just put anything nonzero so that the ioctl fails in the guest.
          */
    +    if (elem->in_sg[elem->in_num - 2].iov_len != sizeof(*scsi)) {
    +        status = VIRTIO_BLK_S_IOERR;
    +        goto fail;
    +    }
         scsi = (void *)elem->in_sg[elem->in_num - 2].iov_base;
         virtio_stl_p(vdev, &scsi->errors, 255);
         status = VIRTIO_BLK_S_UNSUPP;
    
  • qemu-img.c+48 7 modified
    @@ -87,6 +87,7 @@ enum {
         OPTION_FORCE = 276,
         OPTION_SKIP_BROKEN = 277,
         OPTION_LIMITS = 278,
    +    OPTION_REMOVE_ALL = 279,
     };
     
     typedef enum OutputFormat {
    @@ -5018,6 +5019,7 @@ enum ImgBitmapAct {
         BITMAP_ENABLE,
         BITMAP_DISABLE,
         BITMAP_MERGE,
    +    BITMAP_REMOVE_ALL,
     };
     typedef struct ImgBitmapAction {
         enum ImgBitmapAct act;
    @@ -5036,7 +5038,7 @@ static int img_bitmap(const img_cmd_t *ccmd, int argc, char **argv)
         BlockDriverState *bs = NULL, *src_bs = NULL;
         bool image_opts = false;
         int64_t granularity = 0;
    -    bool add = false, merge = false;
    +    bool add = false, merge = false, need_bitmap_name = false;
         QSIMPLEQ_HEAD(, ImgBitmapAction) actions;
         ImgBitmapAction *act, *act_next;
         const char *op;
    @@ -5052,6 +5054,7 @@ static int img_bitmap(const img_cmd_t *ccmd, int argc, char **argv)
                 {"add", no_argument, 0, OPTION_ADD},
                 {"granularity", required_argument, 0, 'g'},
                 {"remove", no_argument, 0, OPTION_REMOVE},
    +            {"remove-all", no_argument, 0, OPTION_REMOVE_ALL},
                 {"clear", no_argument, 0, OPTION_CLEAR},
                 {"enable", no_argument, 0, OPTION_ENABLE},
                 {"disable", no_argument, 0, OPTION_DISABLE},
    @@ -5070,9 +5073,9 @@ static int img_bitmap(const img_cmd_t *ccmd, int argc, char **argv)
             switch (c) {
             case 'h':
                 cmd_help(ccmd, "[-f FMT | --image-opts]\n"
    -"        ( --add [-g SIZE] | --remove | --clear | --enable | --disable |\n"
    -"          --merge SOURCE [-b SRC_FILE [-F SRC_FMT]] )..\n"
    -"        [--object OBJDEF] FILE BITMAP\n"
    +"        ( --add [-g SIZE] | --remove | --remove-all | --clear | --enable |\n"
    +"          --disable | --merge SOURCE [-b SRC_FILE [-F SRC_FMT]] )..\n"
    +"        [--object OBJDEF] FILE [BITMAP]\n"
     ,
     "  -f, --format FMT\n"
     "     specify FILE format explicitly (default: probing is used)\n"
    @@ -5086,6 +5089,8 @@ static int img_bitmap(const img_cmd_t *ccmd, int argc, char **argv)
     "     with optional multiplier suffix (in powers of 1024)\n"
     "  --remove\n"
     "     removes BITMAP from FILE\n"
    +"  --remove-all\n"
    +"     removes all bitmaps from FILE\n"
     "  --clear\n"
     "     clears BITMAP in FILE\n"
     "  --enable, --disable\n"
    @@ -5116,6 +5121,7 @@ static int img_bitmap(const img_cmd_t *ccmd, int argc, char **argv)
                 act->act = BITMAP_ADD;
                 QSIMPLEQ_INSERT_TAIL(&actions, act, next);
                 add = true;
    +            need_bitmap_name = true;
                 break;
             case 'g':
                 granularity = cvtnum("granularity", optarg, true);
    @@ -5127,28 +5133,38 @@ static int img_bitmap(const img_cmd_t *ccmd, int argc, char **argv)
                 act = g_new0(ImgBitmapAction, 1);
                 act->act = BITMAP_REMOVE;
                 QSIMPLEQ_INSERT_TAIL(&actions, act, next);
    +            need_bitmap_name = true;
    +            break;
    +        case OPTION_REMOVE_ALL:
    +            act = g_new0(ImgBitmapAction, 1);
    +            act->act = BITMAP_REMOVE_ALL;
    +            QSIMPLEQ_INSERT_TAIL(&actions, act, next);
                 break;
             case OPTION_CLEAR:
                 act = g_new0(ImgBitmapAction, 1);
                 act->act = BITMAP_CLEAR;
                 QSIMPLEQ_INSERT_TAIL(&actions, act, next);
    +            need_bitmap_name = true;
                 break;
             case OPTION_ENABLE:
                 act = g_new0(ImgBitmapAction, 1);
                 act->act = BITMAP_ENABLE;
                 QSIMPLEQ_INSERT_TAIL(&actions, act, next);
    +            need_bitmap_name = true;
                 break;
             case OPTION_DISABLE:
                 act = g_new0(ImgBitmapAction, 1);
                 act->act = BITMAP_DISABLE;
                 QSIMPLEQ_INSERT_TAIL(&actions, act, next);
    +            need_bitmap_name = true;
                 break;
             case OPTION_MERGE:
                 act = g_new0(ImgBitmapAction, 1);
                 act->act = BITMAP_MERGE;
                 act->src = optarg;
                 QSIMPLEQ_INSERT_TAIL(&actions, act, next);
                 merge = true;
    +            need_bitmap_name = true;
                 break;
             case 'b':
                 src_filename = optarg;
    @@ -5165,8 +5181,8 @@ static int img_bitmap(const img_cmd_t *ccmd, int argc, char **argv)
         }
     
         if (QSIMPLEQ_EMPTY(&actions)) {
    -        error_report("Need at least one of --add, --remove, --clear, "
    -                     "--enable, --disable, or --merge");
    +        error_report("Need at least one of --add, --remove, --remove-all, "
    +                     "--clear, --enable, --disable, or --merge");
             goto out;
         }
     
    @@ -5184,11 +5200,22 @@ static int img_bitmap(const img_cmd_t *ccmd, int argc, char **argv)
             goto out;
         }
     
    -    if (optind != argc - 2) {
    +    if (need_bitmap_name && optind != argc - 2) {
             error_report("Expecting filename and bitmap name");
             goto out;
         }
     
    +    /*
    +     * Every action other than --remove-all sets need_bitmap_name, so
    +     * !need_bitmap_name means the only action(s) given were --remove-all
    +     * and the BITMAP positional argument must be omitted. Combinations
    +     * like '--remove-all --add foo' remain valid via the branch above.
    +     */
    +    if (!need_bitmap_name && optind != argc - 1) {
    +        error_report("Expecting filename");
    +        goto out;
    +    }
    +
         filename = argv[optind];
         bitmap = argv[optind + 1];
     
    @@ -5225,6 +5252,20 @@ static int img_bitmap(const img_cmd_t *ccmd, int argc, char **argv)
                 qmp_block_dirty_bitmap_remove(bs->node_name, bitmap, &err);
                 op = "remove";
                 break;
    +        case BITMAP_REMOVE_ALL: {
    +            BdrvDirtyBitmap *bm;
    +            while ((bm = bdrv_dirty_bitmap_first(bs))) {
    +                const char *name = bdrv_dirty_bitmap_name(bm);
    +                qmp_block_dirty_bitmap_remove(bs->node_name, name, &err);
    +                if (err) {
    +                    /* Save name for proper error reporting */
    +                    bitmap = name;
    +                    break;
    +                }
    +            }
    +            op = "remove-all";
    +            break;
    +        }
             case BITMAP_CLEAR:
                 qmp_block_dirty_bitmap_clear(bs->node_name, bitmap, &err);
                 op = "clear";
    
  • tests/qemu-iotests/046+23 0 modified
    @@ -226,6 +226,26 @@ aio_write -z 0x140000 0x10000
     resume A
     aio_flush
     EOF
    +
    +# Start an allocating write to a previously unallocated cluster and, before
    +# its L2 update is linked, issue a concurrent sub-cluster zero write with
    +# MAY_UNMAP that targets a disjoint range within the same cluster. The zero
    +# write's head/tail are zero (cluster is unallocated), so qcow2_co_pwrite_zeroes
    +# would expand it to the full subcluster. Without waiting for dependencies
    +# before the zero write's "unallocated" type check, that check passes,
    +# qcow2_subcluster_zeroize then yields in wait_for_dependencies, the allocating
    +# write links its L2 entry, and the resumed zeroize unmaps the cluster -
    +# silently discarding the just-written data. Waiting first makes the zero write
    +# fall back to a bounce-buffered real write, which only touches its own
    +# subrange.
    +cat <<EOF
    +break write_aio A
    +aio_write -P 180 0x200000 0x4000
    +wait_break A
    +aio_write -z -u 0x204000 0x4000
    +resume A
    +aio_flush
    +EOF
     }
     
     overlay_io | $QEMU_IO blkdebug::"$TEST_IMG" | _filter_qemu_io |\
    @@ -310,6 +330,9 @@ verify_io()
         echo read -P 0   0x120000 0x10000
         echo read -P 0   0x130000 0x10000
         echo read -P 0   0x140000 0x10000
    +
    +    echo read -P 180 0x200000 0x4000
    +    echo read -P 0   0x204000 0xc000
     }
     
     verify_io | $QEMU_IO "$TEST_IMG" | _filter_qemu_io
    
  • tests/qemu-iotests/046.out+10 0 modified
    @@ -169,6 +169,12 @@ wrote XXX/XXX bytes at offset XXX
     XXX KiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
     wrote XXX/XXX bytes at offset XXX
     XXX KiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
    +blkdebug: Suspended request 'A'
    +blkdebug: Resuming request 'A'
    +wrote XXX/XXX bytes at offset XXX
    +XXX KiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
    +wrote XXX/XXX bytes at offset XXX
    +XXX KiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
     
     == Verify image content ==
     read 65536/65536 bytes at offset 0
    @@ -275,5 +281,9 @@ read 65536/65536 bytes at offset 1245184
     64 KiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
     read 65536/65536 bytes at offset 1310720
     64 KiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
    +read 16384/16384 bytes at offset 2097152
    +16 KiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
    +read 49152/49152 bytes at offset 2113536
    +48 KiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
     No errors were found on the image.
     *** done
    
  • tests/qemu-iotests/136+75 12 modified
    @@ -22,6 +22,7 @@
     
     import iotests
     import os
    +import json
     
     interval_length = 10
     nsec_per_sec = 1000000000
    @@ -45,14 +46,22 @@ class BlockDeviceStatsTestCase(iotests.QMPTestCase):
         wr_highest_offset = 0
         account_invalid = False
         account_failed = False
    +    stats_in_device = False
    +    use_blockdev = False
     
         def blockstats(self, device):
             result = self.vm.qmp("query-blockstats")
             for r in result['return']:
    -            if r['device'] == device:
    +            if r['device'] == device or r['node-name'] == device:
                     return r['stats']
             raise Exception("Device not found for blockstats: %s" % device)
     
    +    def qemu_io(self, cmd):
    +        if self.use_blockdev:
    +            self.vm.hmp_qemu_io("virtio0/virtio-backend", cmd, qdev=True)
    +        else:
    +            self.vm.hmp_qemu_io("drive0", cmd)
    +
         def create_blkdebug_file(self):
             file = open(blkdebug_file, 'w')
             file.write('''
    @@ -73,17 +82,54 @@ sector = "%d"
     
         @iotests.skip_if_unsupported(required_drivers)
         def setUp(self):
    -        drive_args = []
    -        drive_args.append("stats-intervals.0=%d" % interval_length)
    -        drive_args.append("stats-account-invalid=%s" %
    -                          (self.account_invalid and "on" or "off"))
    -        drive_args.append("stats-account-failed=%s" %
    -                          (self.account_failed and "on" or "off"))
    -        drive_args.append("file.image.read-zeroes=on")
             self.create_blkdebug_file()
    -        self.vm = iotests.VM().add_drive('blkdebug:%s:%s://' %
    -                                         (blkdebug_file, self.test_driver),
    -                                         ','.join(drive_args))
    +        self.vm = iotests.VM()
    +
    +        drive_args = [
    +            "file.image.read-zeroes=on",
    +        ]
    +        if self.stats_in_device:
    +            interface = "none"
    +            dev_args = {
    +                "driver": "virtio-blk",
    +                "id": "virtio0",
    +                "drive": "drive0",
    +                "stats-intervals": [ interval_length ],
    +                "account-invalid": "on" if self.account_invalid else "off",
    +                "account-failed": "on" if self.account_failed else "off",
    +            }
    +            self.vm.add_device(json.dumps(dev_args))
    +        else:
    +            assert not self.use_blockdev
    +            interface = "virtio"
    +            drive_args += [
    +                "stats-intervals.0=%d" % interval_length,
    +                "stats-account-invalid=%s" %
    +                    (self.account_invalid and "on" or "off"),
    +                "stats-account-failed=%s" %
    +                    (self.account_failed and "on" or "off"),
    +            ]
    +
    +        if self.use_blockdev:
    +            blockdev_args = {
    +                "node-name": "drive0",
    +                "driver": "raw",
    +                "file": {
    +                    "driver": "blkdebug",
    +                    "config": blkdebug_file,
    +                    "image": {
    +                        "driver": self.test_driver,
    +                        "read-zeroes": True,
    +                    },
    +                },
    +            }
    +            self.vm.add_blockdev(json.dumps(blockdev_args))
    +        else:
    +            self.vm.add_drive('blkdebug:%s:%s://' %
    +                              (blkdebug_file, self.test_driver),
    +                              ','.join(drive_args),
    +                              interface=interface)
    +
             self.vm.launch()
             # Set an initial value for the clock
             self.vm.qtest("clock_step %d" % nsec_per_sec)
    @@ -261,7 +307,7 @@ sector = "%d"
     
             # Now perform all operations
             for op in ops:
    -            self.vm.hmp_qemu_io("drive0", op)
    +            self.qemu_io(op)
     
             # Update the expected totals
             self.total_rd_bytes += rd_ops * rd_size
    @@ -328,6 +374,12 @@ sector = "%d"
             # All values must be sane before doing any I/O
             self.check_values()
     
    +class BlockDeviceStatsTestDevice(BlockDeviceStatsTestCase):
    +    stats_in_device = True
    +
    +class BlockDeviceStatsTestBlockdev(BlockDeviceStatsTestCase):
    +    stats_in_device = True
    +    use_blockdev = True
     
     class BlockDeviceStatsTestAccountInvalid(BlockDeviceStatsTestCase):
         account_invalid = True
    @@ -341,6 +393,17 @@ class BlockDeviceStatsTestAccountBoth(BlockDeviceStatsTestCase):
         account_invalid = True
         account_failed = True
     
    +class BlockDeviceStatsTestAccountBothDevice(BlockDeviceStatsTestCase):
    +    account_invalid = True
    +    account_failed = True
    +    stats_in_device = True
    +
    +class BlockDeviceStatsTestAccountBothBlockdev(BlockDeviceStatsTestCase):
    +    account_invalid = True
    +    account_failed = True
    +    stats_in_device = True
    +    use_blockdev = True
    +
     class BlockDeviceStatsTestCoroutine(BlockDeviceStatsTestCase):
         test_driver = "null-co"
     
    
  • tests/qemu-iotests/136.out+2 2 modified
    @@ -1,5 +1,5 @@
    -...................................
    +...............................................................
     ----------------------------------------------------------------------
    -Ran 35 tests
    +Ran 63 tests
     
     OK
    
  • tests/qemu-iotests/tests/fuse-mmap-shared+105 0 added
    @@ -0,0 +1,105 @@
    +#!/usr/bin/env python3
    +# group: rw
    +#
    +# Test that a FUSE export can be mmap()-ed with MAP_SHARED
    +#
    +# Copyright (C) 2026 Proxmox Server Solutions GmbH
    +#
    +# SPDX-License-Identifier: GPL-2.0-or-later
    +
    +import os
    +import itertools
    +import mmap
    +from mmap import MAP_SHARED
    +from pathlib import Path
    +
    +import iotests
    +from iotests import qemu_img, qemu_io, QemuStorageDaemon
    +
    +def test_fuse_support(mount_point):
    +    test_qsd = QemuStorageDaemon('--blockdev', 'null-co,node-name=node0',
    +                                 qmp=True)
    +    res = test_qsd.qmp('block-export-add', {
    +        'id': 'exp0',
    +        'type': 'fuse',
    +        'node-name': 'node0',
    +        'mountpoint': mount_point,
    +        'allow-other': 'off'
    +    })
    +    test_qsd.stop()
    +    if 'error' in res:
    +        assert (res['error']['desc'] ==
    +                "Parameter 'type' does not accept value 'fuse'")
    +        iotests.notrun('No FUSE support')
    +
    +# Shared mmap when using direct IO is only supported for Linux kernels >= 6.6
    +# with commit e78662e818f94 ("fuse: add a new fuse init flag to relax
    +# estrictions in no cache mode").
    +def test_linux_kernel_support():
    +    [major, minor] = map(int, os.uname().release.split('.')[:2])
    +    if major < 6 or (major == 6 and minor < 6):
    +        iotests.notrun('No kernel support for shared mmap with direct IO')
    +
    +image_size = 1 * 1024 * 1024
    +image = os.path.join(iotests.test_dir, 'image.' + iotests.imgfmt)
    +fuse_mount_point = os.path.join(iotests.test_dir, 'export.fuse')
    +Path(fuse_mount_point).touch()
    +
    +test_fuse_support(fuse_mount_point)
    +test_linux_kernel_support()
    +
    +class TestMmapShared(iotests.QMPTestCase):
    +
    +    def setUp(self):
    +        qemu_img('create', '-f', iotests.imgfmt, image, str(image_size))
    +        qemu_io(image, '-c', f'write -P 23 0 {image_size}')
    +
    +        self.qsd = QemuStorageDaemon(qmp=True)
    +
    +        self.qsd.cmd('blockdev-add', {
    +            'node-name': 'node0',
    +            'driver': iotests.imgfmt,
    +            'file': {
    +                'driver': 'file',
    +                'filename': image
    +            }
    +        })
    +
    +        self.qsd.cmd('block-export-add', {
    +            'id': 'exp0',
    +            'type': 'fuse',
    +            'node-name': 'node0',
    +            'mountpoint': fuse_mount_point,
    +            'writable': True,
    +            'allow-other': 'off'
    +        })
    +
    +    def tearDown(self):
    +        self.stop_qsd()
    +        os.remove(image)
    +        os.remove(fuse_mount_point)
    +
    +    def stop_qsd(self):
    +        if self.qsd:
    +            self.qsd.stop()
    +            self.qsd = None
    +
    +    def test_mmap_shared(self):
    +        with open(fuse_mount_point, 'r+b') as file:
    +            with mmap.mmap(file.fileno(), image_size, flags=MAP_SHARED) as mm:
    +                buf = bytearray(image_size)
    +                buf[:] = itertools.repeat(23, image_size)
    +                assert mm.read(image_size) == buf
    +                buf[:] = itertools.repeat(42, image_size)
    +                mm.seek(0)
    +                mm.write(buf)
    +                mm.flush()
    +        self.stop_qsd()
    +        qemu_io(image, '-c', f'read -P 42 0 {image_size}')
    +
    +if __name__ == '__main__':
    +    # LUKS would require key-secret in blockdev-add
    +    iotests.main(supported_fmts=['generic'],
    +                 unsupported_fmts=['luks'],
    +                 supported_protocols=['file'],
    +                 supported_platforms=['linux'])
    
  • tests/qemu-iotests/tests/fuse-mmap-shared.out+5 0 added
    @@ -0,0 +1,5 @@
    +.
    +----------------------------------------------------------------------
    +Ran 1 tests
    +
    +OK
    
  • tests/qemu-iotests/tests/qemu-img-bitmaps+24 0 modified
    @@ -161,6 +161,30 @@ $QEMU_IMG convert --bitmaps -O qcow2 "$TEST_IMG" "$TEST_IMG.copy"
     TEST_IMG="$TEST_IMG.copy" _img_info --format-specific \
         | _filter_irrelevant_img_info
     
    +echo
    +echo "=== Check --remove-all ==="
    +echo
    +
    +# Start from a fresh image so prior state does not bleed into the assertions
    +_rm_test_img "$TEST_IMG"
    +_make_test_img 10M
    +$QEMU_IMG bitmap --add -f $IMGFMT "$TEST_IMG" b0
    +$QEMU_IMG bitmap --add -f $IMGFMT "$TEST_IMG" b1
    +$QEMU_IMG bitmap --add -f $IMGFMT "$TEST_IMG" b2
    +_img_info --format-specific | _filter_irrelevant_img_info
    +
    +# Sweep every bitmap in a single command, no BITMAP positional
    +echo
    +$QEMU_IMG bitmap --remove-all -f $IMGFMT "$TEST_IMG"
    +_img_info --format-specific | _filter_irrelevant_img_info
    +
    +# Wipe + recreate in one invocation: only 'fresh' should remain
    +echo
    +$QEMU_IMG bitmap --add -f $IMGFMT "$TEST_IMG" b0
    +$QEMU_IMG bitmap --add -f $IMGFMT "$TEST_IMG" b1
    +$QEMU_IMG bitmap --remove-all --add -f $IMGFMT "$TEST_IMG" fresh
    +_img_info --format-specific | _filter_irrelevant_img_info
    +
     # success, all done
     echo '*** done'
     rm -f $seq.full
    
  • tests/qemu-iotests/tests/qemu-img-bitmaps.out+46 0 modified
    @@ -180,4 +180,50 @@ Format specific information:
                 name: b2
                 granularity: 65536
         corrupt: false
    +
    +=== Check --remove-all ===
    +
    +Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=10485760
    +image: TEST_DIR/t.IMGFMT
    +file format: IMGFMT
    +virtual size: 10 MiB (10485760 bytes)
    +cluster_size: 65536
    +Format specific information:
    +    bitmaps:
    +        [0]:
    +            flags:
    +                [0]: auto
    +            name: b0
    +            granularity: 65536
    +        [1]:
    +            flags:
    +                [0]: auto
    +            name: b1
    +            granularity: 65536
    +        [2]:
    +            flags:
    +                [0]: auto
    +            name: b2
    +            granularity: 65536
    +    corrupt: false
    +
    +image: TEST_DIR/t.IMGFMT
    +file format: IMGFMT
    +virtual size: 10 MiB (10485760 bytes)
    +cluster_size: 65536
    +Format specific information:
    +    corrupt: false
    +
    +image: TEST_DIR/t.IMGFMT
    +file format: IMGFMT
    +virtual size: 10 MiB (10485760 bytes)
    +cluster_size: 65536
    +Format specific information:
    +    bitmaps:
    +        [0]:
    +            flags:
    +                [0]: auto
    +            name: fresh
    +            granularity: 65536
    +    corrupt: false
     *** done
    
aeea0c2804c4

virtio-blk: add missing VIRTIO_BLK_T_SCSI_CMD size check (CVE-2026-48914)

https://github.com/qemu/qemuStefan HajnocziMay 26, 2026via github-commit-search
1 file changed · +7 1
  • hw/block/virtio-blk.c+7 1 modified
    @@ -199,10 +199,16 @@ static void virtio_blk_handle_scsi(VirtIOBlockReq *req)
     
         /*
          * The scsi inhdr is placed in the second-to-last input segment, just
    -     * before the regular inhdr.
    +     * before the regular inhdr. VIRTIO implementations normally do not rely on
    +     * the precise message framing, but legacy implementations did and so we do
    +     * too for the legacy virtio-blk SCSI request type.
          *
          * Just put anything nonzero so that the ioctl fails in the guest.
          */
    +    if (elem->in_sg[elem->in_num - 2].iov_len != sizeof(*scsi)) {
    +        status = VIRTIO_BLK_S_IOERR;
    +        goto fail;
    +    }
         scsi = (void *)elem->in_sg[elem->in_num - 2].iov_base;
         virtio_stl_p(vdev, &scsi->errors, 255);
         status = VIRTIO_BLK_S_UNSUPP;
    

Vulnerability mechanics

No source-code context for this CVE — mechanics is only generated when we can read the actual fix diff. Without that, the four sections (root cause, attack vector, affected code, fix) would be speculation rather than analysis.

References

3

News mentions

0

No linked articles in our index yet.