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