Merge tag 'for-upstream' of https://repo.or.cz/qemu/kevin into staging Block layer patches - qcow2: Fix data loss on zero write with detect-zeroes=unmap - qemu-img bitmap: add sub-command --remove-all - export/fuse: set FUSE_DIRECT_IO_ALLOW_MMAP flag to fix regression - virtio-blk: add missing VIRTIO_BLK_T_SCSI_CMD size check (CVE-2026-48914) - qed: Don't try to flush during incoming migration - iotests/136: Test stats-intervals with -blockdev/-device # -----BEGIN PGP SIGNATURE----- # # iQJFBAABCgAvFiEE3D3rFZqa+V09dFb+fwmycsiPL9YFAmopOKwRHGt3b2xmQHJl # ZGhhdC5jb20ACgkQfwmycsiPL9Z7ERAAlB6SSRMCjRkYq6z61yQLskLzPXjP5SQW # kC0s67zJO5cGCTolSMYCF8NMCFA/K7z2ZTZqfv59ERJJnQ2I90cJ3PK1cPkIBOPD # hP1w4/OMVKDrzkCyWmBEief4HD265itjr7h6Q4J0NGFBoiHo+R/J6jIDYebFJkyE # 6Fqj6YKMFm/7dlyULGFG9hg5BnCiAWcCw11rH07GDLRGPYBSZeMU/DNUZG6Bzebr # 3VCUKGAB0IfN8I0rUHnBTadtwf9Nj8PYf+UCQph4n4CHkbpgbgnfZbmsPCHYL738 # B4U34gLfAWaD+D2wF9jU6769HBvoqsm5SMdURHrzPbOA/JSKKBsOoG7Q7s6kGkMI # vrtrHJvtXL5kI3aCSXZ9N1MFxDnTIp1ubB6CfVrId+q7ntMnLf4vsT5EqikUGc6o # OLvmPsYD7cX9oz6pDhKgrhq14pdPlEaBFeNl8is963k82C2aM5s0G72gRJ1fpQwP # rbI+z/sGYJHsanzlkmesrOdZ682beCLb6Fzgo6Mi+djrZWfYmI9A1m1s4rCQuFQi # nrzNHE0mBDaL4Nx49qMnLFKBstKoTJa3FAiPuLael5krfZ4C/ALs8xJGXVs4GdcE # Df/fpyVlx06FJDHP6O+/GuR7jaj/fieCaZpFvzQ6HNLdpk2XTJwbVxa2k48x2foR # mviRxIlT198= # =FwVi # -----END PGP SIGNATURE----- # gpg: Signature made Wed 10 Jun 2026 06:13:00 EDT # gpg: using RSA key DC3DEB159A9AF95D3D7456FE7F09B272C88F2FD6 # gpg: issuer "kwolf@redhat.com" # gpg: Good signature from "Kevin Wolf <kwolf@redhat.com>" [full] # Primary key fingerprint: DC3D EB15 9A9A F95D 3D74 56FE 7F09 B272 C88F 2FD6 * tag 'for-upstream' of https://repo.or.cz/qemu/kevin: qed: Don't try to flush during incoming migration iotests: test shared mmap for fuse export block/export/fuse: set FUSE_DIRECT_IO_ALLOW_MMAP flag to fix regression block/export/fuse: use struct fuse_init_in qcow2: Fix data loss on zero write with detect-zeroes=unmap iotests/136: Test stats-intervals with -blockdev/-device qemu-img: add sub-command --remove-all to 'qemu-img bitmap' virtio-blk: add missing VIRTIO_BLK_T_SCSI_CMD size check (CVE-2026-48914) Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
diff --git a/block/export/fuse.c b/block/export/fuse.c index a2a478d..c0e8dfb 100644 --- a/block/export/fuse.c +++ b/block/export/fuse.c
@@ -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; @@ -630,6 +623,16 @@ } /** + * 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 @@ 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 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 @@ 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,
diff --git a/block/qcow2-cluster.c b/block/qcow2-cluster.c index 8b1e80b..e02fae6 100644 --- a/block/qcow2-cluster.c +++ b/block/qcow2-cluster.c
@@ -1474,9 +1474,9 @@ 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 @@ * 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 @@ * 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. */
diff --git a/block/qcow2.c b/block/qcow2.c index 81fd299..19271b1 100644 --- a/block/qcow2.c +++ b/block/qcow2.c
@@ -4234,10 +4234,16 @@ } 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 &&
diff --git a/block/qcow2.h b/block/qcow2.h index 192a45d..ce51704 100644 --- a/block/qcow2.h +++ b/block/qcow2.h
@@ -966,6 +966,10 @@ 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,
diff --git a/block/qed.c b/block/qed.c index da23a83..0eccfa2 100644 --- a/block/qed.c +++ b/block/qed.c
@@ -351,16 +351,22 @@ { 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);
diff --git a/docs/tools/qemu-img.rst b/docs/tools/qemu-img.rst index 558b0eb..b0c798b 100644 --- a/docs/tools/qemu-img.rst +++ b/docs/tools/qemu-img.rst
@@ -301,15 +301,19 @@ 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.
diff --git a/hw/block/virtio-blk.c b/hw/block/virtio-blk.c index 9cb9f1f..6b92066 100644 --- a/hw/block/virtio-blk.c +++ b/hw/block/virtio-blk.c
@@ -199,10 +199,16 @@ /* * 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;
diff --git a/qemu-img.c b/qemu-img.c index c42dd4e..2f63d31 100644 --- a/qemu-img.c +++ b/qemu-img.c
@@ -87,6 +87,7 @@ OPTION_FORCE = 276, OPTION_SKIP_BROKEN = 277, OPTION_LIMITS = 278, + OPTION_REMOVE_ALL = 279, }; typedef enum OutputFormat { @@ -5018,6 +5019,7 @@ BITMAP_ENABLE, BITMAP_DISABLE, BITMAP_MERGE, + BITMAP_REMOVE_ALL, }; typedef struct ImgBitmapAction { enum ImgBitmapAct act; @@ -5036,7 +5038,7 @@ 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 @@ {"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 @@ 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 @@ " 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 @@ 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,21 +5133,30 @@ 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); @@ -5149,6 +5164,7 @@ 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 @@ } 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 @@ 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 @@ 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";
diff --git a/tests/qemu-iotests/046 b/tests/qemu-iotests/046 index e03dd40..0d84b5c 100755 --- a/tests/qemu-iotests/046 +++ b/tests/qemu-iotests/046
@@ -226,6 +226,26 @@ 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 @@ 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
diff --git a/tests/qemu-iotests/046.out b/tests/qemu-iotests/046.out index 6341df3..137cf52 100644 --- a/tests/qemu-iotests/046.out +++ b/tests/qemu-iotests/046.out
@@ -169,6 +169,12 @@ 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 @@ 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
diff --git a/tests/qemu-iotests/136 b/tests/qemu-iotests/136 index 8fce88b..58df876 100755 --- a/tests/qemu-iotests/136 +++ b/tests/qemu-iotests/136
@@ -22,6 +22,7 @@ import iotests import os +import json interval_length = 10 nsec_per_sec = 1000000000 @@ -45,14 +46,22 @@ 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 @@ @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 @@ # 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 @@ # 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 @@ 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"
diff --git a/tests/qemu-iotests/136.out b/tests/qemu-iotests/136.out index cfa5c0d..4823c11 100644 --- a/tests/qemu-iotests/136.out +++ b/tests/qemu-iotests/136.out
@@ -1,5 +1,5 @@ -................................... +............................................................... ---------------------------------------------------------------------- -Ran 35 tests +Ran 63 tests OK
diff --git a/tests/qemu-iotests/tests/fuse-mmap-shared b/tests/qemu-iotests/tests/fuse-mmap-shared new file mode 100755 index 0000000..52941a3 --- /dev/null +++ b/tests/qemu-iotests/tests/fuse-mmap-shared
@@ -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'])
diff --git a/tests/qemu-iotests/tests/fuse-mmap-shared.out b/tests/qemu-iotests/tests/fuse-mmap-shared.out new file mode 100644 index 0000000..ae1213e --- /dev/null +++ b/tests/qemu-iotests/tests/fuse-mmap-shared.out
@@ -0,0 +1,5 @@ +. +---------------------------------------------------------------------- +Ran 1 tests + +OK
diff --git a/tests/qemu-iotests/tests/qemu-img-bitmaps b/tests/qemu-iotests/tests/qemu-img-bitmaps index 7a3fe8c..979bc0d 100755 --- a/tests/qemu-iotests/tests/qemu-img-bitmaps +++ b/tests/qemu-iotests/tests/qemu-img-bitmaps
@@ -161,6 +161,30 @@ 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
diff --git a/tests/qemu-iotests/tests/qemu-img-bitmaps.out b/tests/qemu-iotests/tests/qemu-img-bitmaps.out index 74b81f7..99290d3 100644 --- a/tests/qemu-iotests/tests/qemu-img-bitmaps.out +++ b/tests/qemu-iotests/tests/qemu-img-bitmaps.out
@@ -180,4 +180,50 @@ 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