All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v7 00/25] parallels: Add full dirty bitmap support
@ 2026-09-03 14:41 Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 01/25] parallels: Set s->used_bmap to NULL in parallels_free_used_bitmap() Denis V. Lunev
                   ` (24 more replies)
  0 siblings, 25 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi

Parallels format driver:
* make some preparation
* add dirty bitmap saving
* make dirty bitmap RW
* fix broken checks
* refactor leak check
* add parallels format support to several tests

Alexander left the team, so this comes from me. v6 was:

https://lore.kernel.org/qemu-devel/20260817185942.1065143-1-den@openvz.org/

and v5, his last one, was:

https://lore.kernel.org/qemu-devel/20240311181850.73013-1-alexander.ivanov@virtuozzo.com/

It does not apply to master on its own. It goes on top of "[PATCH 0/8]
parallels: fix Format Extension parsing":

https://lore.kernel.org/qemu-devel/20260811173857.396571-1-den@openvz.org/

whose patch 3 is merged as dc04053687 ("dirty-bitmap: fix integer
overflow in serialization coverage"). The other seven are unchanged and
still apply, so they are not resent. Together they sit on master at
a925240509 ("Merge tag 'block-pull-request' of
https://gitlab.com/stefanha/qemu into staging").

v7:
Rebased on master, which brought require_hmp=True into 15, 24 and 25.
12: Renamed a module scope variable which the test functions shadow.
17: Moved the read-only open case to 19, where the output it looks at
    exists for the first time. It could not pass here.
19: Reordered the QAPI additions and gave ImageInfoSpecificKind its
    @parallels line, both on Markus' review. Report a bitmap which
    can not be used as inconsistent, and cover that.
21, 22: Renamed the module scope variables which the test functions
    shadow, so that iotest 297 passes at every patch of the series.

v6:
Reworked, so the numbers have shifted.
2: Made it .bdrv_inactivate and armed the in use flag on activation.
3: New patch, inactivating a read-only node.
5: cluster_end is uint64_t.
6: Absorbed the mark_unused helper, fixed the used bitmap growth.
7: New patch, the check no longer dies on what it is meant to report.
9: Absorbed the three parallels_check_leak() patches and the truncation.
10: Dropped the redundant bdrv_pwrite_zeroes() of the reuse branch.
11: Refuse a bitmap which does not fit, report a lost one, added ext_end.
12: Absorbed "Make a loaded dirty bitmap persistent", drop a broken one.
13: Bound the search by the region rather than by the cluster.
14: New patch, the Format Extension against the leak check.
15: Skipped test_reopen_rw, the bitmap name is a UUID.
16: New patch, a bitmap L1 entry has to point inside the data area.
17: Mark the bitmaps inconsistent instead of skipping the load.
18: New patch, block-dirty-bitmap-remove reaches the image.
19: New patch, qemu-img info reports the bitmaps.
20: New patch, renames parallels-read-bitmap to parallels-bitmap.
21: New patch, covers storing a bitmap.
22: New patch, covers the qemu-img bitmap sub-commands.
23: New patch, covers a broken Format Extension and a combined repair.
25: Added a comment about the bitmap name.
Dropped "Preserve extensions cluster for non-transient extensions" and
"Reverse a conditional in parallels_check_leak()".

Signed-off-by: Denis V. Lunev <den@openvz.org>
Cc: Stefan Hajnoczi <stefanha@redhat.com>

Alexander Ivanov (4):
  parallels: Set s->used_bmap to NULL in parallels_free_used_bitmap()
  parallels: Make mark_used() a global function
  parallels: Create used bitmap even if checks needed
  tests: Turned on 256, 299, 304 and block-status-cache for parallels
    format

Denis V. Lunev (21):
  parallels: split inactivation out and add the activation counterpart
  iotests: cover inactivating a read-only node
  parallels: Limit search in parallels_mark_used to the last marked
    cluster
  parallels: Move host clusters allocation to a separate function
  parallels: do not let the check die on what it is meant to report
  parallels: Drop unused clusters at the end of the image
  parallels: Remove unnecessary data_end field
  parallels: Add dirty bitmaps saving
  parallels: Let image extensions work in RW mode
  parallels: Handle L1 entries equal to one
  iotests: cover the Format Extension against the leak check
  iotests: run the persistent dirty bitmap test on parallels
  parallels: reject a bitmap L1 entry outside the data area
  parallels: do not trust the bitmaps of an image which was not closed
  parallels: implement removing a stored dirty bitmap
  parallels: report the stored dirty bitmaps in qemu-img info
  iotests: rename parallels-read-bitmap to parallels-bitmap
  iotests: cover storing a parallels dirty bitmap
  iotests: cover the qemu-img bitmap operations on parallels
  iotests: cover a broken Format Extension and a combined repair
  tests: Add parallels format support to image-fleecing

 block/parallels-ext.c                         | 437 +++++++++++++++-
 block/parallels.c                             | 455 +++++++++++------
 block/parallels.h                             |  23 +-
 qapi/block-core.json                          |  46 +-
 tests/qemu-iotests/165                        |  18 +-
 tests/qemu-iotests/256                        |   2 +-
 tests/qemu-iotests/299                        |   2 +-
 tests/qemu-iotests/304                        |   2 +-
 tests/qemu-iotests/tests/block-status-cache   |   2 +-
 tests/qemu-iotests/tests/image-fleecing       |  14 +-
 tests/qemu-iotests/tests/inactive-node-nbd    |  14 +
 .../qemu-iotests/tests/inactive-node-nbd.out  |   8 +
 tests/qemu-iotests/tests/parallels-bitmap     | 475 ++++++++++++++++++
 tests/qemu-iotests/tests/parallels-bitmap.out | 106 ++++
 tests/qemu-iotests/tests/parallels-checks     | 235 +++++++++
 tests/qemu-iotests/tests/parallels-checks.out | 193 +++++++
 .../qemu-iotests/tests/parallels-read-bitmap  | 231 ---------
 .../tests/parallels-read-bitmap.out           |  39 --
 18 files changed, 1848 insertions(+), 454 deletions(-)
 create mode 100755 tests/qemu-iotests/tests/parallels-bitmap
 create mode 100644 tests/qemu-iotests/tests/parallels-bitmap.out
 delete mode 100755 tests/qemu-iotests/tests/parallels-read-bitmap
 delete mode 100644 tests/qemu-iotests/tests/parallels-read-bitmap.out

-- 
2.53.0



^ permalink raw reply	[flat|nested] 29+ messages in thread

* [PATCH v7 01/25] parallels: Set s->used_bmap to NULL in parallels_free_used_bitmap()
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 02/25] parallels: split inactivation out and add the activation counterpart Denis V. Lunev
                   ` (23 subsequent siblings)
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi, Alexander Ivanov

From: Alexander Ivanov <alexander.ivanov@virtuozzo.com>

After used bitmap freeng s->used_bmap points to the freed memory. If we try
to free used bitmap one more time it leads to double free error.

Set s->used_bmap to NULL to exclude double free error.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
Reviewed-by: Denis V. Lunev <den@openvz.org>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 block/parallels.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/block/parallels.c b/block/parallels.c
index 50748d1415..94692275c9 100644
--- a/block/parallels.c
+++ b/block/parallels.c
@@ -254,6 +254,7 @@ static void parallels_free_used_bitmap(BlockDriverState *bs)
     BDRVParallelsState *s = bs->opaque;
     s->used_bmap_size = 0;
     g_free(s->used_bmap);
+    s->used_bmap = NULL;
 }
 
 static int64_t coroutine_fn GRAPH_RDLOCK
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 02/25] parallels: split inactivation out and add the activation counterpart
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 01/25] parallels: Set s->used_bmap to NULL in parallels_free_used_bitmap() Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 03/25] iotests: cover inactivating a read-only node Denis V. Lunev
                   ` (22 subsequent siblings)
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi

From: Denis V. Lunev <den@openvz.org>

We are going to add parallels image extensions storage and need a
separate function for the inactivation code, which parallels_close()
carries inline today. Move it into parallels_inactivate() and register
it as .bdrv_inactivate, so the image is written out when the node is
inactivated and not only when it is closed.

The condition guarding the old call moves inside, as the handler is now
reached for any node: bdrv_inactivate_recurse() calls it and
blockdev-set-active can ask for it at any time. There is nothing to
write out for a node we can not write to, and trying turns a request
which has nothing to do into "Failed to inactivate node:
Operation not permitted".

Clearing the in use flag now needs someone to set it again.
parallels_open() is the only place doing that, and it does not run when
a node is made active again, which would leave an image whose header
says it was closed correctly while it is open for writing: a crash then
looks like a clean shutdown and the repair on open is skipped. Add
parallels_co_invalidate_cache(), the counterpart of the above, which
arms the flag again and reports through errp when it can not.

The flag may only be cleared once everything which can still fail has
succeeded, as a failed inactivation leaves the node writable and the
image has to keep saying so. The order is therefore the reverse of the
code being moved: truncate first, write the header last and answer with
the result of that write instead of ignoring it.

The migration blocker stays, though its comment asks for it to go once
an activate method exists. An activated node needs more than the in use
flag to be correct, as the used cluster bitmap describes the image as
it was before the handover.

Based on the original work from Alexander Ivanov.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 block/parallels.c | 46 ++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 38 insertions(+), 8 deletions(-)

diff --git a/block/parallels.c b/block/parallels.c
index 94692275c9..f02ad7a0be 100644
--- a/block/parallels.c
+++ b/block/parallels.c
@@ -1481,6 +1481,41 @@ fail:
     return ret;
 }
 
+static int GRAPH_RDLOCK parallels_inactivate(BlockDriverState *bs)
+{
+    BDRVParallelsState *s = bs->opaque;
+    int ret;
+
+    if (!(bs->open_flags & BDRV_O_RDWR) || (bs->open_flags & BDRV_O_INACTIVE)) {
+        return 0;
+    }
+
+    ret = bdrv_truncate(bs->file, s->data_end << BDRV_SECTOR_BITS, true,
+                        PREALLOC_MODE_OFF, 0, NULL);
+    if (ret < 0) {
+        return ret;
+    }
+
+    s->header->inuse = 0;
+    return parallels_update_header(bs);
+}
+
+static void coroutine_fn GRAPH_RDLOCK
+parallels_co_invalidate_cache(BlockDriverState *bs, Error **errp)
+{
+    BDRVParallelsState *s = bs->opaque;
+    int ret;
+
+    if (!(bs->open_flags & BDRV_O_RDWR)) {
+        return;
+    }
+
+    s->header->inuse = cpu_to_le32(HEADER_INUSE_MAGIC);
+    ret = parallels_update_header(bs);
+    if (ret < 0) {
+        error_setg_errno(errp, -ret, "Failed to mark the image in use");
+    }
+}
 
 static void parallels_close(BlockDriverState *bs)
 {
@@ -1488,14 +1523,7 @@ static void parallels_close(BlockDriverState *bs)
 
     GRAPH_RDLOCK_GUARD_MAINLOOP();
 
-    if ((bs->open_flags & BDRV_O_RDWR) && !(bs->open_flags & BDRV_O_INACTIVE)) {
-        s->header->inuse = 0;
-        parallels_update_header(bs);
-
-        /* errors are ignored, so we might as well pass exact=true */
-        bdrv_truncate(bs->file, s->data_end << BDRV_SECTOR_BITS, true,
-                      PREALLOC_MODE_OFF, 0, NULL);
-    }
+    parallels_inactivate(bs);
 
     parallels_free_used_bitmap(bs);
 
@@ -1533,6 +1561,8 @@ static BlockDriver bdrv_parallels = {
     .bdrv_co_check              = parallels_co_check,
     .bdrv_co_pdiscard           = parallels_co_pdiscard,
     .bdrv_co_pwrite_zeroes      = parallels_co_pwrite_zeroes,
+    .bdrv_co_invalidate_cache   = parallels_co_invalidate_cache,
+    .bdrv_inactivate            = parallels_inactivate,
 };
 
 static void bdrv_parallels_init(void)
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 03/25] iotests: cover inactivating a read-only node
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 01/25] parallels: Set s->used_bmap to NULL in parallels_free_used_bitmap() Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 02/25] parallels: split inactivation out and add the activation counterpart Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 04/25] parallels: Make mark_used() a global function Denis V. Lunev
                   ` (21 subsequent siblings)
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi

From: Denis V. Lunev <den@openvz.org>

A driver which writes something out on .bdrv_inactivate has nothing to
write for a node it can not write to, and has to answer such a request
with success rather than with the error its refused write produces.
The test inactivates nodes in several ways and never a read-only one,
so nothing caught that.

Add a read-only node and take it through a deactivation and back. The
case is generic, as every driver implementing the handler has to answer
the same way.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 tests/qemu-iotests/tests/inactive-node-nbd     | 14 ++++++++++++++
 tests/qemu-iotests/tests/inactive-node-nbd.out |  8 ++++++++
 2 files changed, 22 insertions(+)

diff --git a/tests/qemu-iotests/tests/inactive-node-nbd b/tests/qemu-iotests/tests/inactive-node-nbd
index 664157bfd0..24242265b1 100755
--- a/tests/qemu-iotests/tests/inactive-node-nbd
+++ b/tests/qemu-iotests/tests/inactive-node-nbd
@@ -47,6 +47,7 @@ def node_is_active(_vm, node_name):
     return node['active']
 
 with iotests.FilePath('disk.img') as path, \
+     iotests.FilePath('ro.img') as ro_path, \
      iotests.FilePath('snap.qcow2') as snap_path, \
      iotests.FilePath('snap2.qcow2') as snap2_path, \
      iotests.FilePath('target.img') as target_path, \
@@ -58,6 +59,7 @@ with iotests.FilePath('disk.img') as path, \
     iotests.log('Preparing disk...')
     iotests.qemu_img_create('-f', iotests.imgfmt, path, img_size)
     iotests.qemu_img_create('-f', iotests.imgfmt, target_path, img_size)
+    iotests.qemu_img_create('-f', iotests.imgfmt, ro_path, img_size)
 
     iotests.qemu_img_create('-f', 'qcow2', '-b', path, '-F', iotests.imgfmt,
                             snap_path)
@@ -70,6 +72,9 @@ with iotests.FilePath('disk.img') as path, \
                      'active=off')
     vm.add_blockdev(f'file,node-name=target-file,filename={target_path}')
     vm.add_blockdev(f'{iotests.imgfmt},file=target-file,node-name=target-fmt')
+    vm.add_blockdev(f'file,node-name=ro-file,filename={ro_path},read-only=on')
+    vm.add_blockdev(f'{iotests.imgfmt},file=ro-file,node-name=ro-fmt,'
+                     'read-only=on')
     vm.add_blockdev(f'file,node-name=snap-file,filename={snap_path}')
     vm.add_blockdev(f'file,node-name=snap2-file,filename={snap2_path}')
 
@@ -289,6 +294,15 @@ with iotests.FilePath('disk.img') as path, \
     iotests.log(qemu_io.cmd('map'), filters=[filter_qemu_io])
     qemu_io.close()
 
+    iotests.log('\n=== Inactivating a read-only node ===')
+
+    # A driver which writes something out on inactivation has nothing to
+    # write for a node it can not write to, and must not fail the request
+    vm.qmp_log('blockdev-set-active', node_name='ro-fmt', active=False)
+    iotests.log('ro-fmt active: %s' % node_is_active(vm, 'ro-fmt'))
+    vm.qmp_log('blockdev-set-active', node_name='ro-fmt', active=True)
+    iotests.log('ro-fmt active: %s' % node_is_active(vm, 'ro-fmt'))
+
     iotests.log('\n=== Resuming VM activates all images ===')
     vm.qmp_log('cont')
 
diff --git a/tests/qemu-iotests/tests/inactive-node-nbd.out b/tests/qemu-iotests/tests/inactive-node-nbd.out
index 0fb8c18d87..96af7608de 100644
--- a/tests/qemu-iotests/tests/inactive-node-nbd.out
+++ b/tests/qemu-iotests/tests/inactive-node-nbd.out
@@ -227,6 +227,14 @@ qemu-io: discard failed: Operation not permitted
 qemu-io: Failed to get allocation status: Operation not permitted
 
 
+=== Inactivating a read-only node ===
+{"execute": "blockdev-set-active", "arguments": {"active": false, "node-name": "ro-fmt"}}
+{"return": {}}
+ro-fmt active: False
+{"execute": "blockdev-set-active", "arguments": {"active": true, "node-name": "ro-fmt"}}
+{"return": {}}
+ro-fmt active: True
+
 === Resuming VM activates all images ===
 {"execute": "cont", "arguments": {}}
 {"return": {}}
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 04/25] parallels: Make mark_used() a global function
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (2 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 03/25] iotests: cover inactivating a read-only node Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 05/25] parallels: Limit search in parallels_mark_used to the last marked cluster Denis V. Lunev
                   ` (20 subsequent siblings)
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi, Alexander Ivanov

From: Alexander Ivanov <alexander.ivanov@virtuozzo.com>

We will need this function and a function for marking unused clusters (will
be added in the next patch) in parallels-ext.c too. Let it be a global
function parallels_mark_used().

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
Reviewed-by: Denis V. Lunev <den@openvz.org>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 block/parallels.c | 14 ++++++++------
 block/parallels.h |  3 +++
 2 files changed, 11 insertions(+), 6 deletions(-)

diff --git a/block/parallels.c b/block/parallels.c
index f02ad7a0be..6f7a9911d7 100644
--- a/block/parallels.c
+++ b/block/parallels.c
@@ -187,8 +187,8 @@ static void parallels_set_bat_entry(BDRVParallelsState *s,
     bitmap_set(s->bat_dirty_bmap, bat_entry_off(index) / s->bat_dirty_block, 1);
 }
 
-static int mark_used(BlockDriverState *bs, unsigned long *bitmap,
-                     uint32_t bitmap_size, int64_t off, uint32_t count)
+int parallels_mark_used(BlockDriverState *bs, unsigned long *bitmap,
+                        uint32_t bitmap_size, int64_t off, uint32_t count)
 {
     BDRVParallelsState *s = bs->opaque;
     uint32_t cluster_index = host_cluster_index(s, off);
@@ -241,7 +241,8 @@ static int GRAPH_RDLOCK parallels_fill_used_bitmap(BlockDriverState *bs)
             continue;
         }
 
-        err2 = mark_used(bs, s->used_bmap, s->used_bmap_size, host_off, 1);
+        err2 = parallels_mark_used(bs, s->used_bmap, s->used_bmap_size,
+                                   host_off, 1);
         if (err2 < 0 && err == 0) {
             err = err2;
         }
@@ -375,7 +376,8 @@ allocate_clusters(BlockDriverState *bs, int64_t sector_num,
         }
     }
 
-    ret = mark_used(bs, s->used_bmap, s->used_bmap_size, host_off, to_allocate);
+    ret = parallels_mark_used(bs, s->used_bmap, s->used_bmap_size,
+                              host_off, to_allocate);
     if (ret < 0) {
         /* Image consistency is broken. Alarm! */
         return ret;
@@ -840,7 +842,7 @@ parallels_check_duplicate(BlockDriverState *bs, BdrvCheckResult *res,
             continue;
         }
 
-        ret = mark_used(bs, bitmap, bitmap_size, host_off, 1);
+        ret = parallels_mark_used(bs, bitmap, bitmap_size, host_off, 1);
         assert(ret != -E2BIG);
         if (ret == 0) {
             continue;
@@ -900,7 +902,7 @@ parallels_check_duplicate(BlockDriverState *bs, BdrvCheckResult *res,
          * considered, and the bitmap size doesn't change. This specifically
          * means that -E2BIG is OK.
          */
-        ret = mark_used(bs, bitmap, bitmap_size, host_off, 1);
+        ret = parallels_mark_used(bs, bitmap, bitmap_size, host_off, 1);
         if (ret == -EBUSY) {
             res->check_errors++;
             goto out_repair_bat;
diff --git a/block/parallels.h b/block/parallels.h
index 423b2ad727..68077416b1 100644
--- a/block/parallels.h
+++ b/block/parallels.h
@@ -90,6 +90,9 @@ typedef struct BDRVParallelsState {
     Error *migration_blocker;
 } BDRVParallelsState;
 
+int parallels_mark_used(BlockDriverState *bs, unsigned long *bitmap,
+                        uint32_t bitmap_size, int64_t off, uint32_t count);
+
 int GRAPH_RDLOCK
 parallels_read_format_extension(BlockDriverState *bs, int64_t ext_off,
                                 Error **errp);
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 05/25] parallels: Limit search in parallels_mark_used to the last marked cluster
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (3 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 04/25] parallels: Make mark_used() a global function Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 06/25] parallels: Move host clusters allocation to a separate function Denis V. Lunev
                   ` (19 subsequent siblings)
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi

From: Denis V. Lunev <den@openvz.org>

There is no necessity to search to the end of the bitmap. Limit the
search area as cluster_index + count.

Add cluster_end variable to avoid its calculation in a few places.

Based on the original work from Alexander Ivanov.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 block/parallels.c | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/block/parallels.c b/block/parallels.c
index 6f7a9911d7..d537b0bb53 100644
--- a/block/parallels.c
+++ b/block/parallels.c
@@ -192,12 +192,14 @@ int parallels_mark_used(BlockDriverState *bs, unsigned long *bitmap,
 {
     BDRVParallelsState *s = bs->opaque;
     uint32_t cluster_index = host_cluster_index(s, off);
+    uint64_t cluster_end = (uint64_t)cluster_index + count;
     unsigned long next_used;
-    if ((uint64_t)cluster_index + count > bitmap_size) {
+
+    if (cluster_end > bitmap_size) {
         return -E2BIG;
     }
-    next_used = find_next_bit(bitmap, bitmap_size, cluster_index);
-    if (next_used < (uint64_t)cluster_index + count) {
+    next_used = find_next_bit(bitmap, cluster_end, cluster_index);
+    if (next_used < cluster_end) {
         return -EBUSY;
     }
     bitmap_set(bitmap, cluster_index, count);
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 06/25] parallels: Move host clusters allocation to a separate function
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (4 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 05/25] parallels: Limit search in parallels_mark_used to the last marked cluster Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 07/25] parallels: do not let the check die on what it is meant to report Denis V. Lunev
                   ` (18 subsequent siblings)
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi

From: Denis V. Lunev <den@openvz.org>

For parallels images extensions we need to allocate host clusters
without any connection to BAT. Move host clusters allocation code to
parallels_allocate_host_clusters().

This function can be called not only from coroutines so all the
*_co_* functions were replaced by corresponding wrappers.

Add parallels_mark_unused(), the helper releasing an area in the used
bitmap, as the new function needs it to undo an allocation.

The size of the request and the size of the area preallocated for it
live in two variables here, where the code being moved kept them in
one. The used bitmap has to grow by the latter, as it is what tells
the allocator how far the image reaches: counting only the requested
clusters hides the preallocated tail, so the next allocation starts
over at the end it knows about and preallocates the very same space
again.

data_end has to grow past an allocation which lands in the space
preallocated by an earlier one as well, not only past one which appends
to the image. The field marks the end of the payload for the truncation
on inactivation, so an allocation which leaves it behind is cut off the
image the moment the node is closed, and the data written into it is
lost.

Based on the original work from Alexander Ivanov.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 block/parallels.c                             | 145 +++++++++++-------
 block/parallels.h                             |   5 +
 tests/qemu-iotests/tests/parallels-checks     |  34 ++++
 tests/qemu-iotests/tests/parallels-checks.out |  17 ++
 4 files changed, 145 insertions(+), 56 deletions(-)

diff --git a/block/parallels.c b/block/parallels.c
index d537b0bb53..c96bed5ed3 100644
--- a/block/parallels.c
+++ b/block/parallels.c
@@ -206,6 +206,25 @@ int parallels_mark_used(BlockDriverState *bs, unsigned long *bitmap,
     return 0;
 }
 
+int parallels_mark_unused(BlockDriverState *bs, unsigned long *bitmap,
+                          uint32_t bitmap_size, int64_t off, uint32_t count)
+{
+    BDRVParallelsState *s = bs->opaque;
+    uint32_t cluster_index = host_cluster_index(s, off);
+    uint64_t cluster_end = (uint64_t)cluster_index + count;
+    unsigned long next_unused;
+
+    if (cluster_end > bitmap_size) {
+        return -E2BIG;
+    }
+    next_unused = find_next_zero_bit(bitmap, cluster_end, cluster_index);
+    if (next_unused < cluster_end) {
+        return -EINVAL;
+    }
+    bitmap_clear(bitmap, cluster_index, count);
+    return 0;
+}
+
 /*
  * Collect used bitmap. The image can contain errors, we should fill the
  * bitmap anyway, as much as we can. This information will be used for
@@ -260,42 +279,21 @@ static void parallels_free_used_bitmap(BlockDriverState *bs)
     s->used_bmap = NULL;
 }
 
-static int64_t coroutine_fn GRAPH_RDLOCK
-allocate_clusters(BlockDriverState *bs, int64_t sector_num,
-                  int nb_sectors, int *pnum)
+int64_t GRAPH_RDLOCK parallels_allocate_host_clusters(BlockDriverState *bs,
+                                                      int64_t *clusters)
 {
-    int ret = 0;
     BDRVParallelsState *s = bs->opaque;
-    int64_t i, pos, idx, to_allocate, first_free, host_off;
-
-    pos = block_status(s, sector_num, nb_sectors, pnum);
-    if (pos > 0) {
-        return pos;
-    }
-
-    idx = sector_num / s->tracks;
-    to_allocate = DIV_ROUND_UP(sector_num + *pnum, s->tracks) - idx;
-
-    /*
-     * This function is called only by parallels_co_writev(), which will never
-     * pass a sector_num at or beyond the end of the image (because the block
-     * layer never passes such a sector_num to that function). Therefore, idx
-     * is always below s->bat_size.
-     * block_status() will limit *pnum so that sector_num + *pnum will not
-     * exceed the image end. Therefore, idx + to_allocate cannot exceed
-     * s->bat_size.
-     * Note that s->bat_size is an unsigned int, therefore idx + to_allocate
-     * will always fit into a uint32_t.
-     */
-    assert(idx < s->bat_size && idx + to_allocate <= s->bat_size);
+    int64_t first_free, next_used, host_off, prealloc_clusters;
+    int64_t bytes, prealloc_bytes;
+    uint32_t new_usedsize;
+    int ret = 0;
 
     first_free = find_first_zero_bit(s->used_bmap, s->used_bmap_size);
     if (first_free == s->used_bmap_size) {
-        uint32_t new_usedsize;
-        int64_t bytes = to_allocate * s->cluster_size;
-        bytes += s->prealloc_size * BDRV_SECTOR_SIZE;
-
         host_off = s->data_end * BDRV_SECTOR_SIZE;
+        prealloc_clusters = *clusters + s->prealloc_size / s->tracks;
+        bytes = *clusters * s->cluster_size;
+        prealloc_bytes = prealloc_clusters * s->cluster_size;
 
         /*
          * We require the expanded size to read back as zero. If the
@@ -303,33 +301,29 @@ allocate_clusters(BlockDriverState *bs, int64_t sector_num,
          * force the safer-but-slower fallocate.
          */
         if (s->prealloc_mode == PRL_PREALLOC_MODE_TRUNCATE) {
-            ret = bdrv_co_truncate(bs->file, host_off + bytes,
-                                   false, PREALLOC_MODE_OFF,
-                                   BDRV_REQ_ZERO_WRITE, NULL);
+            ret = bdrv_truncate(bs->file, host_off + prealloc_bytes, false,
+                                PREALLOC_MODE_OFF, BDRV_REQ_ZERO_WRITE, NULL);
             if (ret == -ENOTSUP) {
                 s->prealloc_mode = PRL_PREALLOC_MODE_FALLOCATE;
             }
         }
         if (s->prealloc_mode == PRL_PREALLOC_MODE_FALLOCATE) {
-            ret = bdrv_co_pwrite_zeroes(bs->file, host_off, bytes, 0);
+            ret = bdrv_pwrite_zeroes(bs->file, host_off, prealloc_bytes, 0);
         }
         if (ret < 0) {
             return ret;
         }
 
-        new_usedsize = s->used_bmap_size + bytes / s->cluster_size;
+        new_usedsize = s->used_bmap_size + prealloc_bytes / s->cluster_size;
         s->used_bmap = bitmap_zero_extend(s->used_bmap, s->used_bmap_size,
                                           new_usedsize);
         s->used_bmap_size = new_usedsize;
     } else {
-        int64_t next_used;
         next_used = find_next_bit(s->used_bmap, s->used_bmap_size, first_free);
 
         /* Not enough continuous clusters in the middle, adjust the size */
-        if (next_used - first_free < to_allocate) {
-            to_allocate = next_used - first_free;
-            *pnum = (idx + to_allocate) * s->tracks - sector_num;
-        }
+        *clusters = MIN(*clusters, next_used - first_free);
+        bytes = *clusters * s->cluster_size;
 
         host_off = s->data_start * BDRV_SECTOR_SIZE;
         host_off += first_free * s->cluster_size;
@@ -341,14 +335,63 @@ allocate_clusters(BlockDriverState *bs, int64_t sector_num,
          */
         if (s->prealloc_mode == PRL_PREALLOC_MODE_FALLOCATE &&
                 host_off < s->data_end * BDRV_SECTOR_SIZE) {
-            ret = bdrv_co_pwrite_zeroes(bs->file, host_off,
-                                        s->cluster_size * to_allocate, 0);
+            ret = bdrv_pwrite_zeroes(bs->file, host_off, bytes, 0);
             if (ret < 0) {
                 return ret;
             }
         }
     }
 
+    if (host_off + bytes > s->data_end * BDRV_SECTOR_SIZE) {
+        s->data_end = (host_off + bytes) / BDRV_SECTOR_SIZE;
+    }
+
+    ret = parallels_mark_used(bs, s->used_bmap, s->used_bmap_size,
+                              host_off, *clusters);
+    if (ret < 0) {
+        /* Image consistency is broken. Alarm! */
+        return ret;
+    }
+
+    return host_off;
+}
+
+static int64_t coroutine_fn GRAPH_RDLOCK
+allocate_clusters(BlockDriverState *bs, int64_t sector_num,
+                  int nb_sectors, int *pnum)
+{
+    int ret = 0;
+    BDRVParallelsState *s = bs->opaque;
+    int64_t i, pos, idx, to_allocate, host_off;
+
+    pos = block_status(s, sector_num, nb_sectors, pnum);
+    if (pos > 0) {
+        return pos;
+    }
+
+    idx = sector_num / s->tracks;
+    to_allocate = DIV_ROUND_UP(sector_num + *pnum, s->tracks) - idx;
+
+    /*
+     * This function is called only by parallels_co_writev(), which will never
+     * pass a sector_num at or beyond the end of the image (because the block
+     * layer never passes such a sector_num to that function). Therefore, idx
+     * is always below s->bat_size.
+     * block_status() will limit *pnum so that sector_num + *pnum will not
+     * exceed the image end. Therefore, idx + to_allocate cannot exceed
+     * s->bat_size.
+     * Note that s->bat_size is an unsigned int, therefore idx + to_allocate
+     * will always fit into a uint32_t.
+     */
+    assert(idx < s->bat_size && idx + to_allocate <= s->bat_size);
+
+    host_off = parallels_allocate_host_clusters(bs, &to_allocate);
+    if (host_off < 0) {
+        return host_off;
+    }
+
+    *pnum = MIN(*pnum, (idx + to_allocate) * s->tracks - sector_num);
+
     /*
      * Try to read from backing to fill empty clusters
      * FIXME: 1. previous write_zeroes may be redundant
@@ -365,33 +408,23 @@ allocate_clusters(BlockDriverState *bs, int64_t sector_num,
 
         ret = bdrv_co_pread(bs->backing, idx * s->tracks * BDRV_SECTOR_SIZE,
                             nb_cow_bytes, buf, 0);
-        if (ret < 0) {
-            qemu_vfree(buf);
-            return ret;
+        if (ret == 0) {
+            ret = bdrv_co_pwrite(bs->file, host_off, nb_cow_bytes, buf, 0);
         }
 
-        ret = bdrv_co_pwrite(bs->file, s->data_end * BDRV_SECTOR_SIZE,
-                             nb_cow_bytes, buf, 0);
         qemu_vfree(buf);
         if (ret < 0) {
+            parallels_mark_unused(bs, s->used_bmap, s->used_bmap_size,
+                                  host_off, to_allocate);
             return ret;
         }
     }
 
-    ret = parallels_mark_used(bs, s->used_bmap, s->used_bmap_size,
-                              host_off, to_allocate);
-    if (ret < 0) {
-        /* Image consistency is broken. Alarm! */
-        return ret;
-    }
     for (i = 0; i < to_allocate; i++) {
         parallels_set_bat_entry(s, idx + i,
                 host_off / BDRV_SECTOR_SIZE / s->off_multiplier);
         host_off += s->cluster_size;
     }
-    if (host_off > s->data_end * BDRV_SECTOR_SIZE) {
-        s->data_end = host_off / BDRV_SECTOR_SIZE;
-    }
 
     return bat2sect(s, idx) + sector_num % s->tracks;
 }
diff --git a/block/parallels.h b/block/parallels.h
index 68077416b1..493c89e976 100644
--- a/block/parallels.h
+++ b/block/parallels.h
@@ -92,6 +92,11 @@ typedef struct BDRVParallelsState {
 
 int parallels_mark_used(BlockDriverState *bs, unsigned long *bitmap,
                         uint32_t bitmap_size, int64_t off, uint32_t count);
+int parallels_mark_unused(BlockDriverState *bs, unsigned long *bitmap,
+                          uint32_t bitmap_size, int64_t off, uint32_t count);
+
+int64_t GRAPH_RDLOCK parallels_allocate_host_clusters(BlockDriverState *bs,
+                                                      int64_t *clusters);
 
 int GRAPH_RDLOCK
 parallels_read_format_extension(BlockDriverState *bs, int64_t ext_off,
diff --git a/tests/qemu-iotests/tests/parallels-checks b/tests/qemu-iotests/tests/parallels-checks
index d2a08049d9..99af4c5f52 100755
--- a/tests/qemu-iotests/tests/parallels-checks
+++ b/tests/qemu-iotests/tests/parallels-checks
@@ -301,6 +301,40 @@ echo "$(peek_file_le "$TEST_IMG" $VICTIM_OFFSET 4)"
 echo "== data reads back correctly =="
 { $QEMU_IO -r -c "read -P 0x88 0 $SMALL_CLUSTER_SIZE" "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
 
+# Clear image
+_make_test_img $((64 * 1024 * 1024))
+
+echo "== TEST REUSE OF PREALLOCATED SPACE =="
+
+echo "== write 16 clusters, preallocating 16 of them at a time =="
+opts=(--image-opts "driver=$IMGFMT,file.filename=$TEST_IMG,prealloc-size=16M")
+for i in $(seq 0 15); do
+    opts+=(-c "write -P 0x11 $(($i * $CLUSTER_SIZE)) $CLUSTER_SIZE")
+done
+# Die before close(), which would truncate the preallocated tail away
+opts+=(-c "sigraise $(kill -l KILL)")
+orig_io_options=$QEMU_IO_OPTIONS
+QEMU_IO_OPTIONS=$QEMU_IO_OPTIONS_NO_FMT
+echo "clusters written: `$QEMU_IO "${opts[@]}" 2>&1 | grep -c '^wrote'`"
+QEMU_IO_OPTIONS=$orig_io_options
+
+echo "== the space preallocated first must have been handed out since =="
+file_size=`stat --printf="%s" "$TEST_IMG"`
+echo "clusters behind the header: $(($file_size / $CLUSTER_SIZE - 1))"
+
+# Clear image
+_make_test_img $SIZE
+
+echo "== the second cluster comes from the space preallocated for the first =="
+{ $QEMU_IO -c "write -P 0x11 0 $CLUSTER_SIZE" \
+           -c "write -P 0x22 $CLUSTER_SIZE $CLUSTER_SIZE" \
+           "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
+
+echo "== both of them survive the close =="
+{ $QEMU_IO -r -c "read -P 0x11 0 $CLUSTER_SIZE" \
+              -c "read -P 0x22 $CLUSTER_SIZE $CLUSTER_SIZE" \
+              "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
+
 # success, all done
 echo "*** done"
 rm -f $seq.full
diff --git a/tests/qemu-iotests/tests/parallels-checks.out b/tests/qemu-iotests/tests/parallels-checks.out
index c33f3852a8..51eb3f1ef1 100644
--- a/tests/qemu-iotests/tests/parallels-checks.out
+++ b/tests/qemu-iotests/tests/parallels-checks.out
@@ -182,4 +182,21 @@ wrote 512/512 bytes at offset 0
 == data reads back correctly ==
 read 512/512 bytes at offset 0
 512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=67108864
+== TEST REUSE OF PREALLOCATED SPACE ==
+== write 16 clusters, preallocating 16 of them at a time ==
+clusters written: 16
+== the space preallocated first must have been handed out since ==
+clusters behind the header: 17
+Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=4194304
+== the second cluster comes from the space preallocated for the first ==
+wrote 1048576/1048576 bytes at offset 0
+1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+wrote 1048576/1048576 bytes at offset 1048576
+1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+== both of them survive the close ==
+read 1048576/1048576 bytes at offset 0
+1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+read 1048576/1048576 bytes at offset 1048576
+1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
 *** done
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 07/25] parallels: do not let the check die on what it is meant to report
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (5 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 06/25] parallels: Move host clusters allocation to a separate function Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 08/25] parallels: Create used bitmap even if checks needed Denis V. Lunev
                   ` (17 subsequent siblings)
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi

From: Denis V. Lunev <den@openvz.org>

parallels_check_duplicate() sizes its local bitmap by the end of the
payload and asserts that every BAT entry fits into it. An entry
pointing at a cluster which runs past the end of the image file does
not fit, so a plain 'qemu-img check' on such an image dies:

    qemu-img: block/parallels.c:843: parallels_check_duplicate:
    Assertion `ret != -E2BIG' failed.

A repairing check survives by accident, as
parallels_check_outside_image() clears the entry before the duplicate
check gets to see it. There is nothing for the duplicate check to do
with such a cluster anyway, as the corruption has already been
reported, so skip it.

The answer parallels_mark_used() gives has to stay out of ret, which is
what the function returns once the loop is over. -EBUSY for the
duplicate this function exists to find is left in ret whenever the last
allocated BAT entry is the duplicated one and the check is not
repairing. 'qemu-img check' then ends with

    ERROR duplicate offset in BAT entry 1
    qemu-img: Check failed: Device or resource busy

and prints no summary at all, so the corruption it just found is
reported as a failure to look. Keep the answer in a variable of its
own, as the only errors worth returning from here are the I/O ones,
and those leave the loop where they happen.

The tests write two clusters and damage the second entry, one by
cutting the cluster in half so that it no longer fits the file, one by
pointing it at the first cluster.

Fixes: a398275e88 ("parallels: create mark_used() helper which sets bit in used bitmap")
Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 block/parallels.c                             | 12 ++++---
 tests/qemu-iotests/tests/parallels-checks     | 33 ++++++++++++++++++
 tests/qemu-iotests/tests/parallels-checks.out | 34 +++++++++++++++++++
 3 files changed, 74 insertions(+), 5 deletions(-)

diff --git a/block/parallels.c b/block/parallels.c
index c96bed5ed3..cacf23143b 100644
--- a/block/parallels.c
+++ b/block/parallels.c
@@ -872,14 +872,16 @@ parallels_check_duplicate(BlockDriverState *bs, BdrvCheckResult *res,
     buf = qemu_blockalign(bs, s->cluster_size);
 
     for (i = 0; i < s->bat_size; i++) {
+        int used;
+
         host_off = bat2sect(s, i) << BDRV_SECTOR_BITS;
         if (host_off == 0) {
             continue;
         }
 
-        ret = parallels_mark_used(bs, bitmap, bitmap_size, host_off, 1);
-        assert(ret != -E2BIG);
-        if (ret == 0) {
+        used = parallels_mark_used(bs, bitmap, bitmap_size, host_off, 1);
+        if (used == 0 || used == -E2BIG) {
+            /* parallels_check_outside_image() reports the -E2BIG one */
             continue;
         }
 
@@ -937,8 +939,8 @@ parallels_check_duplicate(BlockDriverState *bs, BdrvCheckResult *res,
          * considered, and the bitmap size doesn't change. This specifically
          * means that -E2BIG is OK.
          */
-        ret = parallels_mark_used(bs, bitmap, bitmap_size, host_off, 1);
-        if (ret == -EBUSY) {
+        used = parallels_mark_used(bs, bitmap, bitmap_size, host_off, 1);
+        if (used == -EBUSY) {
             res->check_errors++;
             goto out_repair_bat;
         }
diff --git a/tests/qemu-iotests/tests/parallels-checks b/tests/qemu-iotests/tests/parallels-checks
index 99af4c5f52..cf90eaf152 100755
--- a/tests/qemu-iotests/tests/parallels-checks
+++ b/tests/qemu-iotests/tests/parallels-checks
@@ -335,6 +335,39 @@ echo "== both of them survive the close =="
               -c "read -P 0x22 $CLUSTER_SIZE $CLUSTER_SIZE" \
               "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
 
+# Clear image
+_make_test_img $SIZE
+
+echo "== TEST A CLUSTER WHICH RUNS PAST THE END OF THE FILE =="
+
+echo "== write two clusters =="
+{ $QEMU_IO -c "write -P 0x11 0 $CLUSTER_SIZE" \
+           -c "write -P 0x22 $CLUSTER_SIZE $CLUSTER_SIZE" \
+           "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
+
+echo "== cut the second one in half =="
+file_size=`stat --printf="%s" "$TEST_IMG"`
+truncate -s $((file_size - CLUSTER_SIZE / 2)) "$TEST_IMG"
+
+echo "== the check completes and reports the cluster =="
+_check_test_img
+
+# Clear image
+_make_test_img $SIZE
+
+echo "== TEST A DUPLICATE IN THE LAST ALLOCATED BAT ENTRY =="
+
+echo "== write two clusters =="
+{ $QEMU_IO -c "write -P 0x11 0 $CLUSTER_SIZE" \
+           -c "write -P 0x22 $CLUSTER_SIZE $CLUSTER_SIZE" \
+           "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
+
+echo "== point the second entry at the first cluster =="
+poke_file "$TEST_IMG" "$(($BAT_OFFSET + 4))" "\x01\x00\x00\x00"
+
+echo "== the check completes and reports the duplicate =="
+_check_test_img
+
 # success, all done
 echo "*** done"
 rm -f $seq.full
diff --git a/tests/qemu-iotests/tests/parallels-checks.out b/tests/qemu-iotests/tests/parallels-checks.out
index 51eb3f1ef1..645c4b3679 100644
--- a/tests/qemu-iotests/tests/parallels-checks.out
+++ b/tests/qemu-iotests/tests/parallels-checks.out
@@ -199,4 +199,38 @@ read 1048576/1048576 bytes at offset 0
 1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
 read 1048576/1048576 bytes at offset 1048576
 1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=4194304
+== TEST A CLUSTER WHICH RUNS PAST THE END OF THE FILE ==
+== write two clusters ==
+wrote 1048576/1048576 bytes at offset 0
+1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+wrote 1048576/1048576 bytes at offset 1048576
+1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+== cut the second one in half ==
+== the check completes and reports the cluster ==
+ERROR cluster 1 is outside image
+ERROR space leaked at the end of the image 524288
+
+1 errors were found on the image.
+Data may be corrupted, or further writes to the image may corrupt it.
+
+1 leaked clusters were found on the image.
+This means waste of disk space, but no harm to data.
+Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=4194304
+== TEST A DUPLICATE IN THE LAST ALLOCATED BAT ENTRY ==
+== write two clusters ==
+wrote 1048576/1048576 bytes at offset 0
+1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+wrote 1048576/1048576 bytes at offset 1048576
+1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+== point the second entry at the first cluster ==
+== the check completes and reports the duplicate ==
+ERROR space leaked at the end of the image 1048576
+ERROR duplicate offset in BAT entry 1
+
+1 errors were found on the image.
+Data may be corrupted, or further writes to the image may corrupt it.
+
+1 leaked clusters were found on the image.
+This means waste of disk space, but no harm to data.
 *** done
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 08/25] parallels: Create used bitmap even if checks needed
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (6 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 07/25] parallels: do not let the check die on what it is meant to report Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 09/25] parallels: Drop unused clusters at the end of the image Denis V. Lunev
                   ` (16 subsequent siblings)
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi, Alexander Ivanov

From: Alexander Ivanov <alexander.ivanov@virtuozzo.com>

All the checks were fixed to work with used bitmap. Create used bitmap in
parallels_open() even if need_check is true.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 block/parallels.c | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

diff --git a/block/parallels.c b/block/parallels.c
index cacf23143b..307e90ec71 100644
--- a/block/parallels.c
+++ b/block/parallels.c
@@ -1476,13 +1476,11 @@ static int parallels_open(BlockDriverState *bs, QDict *options, int flags,
         }
     }
 
-    if (!need_check) {
-        ret = parallels_fill_used_bitmap(bs);
-        if (ret == -ENOMEM) {
-            goto fail;
-        }
-        need_check = need_check || ret < 0; /* These are correctable errors */
+    ret = parallels_fill_used_bitmap(bs);
+    if (ret == -ENOMEM) {
+        goto fail;
     }
+    need_check = need_check || ret < 0; /* These are correctable errors */
 
     /*
      * We don't repair the image here if it's opened for checks. Also we don't
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 09/25] parallels: Drop unused clusters at the end of the image
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (7 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 08/25] parallels: Create used bitmap even if checks needed Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 10/25] parallels: Remove unnecessary data_end field Denis V. Lunev
                   ` (15 subsequent siblings)
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi

From: Denis V. Lunev <den@openvz.org>

On an image closing there can be unused clusters in the end of the
image. Since we have used bitmap, they can be found without a leak
check, so parallels_check_unused_clusters() answers both questions:
inactivation asks it to truncate them away, and the leak check asks it
how much there is.

A repairing leak check truncates the file, so the used bitmap is
recreated afterwards, as it would no longer comply to it.

The helper answers with the size of the leak, which is a byte count and
does not belong in an int. parallels_inactivate() keeps it in an int64_t
and reports only a failure, as a leak of 2 GiB or more would otherwise
look like one and fail the inactivation of an image which was truncated
just fine.

A BAT entry pointing at a cluster which does not fit the file makes the
used bitmap reach further than the file does, and that difference is
not a leak of negative size. parallels_check_outside_image() reports it
as corruption on its own, so answer with no leak at all rather than
-EINVAL, which would fail the whole check before the duplicate check
and the statistics ever run.

Repairing such an entry clears it from the BAT, and the used bitmap has
to follow the way parallels_check_data_off() already makes it follow a
repaired data_off. Otherwise it keeps a bit for a cluster the image no
longer has, and 'qemu-img check -r all' leaves the space behind it
untouched instead of truncating it away.

Rebuilding the used bitmap after the truncation fails with -EBUSY or
-E2BIG on a BAT which points a cluster twice or out of the image.
parallels_open() calls those correctable and repairs them, so they must
not abort the check that is meant to do the repairing.

Based on the original work from Alexander Ivanov.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 block/parallels.c                             | 131 +++++++++++++-----
 tests/qemu-iotests/tests/parallels-checks     |  11 ++
 tests/qemu-iotests/tests/parallels-checks.out |  22 ++-
 3 files changed, 126 insertions(+), 38 deletions(-)

diff --git a/block/parallels.c b/block/parallels.c
index 307e90ec71..2be7c20338 100644
--- a/block/parallels.c
+++ b/block/parallels.c
@@ -750,6 +750,7 @@ parallels_check_outside_image(BlockDriverState *bs, BdrvCheckResult *res,
     BDRVParallelsState *s = bs->opaque;
     uint32_t i;
     int64_t off, high_off, size, data_start_off;
+    bool fixed = false;
 
     size = bdrv_co_getlength(bs->file->bs);
     if (size < 0) {
@@ -771,6 +772,7 @@ parallels_check_outside_image(BlockDriverState *bs, BdrvCheckResult *res,
             if (fix & BDRV_FIX_ERRORS) {
                 parallels_set_bat_entry(s, i, 0);
                 res->corruptions_fixed++;
+                fixed = true;
             }
             continue;
         }
@@ -779,6 +781,17 @@ parallels_check_outside_image(BlockDriverState *bs, BdrvCheckResult *res,
         }
     }
 
+    if (fixed) {
+        int err;
+
+        parallels_free_used_bitmap(bs);
+        err = parallels_fill_used_bitmap(bs);
+        if (err == -ENOMEM) {
+            res->check_errors++;
+            return err;
+        }
+    }
+
     if (high_off == 0) {
         res->image_end_offset = s->data_end << BDRV_SECTOR_BITS;
     } else {
@@ -786,51 +799,96 @@ parallels_check_outside_image(BlockDriverState *bs, BdrvCheckResult *res,
         s->data_end = res->image_end_offset >> BDRV_SECTOR_BITS;
     }
 
+
     return 0;
 }
 
+static int64_t GRAPH_RDLOCK
+parallels_check_unused_clusters(BlockDriverState *bs, bool truncate)
+{
+    BDRVParallelsState *s = bs->opaque;
+    int64_t leak, file_size, end_off = 0;
+    int ret;
+
+    file_size = bdrv_getlength(bs->file->bs);
+    if (file_size < 0) {
+        return file_size;
+    }
+
+    if (s->used_bmap_size > 0) {
+        end_off = find_last_bit(s->used_bmap, s->used_bmap_size);
+        if (end_off == s->used_bmap_size) {
+            end_off = 0;
+        } else {
+            end_off = (end_off + 1) * s->cluster_size;
+        }
+    }
+
+    end_off += s->data_start * BDRV_SECTOR_SIZE;
+
+    /*
+     * A cluster in use behind the end of the file is corruption which
+     * parallels_check_outside_image() reports on its own. There is no
+     * leaked space to reclaim behind it, and nothing to truncate.
+     */
+    if (end_off >= file_size) {
+        return 0;
+    }
+
+    leak = file_size - end_off;
+    if (!truncate) {
+        return leak;
+    }
+
+    ret = bdrv_truncate(bs->file, end_off, true, PREALLOC_MODE_OFF, 0, NULL);
+    if (ret) {
+        return ret;
+    }
+
+    parallels_free_used_bitmap(bs);
+    ret = parallels_fill_used_bitmap(bs);
+    if (ret == -ENOMEM) {
+        return ret;
+    }
+
+    return leak;
+}
+
 static int coroutine_fn GRAPH_RDLOCK
 parallels_check_leak(BlockDriverState *bs, BdrvCheckResult *res,
                      BdrvCheckMode fix, bool explicit)
 {
     BDRVParallelsState *s = bs->opaque;
-    int64_t size;
-    int ret;
+    int64_t leak, count, size;
+
+    leak = parallels_check_unused_clusters(bs, fix & BDRV_FIX_LEAKS);
+    if (leak < 0) {
+        res->check_errors++;
+        return leak;
+    }
+    if (leak == 0) {
+        return 0;
+    }
 
     size = bdrv_co_getlength(bs->file->bs);
     if (size < 0) {
         res->check_errors++;
         return size;
     }
+    res->image_end_offset = size;
 
-    if (size > res->image_end_offset) {
-        int64_t count;
-        count = DIV_ROUND_UP(size - res->image_end_offset, s->cluster_size);
-        if (explicit) {
-            fprintf(stderr,
-                    "%s space leaked at the end of the image %" PRId64 "\n",
-                    fix & BDRV_FIX_LEAKS ? "Repairing" : "ERROR",
-                    size - res->image_end_offset);
-            res->leaks += count;
-        }
-        if (fix & BDRV_FIX_LEAKS) {
-            Error *local_err = NULL;
+    if (!explicit) {
+        return 0;
+    }
 
-            /*
-             * In order to really repair the image, we must shrink it.
-             * That means we have to pass exact=true.
-             */
-            ret = bdrv_co_truncate(bs->file, res->image_end_offset, true,
-                                   PREALLOC_MODE_OFF, 0, &local_err);
-            if (ret < 0) {
-                error_report_err(local_err);
-                res->check_errors++;
-                return ret;
-            }
-            if (explicit) {
-                res->leaks_fixed += count;
-            }
-        }
+    count = DIV_ROUND_UP(leak, s->cluster_size);
+    fprintf(stderr,
+            "%s space leaked at the end of the image %" PRId64 "\n",
+            fix & BDRV_FIX_LEAKS ? "Repairing" : "ERROR", leak);
+    res->leaks += count;
+
+    if (fix & BDRV_FIX_LEAKS) {
+        res->leaks_fixed += count;
     }
 
     return 0;
@@ -849,7 +907,10 @@ parallels_check_duplicate(BlockDriverState *bs, BdrvCheckResult *res,
     bool fixed = false;
 
     /*
-     * Create a bitmap of used clusters.
+     * Create a bitmap of used clusters. Please note that this bitmap is not
+     * related to used_bmap field in BDRVParallelsState and is created only for
+     * local usage.
+     *
      * If a bit is set, there is a BAT entry pointing to this cluster.
      * Loop through the BAT entries, check bits relevant to an entry offset.
      * If bit is set, this entry is duplicated. Otherwise set the bit.
@@ -1521,16 +1582,16 @@ fail:
 static int GRAPH_RDLOCK parallels_inactivate(BlockDriverState *bs)
 {
     BDRVParallelsState *s = bs->opaque;
-    int ret;
+    int64_t leak;
 
     if (!(bs->open_flags & BDRV_O_RDWR) || (bs->open_flags & BDRV_O_INACTIVE)) {
         return 0;
     }
 
-    ret = bdrv_truncate(bs->file, s->data_end << BDRV_SECTOR_BITS, true,
-                        PREALLOC_MODE_OFF, 0, NULL);
-    if (ret < 0) {
-        return ret;
+    leak = parallels_check_unused_clusters(bs, true);
+    if (leak < 0) {
+        error_report("Failed to truncate image: %s", strerror(-leak));
+        return leak;
     }
 
     s->header->inuse = 0;
diff --git a/tests/qemu-iotests/tests/parallels-checks b/tests/qemu-iotests/tests/parallels-checks
index cf90eaf152..c9dcd715ac 100755
--- a/tests/qemu-iotests/tests/parallels-checks
+++ b/tests/qemu-iotests/tests/parallels-checks
@@ -352,6 +352,17 @@ truncate -s $((file_size - CLUSTER_SIZE / 2)) "$TEST_IMG"
 echo "== the check completes and reports the cluster =="
 _check_test_img
 
+echo "== nothing can be reclaimed behind it =="
+_check_test_img -r leaks
+echo "file size: `stat --printf="%s" "$TEST_IMG"`"
+
+echo "== a full repair drops the entry and truncates the image =="
+_check_test_img -r all
+echo "file size: `stat --printf="%s" "$TEST_IMG"`"
+
+echo "== the first cluster survived =="
+{ $QEMU_IO -c "read -P 0x11 0 $CLUSTER_SIZE" "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
+
 # Clear image
 _make_test_img $SIZE
 
diff --git a/tests/qemu-iotests/tests/parallels-checks.out b/tests/qemu-iotests/tests/parallels-checks.out
index 645c4b3679..6699848996 100644
--- a/tests/qemu-iotests/tests/parallels-checks.out
+++ b/tests/qemu-iotests/tests/parallels-checks.out
@@ -209,13 +209,29 @@ wrote 1048576/1048576 bytes at offset 1048576
 == cut the second one in half ==
 == the check completes and reports the cluster ==
 ERROR cluster 1 is outside image
-ERROR space leaked at the end of the image 524288
 
 1 errors were found on the image.
 Data may be corrupted, or further writes to the image may corrupt it.
+== nothing can be reclaimed behind it ==
+ERROR cluster 1 is outside image
 
-1 leaked clusters were found on the image.
-This means waste of disk space, but no harm to data.
+1 errors were found on the image.
+Data may be corrupted, or further writes to the image may corrupt it.
+file size: 2621440
+== a full repair drops the entry and truncates the image ==
+Repairing cluster 1 is outside image
+Repairing space leaked at the end of the image 524288
+The following inconsistencies were found and repaired:
+
+    1 leaked clusters
+    1 corruptions
+
+Double checking the fixed image now...
+No errors were found on the image.
+file size: 2097152
+== the first cluster survived ==
+read 1048576/1048576 bytes at offset 0
+1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
 Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=4194304
 == TEST A DUPLICATE IN THE LAST ALLOCATED BAT ENTRY ==
 == write two clusters ==
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 10/25] parallels: Remove unnecessary data_end field
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (8 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 09/25] parallels: Drop unused clusters at the end of the image Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 11/25] parallels: Add dirty bitmaps saving Denis V. Lunev
                   ` (14 subsequent siblings)
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi

From: Denis V. Lunev <den@openvz.org>

Since we have used bitmap, field data_end in BDRVParallelsState is
redundant and can be removed.

Add parallels_data_end() helper and remove data_end handling.

The two are not equivalent, which is why this comes before the Format
Extension is stored. data_end is the highest extent seen in the BAT,
while the helper derives the end of the payload from the image file. A
cluster which belongs to the image without being referenced by the BAT,
as the Format Extension and its bitmap data clusters are, stays
invisible to the field: parallels_allocate_host_clusters() appends at
data_end, so it would hand out an offset which is already occupied and
parallels_mark_used() would refuse it with -EBUSY, failing the guest
write.

seek_to_sector() validates a BAT entry against data_end, so it becomes
a user of the new helper, which suits that check better as well: a
cluster has to live inside the image file, while the field could grow
to whatever extent a corrupted BAT entry claimed.

The BAT scan in parallels_open() no longer tracks the maximum extent,
but it keeps rejecting entries below data_start or beyond the end of the
file, so need_check is still set when an entry is out of bounds. Nothing
is accumulated any more, so the scan stops at the first such entry.
high_off in parallels_check_outside_image() only fed the
image_end_offset which the helper now provides, so it goes away with
the field.

The bdrv_pwrite_zeroes() of the branch which reuses a hole goes as well.
It was guarded by data_end, and it is redundant: the space was already
fallocated when the image grew over it. The 'bytes' variable goes with
it, as the used bitmap grows by the preallocated size rather than the
requested one.

Based on the original work from Alexander Ivanov.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 block/parallels.c | 57 +++++++++++++----------------------------------
 block/parallels.h |  1 -
 2 files changed, 15 insertions(+), 43 deletions(-)

diff --git a/block/parallels.c b/block/parallels.c
index 2be7c20338..ace79ad968 100644
--- a/block/parallels.c
+++ b/block/parallels.c
@@ -116,6 +116,13 @@ static uint32_t bat_entry_off(uint32_t idx)
     return sizeof(ParallelsHeader) + sizeof(uint32_t) * idx;
 }
 
+static int64_t parallels_data_end(BDRVParallelsState *s)
+{
+    int64_t data_end = s->data_start * BDRV_SECTOR_SIZE;
+    data_end += s->used_bmap_size * s->cluster_size;
+    return data_end;
+}
+
 static int64_t seek_to_sector(BDRVParallelsState *s, int64_t sector_num)
 {
     uint32_t index, offset;
@@ -130,7 +137,8 @@ static int64_t seek_to_sector(BDRVParallelsState *s, int64_t sector_num)
     }
 
     cluster_off = bat2sect(s, index);
-    if (cluster_off < s->data_start || cluster_off + s->tracks > s->data_end) {
+    if (cluster_off < s->data_start ||
+        cluster_off + s->tracks > parallels_data_end(s) >> BDRV_SECTOR_BITS) {
         /* Cluster is outside of the image file or overlaps the header. */
         return -1;
     }
@@ -284,15 +292,14 @@ int64_t GRAPH_RDLOCK parallels_allocate_host_clusters(BlockDriverState *bs,
 {
     BDRVParallelsState *s = bs->opaque;
     int64_t first_free, next_used, host_off, prealloc_clusters;
-    int64_t bytes, prealloc_bytes;
+    int64_t prealloc_bytes;
     uint32_t new_usedsize;
     int ret = 0;
 
     first_free = find_first_zero_bit(s->used_bmap, s->used_bmap_size);
     if (first_free == s->used_bmap_size) {
-        host_off = s->data_end * BDRV_SECTOR_SIZE;
+        host_off = parallels_data_end(s);
         prealloc_clusters = *clusters + s->prealloc_size / s->tracks;
-        bytes = *clusters * s->cluster_size;
         prealloc_bytes = prealloc_clusters * s->cluster_size;
 
         /*
@@ -323,27 +330,9 @@ int64_t GRAPH_RDLOCK parallels_allocate_host_clusters(BlockDriverState *bs,
 
         /* Not enough continuous clusters in the middle, adjust the size */
         *clusters = MIN(*clusters, next_used - first_free);
-        bytes = *clusters * s->cluster_size;
 
         host_off = s->data_start * BDRV_SECTOR_SIZE;
         host_off += first_free * s->cluster_size;
-
-        /*
-         * No need to preallocate if we are using tail area from the above
-         * branch. In the other case we are likely re-using hole. Preallocate
-         * the space if required by the prealloc_mode.
-         */
-        if (s->prealloc_mode == PRL_PREALLOC_MODE_FALLOCATE &&
-                host_off < s->data_end * BDRV_SECTOR_SIZE) {
-            ret = bdrv_pwrite_zeroes(bs->file, host_off, bytes, 0);
-            if (ret < 0) {
-                return ret;
-            }
-        }
-    }
-
-    if (host_off + bytes > s->data_end * BDRV_SECTOR_SIZE) {
-        s->data_end = (host_off + bytes) / BDRV_SECTOR_SIZE;
     }
 
     ret = parallels_mark_used(bs, s->used_bmap, s->used_bmap_size,
@@ -749,7 +738,7 @@ parallels_check_outside_image(BlockDriverState *bs, BdrvCheckResult *res,
 {
     BDRVParallelsState *s = bs->opaque;
     uint32_t i;
-    int64_t off, high_off, size, data_start_off;
+    int64_t off, size, data_start_off;
     bool fixed = false;
 
     size = bdrv_co_getlength(bs->file->bs);
@@ -759,7 +748,6 @@ parallels_check_outside_image(BlockDriverState *bs, BdrvCheckResult *res,
     }
     data_start_off = s->data_start << BDRV_SECTOR_BITS;
 
-    high_off = 0;
     for (i = 0; i < s->bat_size; i++) {
         off = bat2sect(s, i) << BDRV_SECTOR_BITS;
         if (off == 0) {
@@ -774,10 +762,6 @@ parallels_check_outside_image(BlockDriverState *bs, BdrvCheckResult *res,
                 res->corruptions_fixed++;
                 fixed = true;
             }
-            continue;
-        }
-        if (high_off < off) {
-            high_off = off;
         }
     }
 
@@ -792,14 +776,7 @@ parallels_check_outside_image(BlockDriverState *bs, BdrvCheckResult *res,
         }
     }
 
-    if (high_off == 0) {
-        res->image_end_offset = s->data_end << BDRV_SECTOR_BITS;
-    } else {
-        res->image_end_offset = high_off + s->cluster_size;
-        s->data_end = res->image_end_offset >> BDRV_SECTOR_BITS;
-    }
-
-
+    res->image_end_offset = parallels_data_end(s);
     return 0;
 }
 
@@ -1466,8 +1443,7 @@ static int parallels_open(BlockDriverState *bs, QDict *options, int flags,
     }
 
     s->data_start = data_start;
-    s->data_end = s->data_start;
-    if (s->data_end < (s->header_size >> BDRV_SECTOR_BITS)) {
+    if (s->data_start < (s->header_size >> BDRV_SECTOR_BITS)) {
         /*
          * There is not enough unused space to fit to block align between BAT
          * and actual data. We can't avoid read-modify-write...
@@ -1530,10 +1506,7 @@ static int parallels_open(BlockDriverState *bs, QDict *options, int flags,
         if (sector < data_start || sector + s->tracks > file_nb_sectors) {
             /* Cluster is outside of the image file or overlaps the header. */
             need_check = true;
-            continue;
-        }
-        if (sector + s->tracks > s->data_end) {
-            s->data_end = sector + s->tracks;
+            break;
         }
     }
 
diff --git a/block/parallels.h b/block/parallels.h
index 493c89e976..eb90aeea81 100644
--- a/block/parallels.h
+++ b/block/parallels.h
@@ -79,7 +79,6 @@ typedef struct BDRVParallelsState {
     unsigned int bat_size;
 
     int64_t  data_start;
-    int64_t  data_end;
     uint64_t prealloc_size;
     ParallelsPreallocMode prealloc_mode;
 
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 11/25] parallels: Add dirty bitmaps saving
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (9 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 10/25] parallels: Remove unnecessary data_end field Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 12/25] parallels: Let image extensions work in RW mode Denis V. Lunev
                   ` (13 subsequent siblings)
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi

From: Denis V. Lunev <den@openvz.org>

Dirty bitmaps can be loaded now, but there is no way to save them. Add
code for dirty bitmap storage.

A bitmap which can not be stored is refused when it is created, by
parallels_co_can_store_new_dirty_bitmap(), as at store time there is
nowhere left to report it to: it would be dropped while the command
which asked to persist it still succeeded. The format identifies a
bitmap by a UUID, so a name which does not parse as one is refused, and
so is one whose L1 table does not fit the Format Extension cluster next
to the feature headers of the bitmaps already stored and the end of
features marker. The hook and the store path share the size arithmetic.

Losing a bitmap on an I/O error is still possible, and it used to be
silent. Report it through errp and answer -EINVAL from
parallels_inactivate(), the way qcow2_inactivate() does, giving up
before the in use flag is cleared: the inactivation failed, the node
stays writable, and the next open must not be told that the image was
closed correctly.

The extension is rebuilt as a whole and written to freshly allocated
clusters on every store, and its clusters are deliberately absent from
used_bmap, which is what lets the space of the copy read at open time
be reused. parallels_check_unused_clusters() derives the end of the
payload from used_bmap alone, though, so it would report the extension
as a leak and 'qemu-img check -r leaks' would truncate it away while
the header still points at it. Remember where the extension ends in
s->ext_end and use it as a lower bound for the end of the payload.

Based on the original work from Alexander Ivanov.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 block/parallels-ext.c | 295 +++++++++++++++++++++++++++++++++++++++++-
 block/parallels.c     |  12 ++
 block/parallels.h     |   7 +
 3 files changed, 312 insertions(+), 2 deletions(-)

diff --git a/block/parallels-ext.c b/block/parallels-ext.c
index 21f54e4e3e..a0e2be395f 100644
--- a/block/parallels-ext.c
+++ b/block/parallels-ext.c
@@ -24,6 +24,7 @@
  */
 
 #include "qemu/osdep.h"
+#include "qemu/error-report.h"
 #include "qapi/error.h"
 #include "block/block-io.h"
 #include "block/block_int.h"
@@ -96,8 +97,9 @@ parallels_load_bitmap_data(BlockDriverState *bs, const uint64_t *l1_table,
         if (entry == 1) {
             bdrv_dirty_bitmap_deserialize_ones(bitmap, offset, count, false);
         } else {
-            ret = bdrv_pread(bs->file, entry << BDRV_SECTOR_BITS,
-                             s->cluster_size, buf, 0);
+            int64_t host_off = entry << BDRV_SECTOR_BITS;
+
+            ret = bdrv_pread(bs->file, host_off, s->cluster_size, buf, 0);
             if (ret < 0) {
                 error_setg_errno(errp, -ret,
                                  "Failed to read bitmap data cluster");
@@ -105,6 +107,7 @@ parallels_load_bitmap_data(BlockDriverState *bs, const uint64_t *l1_table,
             }
             bdrv_dirty_bitmap_deserialize_part(bitmap, buf, offset, count,
                                                false);
+            s->ext_end = MAX(s->ext_end, host_off + s->cluster_size);
         }
     }
     ret = 0;
@@ -328,6 +331,8 @@ int parallels_read_format_extension(BlockDriverState *bs,
 
     assert(ext_off > 0);
 
+    s->ext_end = ext_off + s->cluster_size;
+
     ext_cluster = qemu_try_blockalign(bs->file->bs, s->cluster_size);
     if (!ext_cluster) {
         error_setg(errp, "Failed to allocate the Format Extension cluster");
@@ -347,3 +352,289 @@ out:
 
     return ret;
 }
+
+static uint64_t parallels_bitmap_l1_size(uint32_t cluster_size, uint64_t bytes,
+                                         uint32_t granularity)
+{
+    uint64_t granules = DIV_ROUND_UP(bytes, granularity);
+
+    return DIV_ROUND_UP(granules, (uint64_t)cluster_size * 8);
+}
+
+static uint64_t parallels_bitmap_feature_size(uint64_t l1_size)
+{
+    return l1_size * sizeof(uint64_t) + sizeof(ParallelsFeatureHeader) +
+           sizeof(ParallelsDirtyBitmapFeature);
+}
+
+static int GRAPH_RDLOCK parallels_save_bitmap(BlockDriverState *bs,
+                                              BdrvDirtyBitmap *bitmap,
+                                              uint8_t **buf, int *buf_size,
+                                              GArray *clusters, Error **errp)
+{
+    BDRVParallelsState *s = bs->opaque;
+    ParallelsFeatureHeader *fh;
+    ParallelsDirtyBitmapFeature *bh;
+    uint64_t *l1_table, l1_size, granularity, limit, idx;
+    int64_t bm_size, ser_size, offset, buf_used;
+    int64_t alloc_size = 1;
+    const char *name;
+    uint8_t *bm_buf;
+    QemuUUID uuid;
+    int ret = 0;
+
+    if (!bdrv_dirty_bitmap_get_persistence(bitmap) ||
+        bdrv_dirty_bitmap_inconsistent(bitmap)) {
+        return 0;
+    }
+
+    name = bdrv_dirty_bitmap_name(bitmap);
+    ret = qemu_uuid_parse(name, &uuid);
+    if (ret < 0) {
+        error_setg(errp, "Can't save dirty bitmap: ID parsing error: '%s'",
+                   name);
+        return ret;
+    }
+
+    bm_size = bdrv_dirty_bitmap_size(bitmap);
+    granularity = bdrv_dirty_bitmap_granularity(bitmap);
+    limit = bdrv_dirty_bitmap_serialization_coverage(s->cluster_size, bitmap);
+    ser_size = bdrv_dirty_bitmap_serialization_size(bitmap, 0, bm_size);
+    l1_size = DIV_ROUND_UP(ser_size, s->cluster_size);
+
+    /* The end of features marker has to fit behind the feature as well */
+    buf_used = parallels_bitmap_feature_size(l1_size);
+    if (buf_used + (int64_t)sizeof(*fh) > *buf_size) {
+        error_setg(errp, "Can't save dirty bitmap %s: it needs %" PRId64
+                   " bytes of the Format Extension cluster, %d bytes are left",
+                   name, buf_used, *buf_size);
+        return -ENOSPC;
+    }
+
+    fh = (ParallelsFeatureHeader *)*buf;
+    bh = (ParallelsDirtyBitmapFeature *)(*buf + sizeof(*fh));
+    l1_table = (uint64_t *)((uint8_t *)bh + sizeof(*bh));
+
+    fh->magic = cpu_to_le64(PARALLELS_DIRTY_BITMAP_FEATURE_MAGIC);
+    fh->data_size = cpu_to_le32(l1_size * 8 + sizeof(*bh));
+
+    bh->l1_size = cpu_to_le32(l1_size);
+    bh->size = cpu_to_le64(bm_size >> BDRV_SECTOR_BITS);
+    bh->granularity = cpu_to_le32(granularity >> BDRV_SECTOR_BITS);
+    memcpy(bh->id, &uuid, sizeof(uuid));
+
+    bm_buf = qemu_try_blockalign(bs->file->bs, s->cluster_size);
+    if (!bm_buf) {
+        error_setg(errp, "Can't save dirty bitmap %s: allocation error", name);
+        ret = -ENOMEM;
+        goto fail;
+    }
+
+    offset = 0;
+    while ((offset = bdrv_dirty_bitmap_next_dirty(bitmap, offset,
+                                                  bm_size)) >= 0) {
+        int64_t cluster_off, end, write_size;
+
+        idx = offset / limit;
+
+        offset = QEMU_ALIGN_DOWN(offset, limit);
+        end = MIN(bm_size, offset + limit);
+        write_size = bdrv_dirty_bitmap_serialization_size(bitmap, offset,
+                                                          end - offset);
+        assert(write_size <= s->cluster_size);
+
+        bdrv_dirty_bitmap_serialize_part(bitmap, bm_buf, offset, end - offset);
+        if (write_size < s->cluster_size) {
+            memset(bm_buf + write_size, 0, s->cluster_size - write_size);
+        }
+
+        cluster_off = parallels_allocate_host_clusters(bs, &alloc_size);
+        if (cluster_off <= 0) {
+            ret = cluster_off < 0 ? cluster_off : -ENOSPC;
+            error_setg_errno(errp, -ret, "Can't save dirty bitmap %s: cluster "
+                             "allocation error", name);
+            goto fail;
+        }
+
+        ret = bdrv_pwrite(bs->file, cluster_off, s->cluster_size, bm_buf, 0);
+        if (ret < 0) {
+            parallels_mark_unused(bs, s->used_bmap, s->used_bmap_size,
+                                  cluster_off, 1);
+            error_setg_errno(errp, -ret, "Can't save dirty bitmap %s: IO error",
+                             name);
+            goto fail;
+        }
+
+        l1_table[idx] = cpu_to_le64(cluster_off >> BDRV_SECTOR_BITS);
+        s->ext_end = MAX(s->ext_end, cluster_off + s->cluster_size);
+        g_array_append_val(clusters, cluster_off);
+        offset = end;
+    }
+
+    *buf_size -= buf_used;
+    *buf += buf_used;
+    qemu_vfree(bm_buf);
+    return 0;
+
+fail:
+    /* Hand the clusters of the half written bitmap back to the allocator */
+    for (idx = 0; idx < l1_size; idx++) {
+        uint64_t entry = le64_to_cpu(l1_table[idx]);
+
+        if (entry > 1) {
+            parallels_mark_unused(bs, s->used_bmap, s->used_bmap_size,
+                                  entry << BDRV_SECTOR_BITS, 1);
+        }
+    }
+
+    /* Leave nothing behind a reader could take for a complete feature */
+    memset(fh, 0, buf_used);
+    qemu_vfree(bm_buf);
+    return ret;
+}
+
+void GRAPH_RDLOCK
+parallels_store_persistent_dirty_bitmaps(BlockDriverState *bs, Error **errp)
+{
+    BDRVParallelsState *s = bs->opaque;
+    BdrvDirtyBitmap *bitmap;
+    ParallelsFormatExtensionHeader *eh;
+    int remaining = s->cluster_size - sizeof(*eh);
+    uint8_t *buf, *pos;
+    int64_t header_off, alloc_size = 1;
+    g_autoptr(GArray) clusters = g_array_new(false, false, sizeof(int64_t));
+    g_autofree uint8_t *hash = NULL;
+    Error *bitmap_err = NULL;
+    size_t hash_len = 0;
+    int ret;
+    guint i;
+
+    s->header->ext_off = 0;
+    s->ext_end = 0;
+
+    if (!bdrv_has_named_bitmaps(bs)) {
+        return;
+    }
+
+    buf = qemu_try_blockalign0(bs->file->bs, s->cluster_size);
+    if (!buf) {
+        error_setg(errp, "Can't save dirty bitmaps: allocation error");
+        return;
+    }
+
+    eh = (ParallelsFormatExtensionHeader *)buf;
+    pos = buf + sizeof(*eh);
+
+    eh->magic = cpu_to_le64(PARALLELS_FORMAT_EXTENSION_MAGIC);
+
+    FOR_EACH_DIRTY_BITMAP(bs, bitmap) {
+        Error *local_err = NULL;
+
+        /*
+         * The extension is written as a whole, so a bitmap which does not
+         * make it must not take with it the ones which did.
+         */
+        if (parallels_save_bitmap(bs, bitmap, &pos, &remaining, clusters,
+                                  &local_err) < 0) {
+            error_propagate(&bitmap_err, local_err);
+        }
+    }
+
+    if (pos == buf + sizeof(*eh)) {
+        /* Not a single bitmap made it, so there is nothing to point at */
+        goto end;
+    }
+
+    header_off = parallels_allocate_host_clusters(bs, &alloc_size);
+    if (header_off <= 0) {
+        ret = header_off < 0 ? header_off : -ENOSPC;
+        error_setg_errno(errp, -ret,
+                         "Can't save dirty bitmaps: cluster allocation error");
+        goto end;
+    }
+    g_array_append_val(clusters, header_off);
+
+    ret = qcrypto_hash_bytes(QCRYPTO_HASH_ALGO_MD5,
+                             (const char *)(buf + sizeof(*eh)),
+                             s->cluster_size - sizeof(*eh),
+                             &hash, &hash_len, NULL);
+    if (ret < 0 || hash_len != sizeof(eh->check_sum)) {
+        error_setg(errp, "Can't save dirty bitmaps: hash error");
+        goto end;
+    }
+    memcpy(eh->check_sum, hash, hash_len);
+
+    ret = bdrv_pwrite(bs->file, header_off, s->cluster_size, buf, 0);
+    if (ret < 0) {
+        error_setg_errno(errp, -ret, "Can't save dirty bitmaps: IO error");
+        goto end;
+    }
+
+    s->header->ext_off = cpu_to_le64(header_off / BDRV_SECTOR_SIZE);
+    s->ext_end = MAX(s->ext_end, header_off + s->cluster_size);
+end:
+    for (i = 0; i < clusters->len; i++) {
+        parallels_mark_unused(bs, s->used_bmap, s->used_bmap_size,
+                              g_array_index(clusters, int64_t, i), 1);
+    }
+
+    /* A bitmap which was dropped only matters if the rest went through */
+    error_propagate(errp, bitmap_err);
+    qemu_vfree(buf);
+}
+
+bool coroutine_fn parallels_co_can_store_new_dirty_bitmap(BlockDriverState *bs,
+                                                          const char *name,
+                                                          uint32_t granularity,
+                                                          Error **errp)
+{
+    BDRVParallelsState *s = bs->opaque;
+    BdrvDirtyBitmap *bitmap;
+    uint64_t needed, available;
+    QemuUUID uuid;
+
+    if (bdrv_find_dirty_bitmap(bs, name)) {
+        error_setg(errp, "Bitmap already exists: %s", name);
+        return false;
+    }
+
+    if (qemu_uuid_parse(name, &uuid) < 0) {
+        error_setg(errp, "Bitmap name must be a UUID to be stored in a "
+                   "parallels image: %s", name);
+        return false;
+    }
+
+    /*
+     * One L1 entry covers a cluster worth of serialized bits, and every
+     * bitmap of the image shares the Format Extension cluster with the
+     * feature headers and the end of features marker.
+     */
+    needed = parallels_bitmap_feature_size(
+        parallels_bitmap_l1_size(s->cluster_size,
+                                 bs->total_sectors << BDRV_SECTOR_BITS,
+                                 granularity));
+
+    FOR_EACH_DIRTY_BITMAP(bs, bitmap) {
+        if (!bdrv_dirty_bitmap_get_persistence(bitmap)) {
+            continue;
+        }
+
+        needed += parallels_bitmap_feature_size(
+            parallels_bitmap_l1_size(s->cluster_size,
+                                     bdrv_dirty_bitmap_size(bitmap),
+                                     bdrv_dirty_bitmap_granularity(bitmap)));
+    }
+
+    needed += sizeof(ParallelsFeatureHeader);
+
+    available = s->cluster_size - sizeof(ParallelsFormatExtensionHeader);
+    if (needed > available) {
+        error_setg(errp, "Bitmap %s with granularity %" PRIu32 " does not fit "
+                   "into the Format Extension cluster: every bitmap of the "
+                   "image would need %" PRIu64 " bytes of it, %" PRIu64 " are "
+                   "available", name, granularity, needed, available);
+        return false;
+    }
+
+    return true;
+}
diff --git a/block/parallels.c b/block/parallels.c
index ace79ad968..a9464d5352 100644
--- a/block/parallels.c
+++ b/block/parallels.c
@@ -802,6 +802,7 @@ parallels_check_unused_clusters(BlockDriverState *bs, bool truncate)
     }
 
     end_off += s->data_start * BDRV_SECTOR_SIZE;
+    end_off = MAX(end_off, s->ext_end);
 
     /*
      * A cluster in use behind the end of the file is corruption which
@@ -1555,12 +1556,21 @@ fail:
 static int GRAPH_RDLOCK parallels_inactivate(BlockDriverState *bs)
 {
     BDRVParallelsState *s = bs->opaque;
+    Error *err = NULL;
     int64_t leak;
 
     if (!(bs->open_flags & BDRV_O_RDWR) || (bs->open_flags & BDRV_O_INACTIVE)) {
         return 0;
     }
 
+    parallels_store_persistent_dirty_bitmaps(bs, &err);
+    if (err != NULL) {
+        error_reportf_err(err, "Lost persistent bitmaps during "
+                          "inactivation of node '%s': ",
+                          bdrv_get_device_or_node_name(bs));
+        return -EINVAL;
+    }
+
     leak = parallels_check_unused_clusters(bs, true);
     if (leak < 0) {
         error_report("Failed to truncate image: %s", strerror(-leak));
@@ -1634,6 +1644,8 @@ static BlockDriver bdrv_parallels = {
     .bdrv_co_pwrite_zeroes      = parallels_co_pwrite_zeroes,
     .bdrv_co_invalidate_cache   = parallels_co_invalidate_cache,
     .bdrv_inactivate            = parallels_inactivate,
+    .bdrv_co_can_store_new_dirty_bitmap =
+                                  parallels_co_can_store_new_dirty_bitmap,
 };
 
 static void bdrv_parallels_init(void)
diff --git a/block/parallels.h b/block/parallels.h
index eb90aeea81..4684ba2890 100644
--- a/block/parallels.h
+++ b/block/parallels.h
@@ -79,6 +79,8 @@ typedef struct BDRVParallelsState {
     unsigned int bat_size;
 
     int64_t  data_start;
+    /* Exclusive end of the Format Extension, which is absent from used_bmap */
+    int64_t  ext_end;
     uint64_t prealloc_size;
     ParallelsPreallocMode prealloc_mode;
 
@@ -100,5 +102,10 @@ int64_t GRAPH_RDLOCK parallels_allocate_host_clusters(BlockDriverState *bs,
 int GRAPH_RDLOCK
 parallels_read_format_extension(BlockDriverState *bs, int64_t ext_off,
                                 Error **errp);
+void GRAPH_RDLOCK
+parallels_store_persistent_dirty_bitmaps(BlockDriverState *bs, Error **errp);
+bool coroutine_fn GRAPH_RDLOCK
+parallels_co_can_store_new_dirty_bitmap(BlockDriverState *bs, const char *name,
+                                        uint32_t granularity, Error **errp);
 
 #endif
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 12/25] parallels: Let image extensions work in RW mode
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (10 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 11/25] parallels: Add dirty bitmaps saving Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 13/25] parallels: Handle L1 entries equal to one Denis V. Lunev
                   ` (12 subsequent siblings)
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi

From: Denis V. Lunev <den@openvz.org>

Saving extensions is supported now, so we can let them work in
read-write mode as well.

The bounds check rejecting a Format Extension outside the image file
used to sit in the read-only branch of the condition. Perform it
unconditionally, as the extension is now read in both modes.

A loaded bitmap was marked read-only unconditionally, guarded by an
assert that the image is not writable. Mark it so for a read-only node
only, as qcow2_load_dirty_bitmaps() does, or the next inactivation
fails with "No write access". The condition is read-only alone rather
than the full write access qcow2 asks about, as an inactive node can
become writable later and parallels has no path which would clear the
flag again. Mark the bitmap persistent as well, or it disappears from
the image the first time it is written out.

An unreadable extension must not keep the image shut. Its clusters are
deliberately left free for reuse, so a guest write may have landed on
them, and every qemu up to this one ignores the extension in read-write
mode and truncates the file to the end of the payload on close, leaving
ext_off pointing past the end of the file. Both refuse an image whose
payload is perfectly fine, in either mode, with no way back through
qemu-img check or convert. So do not ask the in use flag, ask whether
what the header points at is an extension at all: an offset outside the
file, a wrong magic or a wrong checksum all say that it is not, whoever
left it behind, so drop it with a warning and forget where it ended,
which lets the leak check reclaim the space.

Behind the checksum the extension says what its writer meant it to say,
so a feature this driver does not implement, or a bitmap it can not
parse, keeps the image shut as before. Only an image which was not
closed correctly gives up its extension there, as its bitmaps predate
every write which followed the last inactivation.

Reading a bitmap allocates a cluster sized buffer with the aborting
qemu_blockalign(), and parallels_open() lets a cluster reach almost
2 GiB. That is reachable through every open now, so allocate it the way
the storing side does and align it to bs->file.

Based on the original work from Alexander Ivanov.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 block/parallels-ext.c                         | 24 ++++++++-----
 block/parallels.c                             | 35 +++++++++++--------
 .../qemu-iotests/tests/parallels-read-bitmap  | 18 ++++++++--
 .../tests/parallels-read-bitmap.out           | 12 +++++--
 4 files changed, 60 insertions(+), 29 deletions(-)

diff --git a/block/parallels-ext.c b/block/parallels-ext.c
index a0e2be395f..e89c489730 100644
--- a/block/parallels-ext.c
+++ b/block/parallels-ext.c
@@ -74,7 +74,11 @@ parallels_load_bitmap_data(BlockDriverState *bs, const uint64_t *l1_table,
     uint8_t *buf = NULL;
     uint64_t i;
 
-    buf = qemu_blockalign(bs, s->cluster_size);
+    buf = qemu_try_blockalign(bs->file->bs, s->cluster_size);
+    if (!buf) {
+        error_setg(errp, "Failed to allocate a bitmap data cluster");
+        return -ENOMEM;
+    }
     limit = bdrv_dirty_bitmap_serialization_coverage(s->cluster_size, bitmap);
     for (i = 0, offset = 0; i < l1_size; ++i, offset += limit) {
         uint64_t count, entry;
@@ -210,9 +214,9 @@ parallels_load_bitmap(BlockDriverState *bs, uint8_t *data, size_t data_size,
         }
     }
 
-    /* We support format extension only for RO parallels images. */
-    assert(!(bs->open_flags & BDRV_O_RDWR));
-    bdrv_dirty_bitmap_set_readonly(bitmap, true);
+    if (!(bs->open_flags & BDRV_O_RDWR)) {
+        bdrv_dirty_bitmap_set_readonly(bitmap, true);
+    }
 
     return bitmap;
 
@@ -226,7 +230,7 @@ parallels_parse_format_extension(BlockDriverState *bs, uint8_t *ext_cluster,
                                  Error **errp)
 {
     BDRVParallelsState *s = bs->opaque;
-    int ret;
+    int ret = -EINVAL;
     int remaining = s->cluster_size;
     uint8_t *pos = ext_cluster;
     ParallelsFormatExtensionHeader eh;
@@ -243,12 +247,12 @@ parallels_parse_format_extension(BlockDriverState *bs, uint8_t *ext_cluster,
         error_setg(errp, "Wrong parallels Format Extension magic: 0x%" PRIx64
                    ", expected: 0x%llx", eh.magic,
                    PARALLELS_FORMAT_EXTENSION_MAGIC);
+        ret = -ENOENT;
         goto fail;
     }
 
-    ret = qcrypto_hash_bytes(QCRYPTO_HASH_ALGO_MD5, (char *)pos, remaining,
-                             &hash, &hash_len, errp);
-    if (ret < 0) {
+    if (qcrypto_hash_bytes(QCRYPTO_HASH_ALGO_MD5, (char *)pos, remaining,
+                           &hash, &hash_len, errp) < 0) {
         goto fail;
     }
 
@@ -256,6 +260,7 @@ parallels_parse_format_extension(BlockDriverState *bs, uint8_t *ext_cluster,
         memcmp(hash, eh.check_sum, sizeof(eh.check_sum)) != 0) {
         error_setg(errp, "Wrong checksum in Format Extension header. Format "
                    "extension is corrupted.");
+        ret = -ENOENT;
         goto fail;
     }
 
@@ -301,6 +306,7 @@ parallels_parse_format_extension(BlockDriverState *bs, uint8_t *ext_cluster,
             if (!bitmap) {
                 goto fail;
             }
+            bdrv_dirty_bitmap_set_persistence(bitmap, true);
             bitmaps = g_slist_append(bitmaps, bitmap);
             break;
 
@@ -319,7 +325,7 @@ fail:
     }
     g_slist_free(bitmaps);
 
-    return -EINVAL;
+    return ret;
 }
 
 int parallels_read_format_extension(BlockDriverState *bs,
diff --git a/block/parallels.c b/block/parallels.c
index a9464d5352..90b7f7c8de 100644
--- a/block/parallels.c
+++ b/block/parallels.c
@@ -1454,25 +1454,30 @@ static int parallels_open(BlockDriverState *bs, QDict *options, int flags,
 
     if (ph.ext_off) {
         int64_t ext_off = le64_to_cpu(ph.ext_off);
+        Error *ext_err = NULL;
 
-        if (flags & BDRV_O_RDWR) {
-            /*
-             * It's unsafe to open image RW if there is an extension (as we
-             * don't support it). But parallels driver in QEMU historically
-             * ignores the extension, so print warning and don't care.
-             */
-            warn_report("Format Extension ignored in RW mode");
-        } else if (ext_off + s->tracks > file_nb_sectors) {
-            error_setg(errp, "Invalid image: Format Extension is outside the "
-                       "image file");
-            ret = -EINVAL;
-            goto fail;
+        if (ext_off + s->tracks > file_nb_sectors) {
+            ret = -ENOENT;
+            error_setg(&ext_err, "Format Extension is outside the image file");
         } else {
-            ret = parallels_read_format_extension(
-                    bs, ext_off << BDRV_SECTOR_BITS, errp);
-            if (ret < 0) {
+            ret = parallels_read_format_extension(bs,
+                                                  ext_off << BDRV_SECTOR_BITS,
+                                                  &ext_err);
+        }
+        if (ret == -ENOENT) {
+            s->ext_end = 0;
+            warn_reportf_err(ext_err, "Dropping the Format Extension of node "
+                             "'%s', which does not look like one: ",
+                             bdrv_get_device_or_node_name(bs));
+        } else if (ret < 0) {
+            if (!s->header_unclean) {
+                error_propagate(errp, ext_err);
                 goto fail;
             }
+            s->ext_end = 0;
+            warn_reportf_err(ext_err, "Dropping the Format Extension of node "
+                             "'%s', which was not closed correctly: ",
+                             bdrv_get_device_or_node_name(bs));
         }
     }
 
diff --git a/tests/qemu-iotests/tests/parallels-read-bitmap b/tests/qemu-iotests/tests/parallels-read-bitmap
index 5cbef25018..6990926d60 100755
--- a/tests/qemu-iotests/tests/parallels-read-bitmap
+++ b/tests/qemu-iotests/tests/parallels-read-bitmap
@@ -121,8 +121,16 @@ def report(name):
         log(f'qemu-img died with signal {-exc.returncode}')
         return
 
-    log('image opened' if res.returncode == 0
-        else iotests.filter_testfiles(res.stdout).strip())
+    out = iotests.filter_generated_node_ids(
+        iotests.filter_testfiles(res.stdout))
+    if res.returncode != 0:
+        log(out.strip())
+        return
+
+    log('image opened')
+    for line in out.splitlines():
+        if 'warning' in line:
+            log(line.strip())
 
 
 def check(name, ext, tracks=1):
@@ -148,6 +156,12 @@ check('wrong extension magic',
 check('wrong extension checksum',
       extension(feature(0, 0), checksum=False))
 
+# An older qemu truncates the extension cluster away on close.
+with open(crafted, 'wb') as img:
+    img.write(parallels_header(1, SECTORS, SECTORS, EXT_SECTOR))
+    img.truncate(EXT_SECTOR * 512)
+report('extension cut off the end of the file')
+
 check('unknown feature', extension(feature(BITMAP_MAGIC ^ 1, 0)))
 
 check('feature flags set', extension(feature(0, 0, flags=1)))
diff --git a/tests/qemu-iotests/tests/parallels-read-bitmap.out b/tests/qemu-iotests/tests/parallels-read-bitmap.out
index 3b3f90c8de..13b10af5a4 100644
--- a/tests/qemu-iotests/tests/parallels-read-bitmap.out
+++ b/tests/qemu-iotests/tests/parallels-read-bitmap.out
@@ -8,9 +8,14 @@ Kill NBD server
 --- well-formed extension
 image opened
 --- wrong extension magic
-qemu-img: Could not open 'TEST_DIR/PID-crafted': Wrong parallels Format Extension magic: 0xab234cef23dcea86, expected: 0xab234cef23dcea87
+image opened
+qemu-img: warning: Dropping the Format Extension of node 'NODE_NAME', which does not look like one: Wrong parallels Format Extension magic: 0xab234cef23dcea86, expected: 0xab234cef23dcea87
 --- wrong extension checksum
-qemu-img: Could not open 'TEST_DIR/PID-crafted': Wrong checksum in Format Extension header. Format extension is corrupted.
+image opened
+qemu-img: warning: Dropping the Format Extension of node 'NODE_NAME', which does not look like one: Wrong checksum in Format Extension header. Format extension is corrupted.
+--- extension cut off the end of the file
+image opened
+qemu-img: warning: Dropping the Format Extension of node 'NODE_NAME', which does not look like one: Format Extension is outside the image file
 --- unknown feature
 qemu-img: Could not open 'TEST_DIR/PID-crafted': Unknown feature: 0x20385fae252cb34b
 --- feature flags set
@@ -30,7 +35,8 @@ qemu-img: Could not open 'TEST_DIR/PID-crafted': Invalid bitmap granularity 4294
 --- bitmap L1 entry overflows
 qemu-img: Could not open 'TEST_DIR/PID-crafted': Failed to read bitmap data cluster: Input/output error
 --- cluster_size beyond the file size
-qemu-img: Could not open 'TEST_DIR/PID-crafted': Invalid image: Format Extension is outside the image file
+image opened
+qemu-img: warning: Dropping the Format Extension of node 'NODE_NAME', which does not look like one: Format Extension is outside the image file
 --- bitmap serialization coverage overflow
 image opened
 --- bitmap spanning two L1 entries
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 13/25] parallels: Handle L1 entries equal to one
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (11 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 12/25] parallels: Let image extensions work in RW mode Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 14/25] iotests: cover the Format Extension against the leak check Denis V. Lunev
                   ` (11 subsequent siblings)
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi

From: Denis V. Lunev <den@openvz.org>

If all the bits in a dirty bitmap cluster are ones, the cluster shouldn't
be written. Instead the corresponding L1 entry should be set to 1.

Ask bdrv_dirty_bitmap_next_zero() for a clean bit in the range the entry
covers, and store the marker when there is none. Two things have to
match the region rather than the cluster which serializes it: the search
is bounded by the end of the region, as a count and not as an offset, and
a missing clean bit is the answer we are looking for rather than a reason
to give up. A dirty run is not a substitute for either. Its length says
nothing about the region it lies in, so comparing it to the cluster size
marks a chunk as all ones when only a cluster worth of it is dirty, and
treating "no clean bit at all" as a failure leaves the entry at zero,
which says the whole chunk is clean.

Both directions are silent: a full disk overwrite comes back as a
completely clean bitmap, and a single dirty cluster comes back as
everything the entry covers being dirty. An incremental backup driven by
the first one copies nothing.

The marker goes through cpu_to_le64() like the cluster offsets below it,
as the loading side reads the table with ldq_le_p(). The serialization
moved behind the check, as there is nothing to write when the marker is
stored.

Based on the original work from Alexander Ivanov.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 block/parallels-ext.c | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/block/parallels-ext.c b/block/parallels-ext.c
index e89c489730..6a889d86fa 100644
--- a/block/parallels-ext.c
+++ b/block/parallels-ext.c
@@ -439,12 +439,20 @@ static int GRAPH_RDLOCK parallels_save_bitmap(BlockDriverState *bs,
     offset = 0;
     while ((offset = bdrv_dirty_bitmap_next_dirty(bitmap, offset,
                                                   bm_size)) >= 0) {
-        int64_t cluster_off, end, write_size;
+        int64_t cluster_off, end, write_size, first_zero;
 
         idx = offset / limit;
 
         offset = QEMU_ALIGN_DOWN(offset, limit);
         end = MIN(bm_size, offset + limit);
+
+        first_zero = bdrv_dirty_bitmap_next_zero(bitmap, offset, end - offset);
+        if (first_zero < 0) {
+            l1_table[idx] = cpu_to_le64(1);
+            offset = end;
+            continue;
+        }
+
         write_size = bdrv_dirty_bitmap_serialization_size(bitmap, offset,
                                                           end - offset);
         assert(write_size <= s->cluster_size);
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 14/25] iotests: cover the Format Extension against the leak check
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (12 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 13/25] parallels: Handle L1 entries equal to one Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 15/25] iotests: run the persistent dirty bitmap test on parallels Denis V. Lunev
                   ` (10 subsequent siblings)
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi

From: Denis V. Lunev <den@openvz.org>

The Format Extension and the bitmap data clusters it points to are
absent from used_bmap, so the leak check has to be told about them
through s->ext_end or it reports the space they occupy as leaked and
'qemu-img check -r leaks' truncates it away while the header still
points at it.

The leak coverage in parallels-checks grows a persistent bitmap, dirtied
one granule at a time so that parallels_save_bitmap() stores a bitmap
data cluster rather than the all ones L1 entry and the extension spans
two clusters. It asserts that neither is a leak, that a genuine leak
appended after them is still reported and repaired, that the bitmap and
the guest data survive the repair, and that dropping the bitmap makes
the space reclaimable. Without s->ext_end the check reports two leaked
clusters where there should be none.

A name which is not a UUID is covered there too, as the refusal belongs
to the same command.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 tests/qemu-iotests/tests/parallels-checks     | 48 +++++++++++++++++++
 tests/qemu-iotests/tests/parallels-checks.out | 35 ++++++++++++++
 2 files changed, 83 insertions(+)

diff --git a/tests/qemu-iotests/tests/parallels-checks b/tests/qemu-iotests/tests/parallels-checks
index c9dcd715ac..6f60fda62b 100755
--- a/tests/qemu-iotests/tests/parallels-checks
+++ b/tests/qemu-iotests/tests/parallels-checks
@@ -362,6 +362,54 @@ echo "file size: `stat --printf="%s" "$TEST_IMG"`"
 
 echo "== the first cluster survived =="
 { $QEMU_IO -c "read -P 0x11 0 $CLUSTER_SIZE" "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
+_make_test_img $SIZE
+
+echo "== TEST FORMAT EXTENSION IS NOT LEAKED SPACE =="
+
+BITMAP=b2c9e1a4-5d3f-4e8b-9a7c-6f0d1e2b3a45
+
+echo "== a name which is not a UUID can not be stored, so refuse it =="
+$QEMU_IMG bitmap --add -f $IMGFMT "$TEST_IMG" bitmap0 2>&1 | _filter_testdir
+
+echo "== add a persistent dirty bitmap =="
+$QEMU_IMG bitmap --add -f $IMGFMT "$TEST_IMG" $BITMAP 2>&1 | _filter_testdir
+
+# A dirty run shorter than a cluster is stored as a bitmap data cluster
+# instead of the all-ones L1 entry, so the extension spans two clusters.
+echo "== dirty a single granule of the bitmap =="
+{ $QEMU_IO -c "write -P 0x11 0 65536" "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
+
+file_size=`stat --printf="%s" "$TEST_IMG"`
+echo "file size: $file_size"
+
+echo "== the extension and its bitmap data are not a leak =="
+_check_test_img
+
+echo "== extend image by 1 cluster =="
+fallocate -xl $((file_size + CLUSTER_SIZE)) "$TEST_IMG"
+
+echo "== a real leak is still reported =="
+_check_test_img
+
+echo "== repair reclaims the leak only =="
+_check_test_img -r leaks
+
+file_size=`stat --printf="%s" "$TEST_IMG"`
+echo "file size: $file_size"
+
+echo "== the bitmap survived the repair =="
+$QEMU_IMG bitmap --add -f $IMGFMT "$TEST_IMG" $BITMAP 2>&1 | _filter_testdir
+
+echo "== data survived the repair =="
+{ $QEMU_IO -r -c "read -P 0x11 0 65536" "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
+
+echo "== dropping the bitmap makes the space reclaimable again =="
+$QEMU_IMG bitmap --remove -f $IMGFMT "$TEST_IMG" $BITMAP 2>&1 | _filter_testdir
+
+file_size=`stat --printf="%s" "$TEST_IMG"`
+echo "file size: $file_size"
+
+_check_test_img
 
 # Clear image
 _make_test_img $SIZE
diff --git a/tests/qemu-iotests/tests/parallels-checks.out b/tests/qemu-iotests/tests/parallels-checks.out
index 6699848996..d40f865868 100644
--- a/tests/qemu-iotests/tests/parallels-checks.out
+++ b/tests/qemu-iotests/tests/parallels-checks.out
@@ -233,6 +233,41 @@ file size: 2097152
 read 1048576/1048576 bytes at offset 0
 1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
 Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=4194304
+== TEST FORMAT EXTENSION IS NOT LEAKED SPACE ==
+== a name which is not a UUID can not be stored, so refuse it ==
+qemu-img: Operation add on bitmap bitmap0 failed: Bitmap name must be a UUID to be stored in a parallels image: bitmap0
+== add a persistent dirty bitmap ==
+== dirty a single granule of the bitmap ==
+wrote 65536/65536 bytes at offset 0
+64 KiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+file size: 4194304
+== the extension and its bitmap data are not a leak ==
+No errors were found on the image.
+== extend image by 1 cluster ==
+== a real leak is still reported ==
+ERROR space leaked at the end of the image 1048576
+
+1 leaked clusters were found on the image.
+This means waste of disk space, but no harm to data.
+== repair reclaims the leak only ==
+Repairing space leaked at the end of the image 1048576
+The following inconsistencies were found and repaired:
+
+    1 leaked clusters
+    0 corruptions
+
+Double checking the fixed image now...
+No errors were found on the image.
+file size: 4194304
+== the bitmap survived the repair ==
+qemu-img: Operation add on bitmap b2c9e1a4-5d3f-4e8b-9a7c-6f0d1e2b3a45 failed: Bitmap already exists: b2c9e1a4-5d3f-4e8b-9a7c-6f0d1e2b3a45
+== data survived the repair ==
+read 65536/65536 bytes at offset 0
+64 KiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+== dropping the bitmap makes the space reclaimable again ==
+file size: 2097152
+No errors were found on the image.
+Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=4194304
 == TEST A DUPLICATE IN THE LAST ALLOCATED BAT ENTRY ==
 == write two clusters ==
 wrote 1048576/1048576 bytes at offset 0
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 15/25] iotests: run the persistent dirty bitmap test on parallels
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (13 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 14/25] iotests: cover the Format Extension against the leak check Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 16/25] parallels: reject a bitmap L1 entry outside the data area Denis V. Lunev
                   ` (9 subsequent siblings)
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi

From: Denis V. Lunev <den@openvz.org>

parallels stores persistent dirty bitmaps now, so test 165 applies to it
as well. The bitmap name is the only format specific bit: parallels
keeps it as the UUID identifying the bitmap in the image, so a name like
'bitmap0' is rejected on store.

test_reopen_rw is skipped, as parallels has no bdrv_reopen_prepare() and
blockdev-reopen fails with "does not support reopening files". Skipped
cases do not show up in the reference output, so 165.out stays as it is.

test_persistent covers what the series adds: it dirties the bitmap,
records its hash, restarts the VM twice and compares the hash again,
which passes only if the bitmap survives both the store on inactivation
and the load on open.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 tests/qemu-iotests/165 | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)

diff --git a/tests/qemu-iotests/165 b/tests/qemu-iotests/165
index 8fc79daae1..13b784fcc6 100755
--- a/tests/qemu-iotests/165
+++ b/tests/qemu-iotests/165
@@ -27,6 +27,12 @@ from iotests import qemu_img
 disk = os.path.join(iotests.test_dir, 'disk')
 disk_size = 0x40000000 # 1G
 
+# parallels stores a bitmap name as the UUID identifying it in the image
+if iotests.imgfmt == 'parallels':
+    bitmap_name = 'b2c9e1a4-5d3f-4e8b-9a7c-6f0d1e2b3a45'
+else:
+    bitmap_name = 'bitmap0'
+
 # regions for qemu_io: (start, count) in bytes
 regions1 = ((0x0fff00, 0x10000),
             (0x200000, 0x100000))
@@ -50,12 +56,12 @@ class TestPersistentDirtyBitmap(iotests.QMPTestCase):
 
     def getSha256(self):
         result = self.vm.qmp('x-debug-block-dirty-bitmap-sha256',
-                             node='drive0', name='bitmap0')
+                             node='drive0', name=bitmap_name)
         return result['return']['sha256']
 
     def checkBitmap(self, sha256):
         result = self.vm.qmp('x-debug-block-dirty-bitmap-sha256',
-                             node='drive0', name='bitmap0')
+                             node='drive0', name=bitmap_name)
         self.assert_qmp(result, 'return/sha256', sha256);
 
     def writeRegions(self, regions):
@@ -65,7 +71,7 @@ class TestPersistentDirtyBitmap(iotests.QMPTestCase):
 
     def qmpAddBitmap(self):
         self.vm.qmp('block-dirty-bitmap-add', node='drive0',
-                    name='bitmap0', persistent=True)
+                    name=bitmap_name, persistent=True)
 
     def test_persistent(self):
         self.vm = self.mkVm()
@@ -100,6 +106,8 @@ class TestPersistentDirtyBitmap(iotests.QMPTestCase):
 
         self.vm.shutdown()
 
+    # parallels has no bdrv_reopen_prepare() yet
+    @iotests.skip_for_formats(('parallels',))
     def test_reopen_rw(self):
         self.vm = self.mkVm()
         self.vm.launch()
@@ -115,7 +123,7 @@ class TestPersistentDirtyBitmap(iotests.QMPTestCase):
         assert sha256_1 != sha256_2 # Otherwise, it's not very interesting.
 
         self.vm.cmd('block-dirty-bitmap-clear', node='drive0',
-                    name='bitmap0')
+                    name=bitmap_name)
 
         # Start with regions1
 
@@ -152,7 +160,7 @@ class TestPersistentDirtyBitmap(iotests.QMPTestCase):
 
 
 if __name__ == '__main__':
-    iotests.main(supported_fmts=['qcow2'],
+    iotests.main(supported_fmts=['qcow2', 'parallels'],
                  supported_protocols=['file'],
                  unsupported_imgopts=['compat'],
                  require_hmp=True)
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 16/25] parallels: reject a bitmap L1 entry outside the data area
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (14 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 15/25] iotests: run the persistent dirty bitmap test on parallels Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 17/25] parallels: do not trust the bitmaps of an image which was not closed Denis V. Lunev
                   ` (8 subsequent siblings)
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi

From: Denis V. Lunev <den@openvz.org>

Nothing checks where a bitmap L1 entry points. The entry is turned into
an offset and the cluster is read from there, so the image decides which
part of the file, if any, is deserialized as bitmap data.

A short read on the protocol node is zero filled rather than refused, so
an entry beyond the end of the file does not even fail: the bitmap
quietly loads as completely clean. An entry below data_off deserializes
the header and the BAT as bitmap data instead. Both cases used to be
harmless in the sense that the extension was only parsed for read-only
images, but the bitmap becomes writable and is stored back as
authoritative once the image can be opened read-write.

Bound the entry the way parallels_check_outside_image() bounds a BAT
entry: it has to address the data area of the image file. The offset
computation is bounded first, as the entry is a 64 bit value coming from
the image and the shift by BDRV_SECTOR_BITS would overflow.

The overflow case was reported as an I/O error before, so the test
expectation changes along with it.

Fixes: baefd977002e ("parallels: support bitmap extension for read-only mode")
Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 block/parallels-ext.c                         | 30 ++++++++++++++++++-
 .../qemu-iotests/tests/parallels-read-bitmap  | 23 +++++++++++---
 .../tests/parallels-read-bitmap.out           |  6 +++-
 3 files changed, 53 insertions(+), 6 deletions(-)

diff --git a/block/parallels-ext.c b/block/parallels-ext.c
index 6a889d86fa..17445d183b 100644
--- a/block/parallels-ext.c
+++ b/block/parallels-ext.c
@@ -71,9 +71,17 @@ parallels_load_bitmap_data(BlockDriverState *bs, const uint64_t *l1_table,
     int ret = 0;
     uint64_t offset, limit;
     uint64_t bm_size = bdrv_dirty_bitmap_size(bitmap);
+    int64_t file_size, data_start_off;
     uint8_t *buf = NULL;
     uint64_t i;
 
+    file_size = bdrv_getlength(bs->file->bs);
+    if (file_size < 0) {
+        error_setg_errno(errp, -file_size, "Failed to get image file length");
+        return file_size;
+    }
+    data_start_off = s->data_start << BDRV_SECTOR_BITS;
+
     buf = qemu_try_blockalign(bs->file->bs, s->cluster_size);
     if (!buf) {
         error_setg(errp, "Failed to allocate a bitmap data cluster");
@@ -101,7 +109,27 @@ parallels_load_bitmap_data(BlockDriverState *bs, const uint64_t *l1_table,
         if (entry == 1) {
             bdrv_dirty_bitmap_deserialize_ones(bitmap, offset, count, false);
         } else {
-            int64_t host_off = entry << BDRV_SECTOR_BITS;
+            int64_t host_off;
+
+            if (entry > INT64_MAX / BDRV_SECTOR_SIZE) {
+                error_setg(errp, "Bitmap L1 entry %" PRIu64 " is out of range",
+                           i);
+                ret = -EINVAL;
+                goto finish;
+            }
+            host_off = entry * BDRV_SECTOR_SIZE;
+            if (host_off < data_start_off) {
+                error_setg(errp, "Bitmap L1 entry %" PRIu64 " points before "
+                           "the data area of the image", i);
+                ret = -EINVAL;
+                goto finish;
+            }
+            if (host_off > file_size - (int64_t)s->cluster_size) {
+                error_setg(errp, "Bitmap L1 entry %" PRIu64 " points outside "
+                           "the image file", i);
+                ret = -EINVAL;
+                goto finish;
+            }
 
             ret = bdrv_pread(bs->file, host_off, s->cluster_size, buf, 0);
             if (ret < 0) {
diff --git a/tests/qemu-iotests/tests/parallels-read-bitmap b/tests/qemu-iotests/tests/parallels-read-bitmap
index 6990926d60..f5a1f33907 100755
--- a/tests/qemu-iotests/tests/parallels-read-bitmap
+++ b/tests/qemu-iotests/tests/parallels-read-bitmap
@@ -93,9 +93,10 @@ def extension(body, magic=EXT_MAGIC, checksum=True):
     return struct.pack('<Q16s', magic, csum) + body
 
 
-def parallels_header(tracks, bat_entries, nb_sectors, ext_sector):
+def parallels_header(tracks, bat_entries, nb_sectors, ext_sector, data_off=1):
     return struct.pack('<16sIIIIIQIIIQ', b'WithouFreSpacExt', 2, 16, 0,
-                       tracks, bat_entries, nb_sectors, 0, 1, 0, ext_sector)
+                       tracks, bat_entries, nb_sectors, 0, data_off, 0,
+                       ext_sector)
 
 
 def write_sparse_image(body, tracks, nb_sectors, bat_entries, ext_sector):
@@ -133,12 +134,14 @@ def report(name):
             log(line.strip())
 
 
-def check(name, ext, tracks=1):
-    header = parallels_header(tracks, SECTORS, SECTORS, EXT_SECTOR)
+def check(name, ext, tracks=1, data_off=1, pad=0):
+    header = parallels_header(tracks, SECTORS, SECTORS, EXT_SECTOR, data_off)
     with open(crafted, 'wb') as f:
         f.write(header)
         f.write(bytes(EXT_SECTOR * 512 - len(header)))   # BAT, unallocated
         f.write(ext)
+        if pad:
+            f.truncate(EXT_SECTOR * 512 + tracks * 512 * (1 + pad))
 
     report(name)
 
@@ -192,6 +195,18 @@ bf = bitmap_feature([0xffffffffffffffff])
 check('bitmap L1 entry overflows',
       extension(feature(BITMAP_MAGIC, len(bf)) + bf + feature(0, 0)))
 
+# A cluster which is not there reads back as zeroes, as short reads on the
+# protocol node are zero filled, so the bitmap would silently load as clean.
+bf = bitmap_feature([8])
+check('bitmap L1 entry past the image file',
+      extension(feature(BITMAP_MAGIC, len(bf)) + bf + feature(0, 0)))
+
+# An entry pointing at the header would deserialize the header as bitmap data.
+bf = bitmap_feature([4])
+check('bitmap L1 entry inside the header',
+      extension(feature(BITMAP_MAGIC, len(bf)) + bf + feature(0, 0)),
+      data_off=8, pad=16)
+
 # Largest cluster_size parallels_open() accepts, about 2 GiB.
 check('cluster_size beyond the file size',
       extension(feature(0, 0)), tracks=0x7fffffff // 513)
diff --git a/tests/qemu-iotests/tests/parallels-read-bitmap.out b/tests/qemu-iotests/tests/parallels-read-bitmap.out
index 13b10af5a4..ebfc0acb4d 100644
--- a/tests/qemu-iotests/tests/parallels-read-bitmap.out
+++ b/tests/qemu-iotests/tests/parallels-read-bitmap.out
@@ -33,7 +33,11 @@ qemu-img: Could not open 'TEST_DIR/PID-crafted': Invalid bitmap granularity 1536
 --- bitmap granularity 8388608
 qemu-img: Could not open 'TEST_DIR/PID-crafted': Invalid bitmap granularity 4294967296, expected a power of two of at least 512 bytes
 --- bitmap L1 entry overflows
-qemu-img: Could not open 'TEST_DIR/PID-crafted': Failed to read bitmap data cluster: Input/output error
+qemu-img: Could not open 'TEST_DIR/PID-crafted': Bitmap L1 entry 0 is out of range
+--- bitmap L1 entry past the image file
+qemu-img: Could not open 'TEST_DIR/PID-crafted': Bitmap L1 entry 0 points outside the image file
+--- bitmap L1 entry inside the header
+qemu-img: Could not open 'TEST_DIR/PID-crafted': Bitmap L1 entry 0 points before the data area of the image
 --- cluster_size beyond the file size
 image opened
 qemu-img: warning: Dropping the Format Extension of node 'NODE_NAME', which does not look like one: Format Extension is outside the image file
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 17/25] parallels: do not trust the bitmaps of an image which was not closed
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (15 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 16/25] parallels: reject a bitmap L1 entry outside the data area Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 18/25] parallels: implement removing a stored dirty bitmap Denis V. Lunev
                   ` (7 subsequent siblings)
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi

From: Denis V. Lunev <den@openvz.org>

The inuse magic in the header says that the image was not closed
correctly. The bitmaps stored in the Format Extension are then stale by
definition: they were written by the last inactivation, and every write
which happened after it is missing from them. Nothing said so, the
bitmaps were loaded and handed out as valid, and an incremental backup
taken from one of them would silently miss the data written after the
last clean close.

Mark them inconsistent, as qcow2 does for a bitmap whose in-use flag
survived a crash. Using such a bitmap fails with an error naming it, so
the loss is reported to whoever tries to rely on it instead of being
discovered later in a backup.

parallels_save_bitmap() already skips an inconsistent bitmap, so it is
not written back. The format has no per bitmap flag to record that the
contents are unusable, so keeping it would present it as valid again on
the next open. Say what happens, as the bitmap disappears from the image
and 'qemu-img bitmap --remove' would report it as missing afterwards.

The image data itself is unaffected: it is repaired at open as before,
and only the bitmaps are dropped.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 block/parallels-ext.c                         | 13 +++++++++--
 tests/qemu-iotests/tests/parallels-checks     | 23 +++++++++++++++++++
 tests/qemu-iotests/tests/parallels-checks.out | 15 ++++++++++++
 3 files changed, 49 insertions(+), 2 deletions(-)

diff --git a/block/parallels-ext.c b/block/parallels-ext.c
index 17445d183b..b7fac2514a 100644
--- a/block/parallels-ext.c
+++ b/block/parallels-ext.c
@@ -335,6 +335,9 @@ parallels_parse_format_extension(BlockDriverState *bs, uint8_t *ext_cluster,
                 goto fail;
             }
             bdrv_dirty_bitmap_set_persistence(bitmap, true);
+            if (s->header_unclean) {
+                bdrv_dirty_bitmap_set_inconsistent(bitmap);
+            }
             bitmaps = g_slist_append(bitmaps, bitmap);
             break;
 
@@ -417,8 +420,14 @@ static int GRAPH_RDLOCK parallels_save_bitmap(BlockDriverState *bs,
     QemuUUID uuid;
     int ret = 0;
 
-    if (!bdrv_dirty_bitmap_get_persistence(bitmap) ||
-        bdrv_dirty_bitmap_inconsistent(bitmap)) {
+    if (!bdrv_dirty_bitmap_get_persistence(bitmap)) {
+        return 0;
+    }
+
+    /* The format has no way to mark a stored bitmap unusable */
+    if (bdrv_dirty_bitmap_inconsistent(bitmap)) {
+        warn_report("Dropping inconsistent bitmap %s",
+                    bdrv_dirty_bitmap_name(bitmap));
         return 0;
     }
 
diff --git a/tests/qemu-iotests/tests/parallels-checks b/tests/qemu-iotests/tests/parallels-checks
index 6f60fda62b..575b736e35 100755
--- a/tests/qemu-iotests/tests/parallels-checks
+++ b/tests/qemu-iotests/tests/parallels-checks
@@ -414,6 +414,29 @@ _check_test_img
 # Clear image
 _make_test_img $SIZE
 
+echo "== TEST BITMAP OF AN IMAGE WHICH WAS NOT CLOSED =="
+
+INUSE_OFFSET=44
+
+echo "== add a persistent dirty bitmap and dirty it =="
+$QEMU_IMG bitmap --add -f $IMGFMT "$TEST_IMG" $BITMAP 2>&1 | _filter_testdir
+{ $QEMU_IO -c "write -P 0x11 0 65536" "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
+
+echo "== pretend the image was not closed correctly =="
+poke_file "$TEST_IMG" "$INUSE_OFFSET" "\x59\x6e\x6f\x74"
+
+echo "== the bitmap is stale, so it can not be used and is dropped =="
+$QEMU_IMG bitmap --clear -f $IMGFMT "$TEST_IMG" $BITMAP 2>&1 | _filter_testdir
+
+echo "== the name is free again =="
+$QEMU_IMG bitmap --add -f $IMGFMT "$TEST_IMG" $BITMAP 2>&1 | _filter_testdir
+
+echo "== guest data was never in doubt =="
+{ $QEMU_IO -r -c "read -P 0x11 0 65536" "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
+
+# Clear image
+_make_test_img $SIZE
+
 echo "== TEST A DUPLICATE IN THE LAST ALLOCATED BAT ENTRY =="
 
 echo "== write two clusters =="
diff --git a/tests/qemu-iotests/tests/parallels-checks.out b/tests/qemu-iotests/tests/parallels-checks.out
index d40f865868..f390ea90d4 100644
--- a/tests/qemu-iotests/tests/parallels-checks.out
+++ b/tests/qemu-iotests/tests/parallels-checks.out
@@ -268,6 +268,21 @@ read 65536/65536 bytes at offset 0
 file size: 2097152
 No errors were found on the image.
 Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=4194304
+== TEST BITMAP OF AN IMAGE WHICH WAS NOT CLOSED ==
+== add a persistent dirty bitmap and dirty it ==
+wrote 65536/65536 bytes at offset 0
+64 KiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+== pretend the image was not closed correctly ==
+== the bitmap is stale, so it can not be used and is dropped ==
+Repairing image was not closed correctly
+qemu-img: Operation clear on bitmap b2c9e1a4-5d3f-4e8b-9a7c-6f0d1e2b3a45 failed: Bitmap 'b2c9e1a4-5d3f-4e8b-9a7c-6f0d1e2b3a45' is inconsistent and cannot be used
+Try block-dirty-bitmap-remove to delete this bitmap from disk
+qemu-img: warning: Dropping inconsistent bitmap b2c9e1a4-5d3f-4e8b-9a7c-6f0d1e2b3a45
+== the name is free again ==
+== guest data was never in doubt ==
+read 65536/65536 bytes at offset 0
+64 KiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=4194304
 == TEST A DUPLICATE IN THE LAST ALLOCATED BAT ENTRY ==
 == write two clusters ==
 wrote 1048576/1048576 bytes at offset 0
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 18/25] parallels: implement removing a stored dirty bitmap
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (16 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 17/25] parallels: do not trust the bitmaps of an image which was not closed Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 19/25] parallels: report the stored dirty bitmaps in qemu-img info Denis V. Lunev
                   ` (6 subsequent siblings)
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi

From: Denis V. Lunev <den@openvz.org>

block-dirty-bitmap-remove leaves a parallels image alone. The driver has
no bdrv_co_remove_persistent_dirty_bitmap(), and a missing handler means
nothing to do, so the command reports success while the bitmap stays in
the image until the node is inactivated. qcow2 updates its bitmap
directory on the spot.

Implement the handler. The Format Extension is written as a whole rather
than edited in place, so the bitmap is dropped by clearing its
persistence and storing what is left, and the header is updated to point
at the new extension. Releasing the bitmap itself is up to the caller.
The store runs under s->lock, as it allocates a cluster and a request
can be in flight, unlike the inactivation path where the node is
quiesced already.

Removing a bitmap which was never stored is not an error, as
bdrv_co_remove_persistent_dirty_bitmap() spells out, so a name which is
not there, or is not persistent, is answered with success and no write.

Write access is checked first. The extension is only rewritten while the
node is writable, so on a read-only or an inactive node the request could
not reach the image at all and reporting success would be a lie. The
generic BDRV_BITMAP_RO check in block_dirty_bitmap_remove() hides this
for a read-only node, but it does not cover an inactive one, and
dropping a bitmap which was never stored needs no write access at all.

Persistence is only dropped for the duration of the rewrite, so it goes
back on when the rewrite fails. The bitmap is still in the image, and a
bitmap which is no longer marked persistent would be dropped for good by
the next inactivation, turning a removal the caller was told had failed
into one which silently succeeded.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 block/parallels-ext.c | 47 +++++++++++++++++++++++++++++++++++++++++++
 block/parallels.c     |  4 +++-
 block/parallels.h     |  4 ++++
 3 files changed, 54 insertions(+), 1 deletion(-)

diff --git a/block/parallels-ext.c b/block/parallels-ext.c
index b7fac2514a..f687f2da7c 100644
--- a/block/parallels-ext.c
+++ b/block/parallels-ext.c
@@ -689,3 +689,50 @@ bool coroutine_fn parallels_co_can_store_new_dirty_bitmap(BlockDriverState *bs,
 
     return true;
 }
+
+int coroutine_fn
+parallels_co_remove_persistent_dirty_bitmap(BlockDriverState *bs,
+                                            const char *name, Error **errp)
+{
+    BDRVParallelsState *s = bs->opaque;
+    BdrvDirtyBitmap *bitmap;
+    Error *err = NULL;
+    int ret;
+
+    if (bdrv_is_read_only(bs) || (bdrv_get_flags(bs) & BDRV_O_INACTIVE)) {
+        error_setg(errp, "Cannot remove persistent bitmap '%s': no write "
+                   "access to node '%s'", name, bdrv_get_node_name(bs));
+        return -EACCES;
+    }
+
+    bitmap = bdrv_find_dirty_bitmap(bs, name);
+    if (bitmap == NULL || !bdrv_dirty_bitmap_get_persistence(bitmap)) {
+        return 0;
+    }
+
+    /* The extension is written as a whole, so drop it from what goes in */
+    bdrv_dirty_bitmap_set_persistence(bitmap, false);
+
+    ret = 0;
+    WITH_QEMU_LOCK_GUARD(&s->lock) {
+        parallels_store_persistent_dirty_bitmaps(bs, &err);
+        if (err != NULL) {
+            error_propagate(errp, err);
+            ret = -EIO;
+            break;
+        }
+
+        ret = parallels_update_header(bs);
+        if (ret < 0) {
+            error_setg_errno(errp, -ret, "Failed to update the image header");
+            break;
+        }
+    }
+
+    if (ret < 0) {
+        /* Nothing was removed, so the bitmap is as persistent as it was */
+        bdrv_dirty_bitmap_set_persistence(bitmap, true);
+    }
+
+    return ret;
+}
diff --git a/block/parallels.c b/block/parallels.c
index 90b7f7c8de..2a5ceb8978 100644
--- a/block/parallels.c
+++ b/block/parallels.c
@@ -1288,7 +1288,7 @@ static int parallels_probe(const uint8_t *buf, int buf_size,
     return 0;
 }
 
-static int GRAPH_RDLOCK parallels_update_header(BlockDriverState *bs)
+int GRAPH_RDLOCK parallels_update_header(BlockDriverState *bs)
 {
     BDRVParallelsState *s = bs->opaque;
     unsigned size = MAX(bdrv_opt_mem_align(bs->file->bs),
@@ -1651,6 +1651,8 @@ static BlockDriver bdrv_parallels = {
     .bdrv_inactivate            = parallels_inactivate,
     .bdrv_co_can_store_new_dirty_bitmap =
                                   parallels_co_can_store_new_dirty_bitmap,
+    .bdrv_co_remove_persistent_dirty_bitmap =
+                                  parallels_co_remove_persistent_dirty_bitmap,
 };
 
 static void bdrv_parallels_init(void)
diff --git a/block/parallels.h b/block/parallels.h
index 4684ba2890..27d8c3ac83 100644
--- a/block/parallels.h
+++ b/block/parallels.h
@@ -98,6 +98,7 @@ int parallels_mark_unused(BlockDriverState *bs, unsigned long *bitmap,
 
 int64_t GRAPH_RDLOCK parallels_allocate_host_clusters(BlockDriverState *bs,
                                                       int64_t *clusters);
+int GRAPH_RDLOCK parallels_update_header(BlockDriverState *bs);
 
 int GRAPH_RDLOCK
 parallels_read_format_extension(BlockDriverState *bs, int64_t ext_off,
@@ -107,5 +108,8 @@ parallels_store_persistent_dirty_bitmaps(BlockDriverState *bs, Error **errp);
 bool coroutine_fn GRAPH_RDLOCK
 parallels_co_can_store_new_dirty_bitmap(BlockDriverState *bs, const char *name,
                                         uint32_t granularity, Error **errp);
+int coroutine_fn GRAPH_RDLOCK
+parallels_co_remove_persistent_dirty_bitmap(BlockDriverState *bs,
+                                            const char *name, Error **errp);
 
 #endif
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 19/25] parallels: report the stored dirty bitmaps in qemu-img info
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (17 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 18/25] parallels: implement removing a stored dirty bitmap Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-04  8:49   ` Markus Armbruster
  2026-09-03 14:41 ` [PATCH v7 20/25] iotests: rename parallels-read-bitmap to parallels-bitmap Denis V. Lunev
                   ` (5 subsequent siblings)
  24 siblings, 1 reply; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel
  Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi, Eric Blake,
	Markus Armbruster

From: Denis V. Lunev <den@openvz.org>

Nothing tells which persistent bitmaps an image carries. qemu-img info
says nothing about them, and the only other way to see one is to export
the image over NBD and ask for a bitmap by name, which needs the name
beforehand.

Add ImageInfoSpecificParallels with the bitmaps and their granularity,
in the same way as qcow2 reports the contents of its bitmap directory:

    Format specific information:
        bitmaps:
            [0]:
                name: b2c9e1a4-5d3f-4e8b-9a7c-6f0d1e2b3a45
                granularity: 65536

For running VM process the list is built on the fly from in-memory
list. This is the best we can do. FormatExtension could be dead in
the image while VMs are running as all bitmaps are cleared on
non-clean VM stop.

A bitmap of an image which was not closed correctly is inconsistent
and can not be used. Report that as well, the way qcow2 reports its
in-use flag, so such a bitmap is not listed as a valid one.

The section is left empty for an image with no persistent dirty
bitmaps, and an empty section is not printed, so the human readable
output of "qemu-img info" and "info block" is unchanged.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Cc: Eric Blake <eblake@redhat.com>
Cc: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 block/parallels-ext.c                         | 26 +++++++++++
 block/parallels.c                             | 20 ++++++++
 block/parallels.h                             |  3 ++
 qapi/block-core.json                          | 46 ++++++++++++++++++-
 tests/qemu-iotests/tests/parallels-checks     |  6 +++
 tests/qemu-iotests/tests/parallels-checks.out | 19 ++++++++
 6 files changed, 118 insertions(+), 2 deletions(-)

diff --git a/block/parallels-ext.c b/block/parallels-ext.c
index f687f2da7c..8b8035af91 100644
--- a/block/parallels-ext.c
+++ b/block/parallels-ext.c
@@ -736,3 +736,29 @@ parallels_co_remove_persistent_dirty_bitmap(BlockDriverState *bs,
 
     return ret;
 }
+
+void parallels_get_bitmap_info_list(BlockDriverState *bs,
+                                    ParallelsBitmapInfoList **info_list)
+{
+    BdrvDirtyBitmap *bitmap;
+    ParallelsBitmapInfoList **tail = info_list;
+
+    *info_list = NULL;
+
+    FOR_EACH_DIRTY_BITMAP(bs, bitmap) {
+        ParallelsBitmapInfo *info;
+
+        if (!bdrv_dirty_bitmap_get_persistence(bitmap)) {
+            continue;
+        }
+
+        info = g_new0(ParallelsBitmapInfo, 1);
+        info->name = g_strdup(bdrv_dirty_bitmap_name(bitmap));
+        info->granularity = bdrv_dirty_bitmap_granularity(bitmap);
+        if (bdrv_dirty_bitmap_inconsistent(bitmap)) {
+            info->has_inconsistent = true;
+            info->inconsistent = true;
+        }
+        QAPI_LIST_APPEND(tail, info);
+    }
+}
diff --git a/block/parallels.c b/block/parallels.c
index 2a5ceb8978..e7d65d0458 100644
--- a/block/parallels.c
+++ b/block/parallels.c
@@ -1624,6 +1624,25 @@ static bool parallels_is_support_dirty_bitmaps(BlockDriverState *bs)
     return 1;
 }
 
+static ImageInfoSpecific * GRAPH_RDLOCK
+parallels_get_specific_info(BlockDriverState *bs, Error **errp)
+{
+    ImageInfoSpecificParallels *parallels_info;
+    ImageInfoSpecific *spec_info;
+
+    parallels_info = g_new0(ImageInfoSpecificParallels, 1);
+    parallels_get_bitmap_info_list(bs, &parallels_info->bitmaps);
+    parallels_info->has_bitmaps = !!parallels_info->bitmaps;
+
+    spec_info = g_new(ImageInfoSpecific, 1);
+    *spec_info = (ImageInfoSpecific){
+        .type = IMAGE_INFO_SPECIFIC_KIND_PARALLELS,
+        .u.parallels.data = parallels_info,
+    };
+
+    return spec_info;
+}
+
 static BlockDriver bdrv_parallels = {
     .format_name                = "parallels",
     .instance_size              = sizeof(BDRVParallelsState),
@@ -1653,6 +1672,7 @@ static BlockDriver bdrv_parallels = {
                                   parallels_co_can_store_new_dirty_bitmap,
     .bdrv_co_remove_persistent_dirty_bitmap =
                                   parallels_co_remove_persistent_dirty_bitmap,
+    .bdrv_get_specific_info     = parallels_get_specific_info,
 };
 
 static void bdrv_parallels_init(void)
diff --git a/block/parallels.h b/block/parallels.h
index 27d8c3ac83..012f47320b 100644
--- a/block/parallels.h
+++ b/block/parallels.h
@@ -32,6 +32,7 @@
 #ifndef BLOCK_PARALLELS_H
 #define BLOCK_PARALLELS_H
 #include "qemu/coroutine.h"
+#include "qapi/qapi-types-block-core.h"
 
 #define HEADS_NUMBER 16
 #define SEC_IN_CYL 32
@@ -111,5 +112,7 @@ parallels_co_can_store_new_dirty_bitmap(BlockDriverState *bs, const char *name,
 int coroutine_fn GRAPH_RDLOCK
 parallels_co_remove_persistent_dirty_bitmap(BlockDriverState *bs,
                                             const char *name, Error **errp);
+void parallels_get_bitmap_info_list(BlockDriverState *bs,
+                                    ParallelsBitmapInfoList **info_list);
 
 #endif
diff --git a/qapi/block-core.json b/qapi/block-core.json
index 199efc1e00..7499aea641 100644
--- a/qapi/block-core.json
+++ b/qapi/block-core.json
@@ -188,6 +188,35 @@
       '*extent-size-hint': 'size'
   } }
 
+##
+# @ParallelsBitmapInfo:
+#
+# Parallels dirty bitmap information.
+#
+# @name: the name of the bitmap
+#
+# @granularity: granularity of the bitmap in bytes
+#
+# @inconsistent: true if the bitmap was improperly stored and cannot
+#     be used
+#
+# Since: 11.2
+##
+{ 'struct': 'ParallelsBitmapInfo',
+  'data': { 'name': 'str', 'granularity': 'uint32',
+            '*inconsistent': 'bool' } }
+
+##
+# @ImageInfoSpecificParallels:
+#
+# @bitmaps: A list of the persistent dirty bitmaps of the image,
+#     including the ones which are not written out yet
+#
+# Since: 11.2
+##
+{ 'struct': 'ImageInfoSpecificParallels',
+  'data': { '*bitmaps': ['ParallelsBitmapInfo'] } }
+
 ##
 # @ImageInfoSpecificKind:
 #
@@ -197,10 +226,12 @@
 #
 # @file: Since 8.0
 #
+# @parallels: Since 11.2
+#
 # Since: 1.7
 ##
 { 'enum': 'ImageInfoSpecificKind',
-  'data': [ 'qcow2', 'vmdk', 'luks', 'rbd', 'file' ] }
+  'data': [ 'qcow2', 'vmdk', 'luks', 'rbd', 'file', 'parallels' ] }
 
 ##
 # @ImageInfoSpecificQCow2Wrapper:
@@ -255,6 +286,16 @@
 { 'struct': 'ImageInfoSpecificFileWrapper',
   'data': { 'data': 'ImageInfoSpecificFile' } }
 
+##
+# @ImageInfoSpecificParallelsWrapper:
+#
+# @data: image information specific to Parallels
+#
+# Since: 11.2
+##
+{ 'struct': 'ImageInfoSpecificParallelsWrapper',
+  'data': { 'data': 'ImageInfoSpecificParallels' } }
+
 ##
 # @ImageInfoSpecific:
 #
@@ -273,7 +314,8 @@
       'vmdk': 'ImageInfoSpecificVmdkWrapper',
       'luks': 'ImageInfoSpecificLUKSWrapper',
       'rbd': 'ImageInfoSpecificRbdWrapper',
-      'file': 'ImageInfoSpecificFileWrapper'
+      'file': 'ImageInfoSpecificFileWrapper',
+      'parallels': 'ImageInfoSpecificParallelsWrapper'
   } }
 
 ##
diff --git a/tests/qemu-iotests/tests/parallels-checks b/tests/qemu-iotests/tests/parallels-checks
index 575b736e35..84eb526f44 100755
--- a/tests/qemu-iotests/tests/parallels-checks
+++ b/tests/qemu-iotests/tests/parallels-checks
@@ -382,6 +382,9 @@ echo "== dirty a single granule of the bitmap =="
 file_size=`stat --printf="%s" "$TEST_IMG"`
 echo "file size: $file_size"
 
+echo "== the bitmap is reported by qemu-img info =="
+_img_info --format-specific
+
 echo "== the extension and its bitmap data are not a leak =="
 _check_test_img
 
@@ -425,6 +428,9 @@ $QEMU_IMG bitmap --add -f $IMGFMT "$TEST_IMG" $BITMAP 2>&1 | _filter_testdir
 echo "== pretend the image was not closed correctly =="
 poke_file "$TEST_IMG" "$INUSE_OFFSET" "\x59\x6e\x6f\x74"
 
+echo "== a read-only open still reports it, marked inconsistent =="
+_img_info --format-specific
+
 echo "== the bitmap is stale, so it can not be used and is dropped =="
 $QEMU_IMG bitmap --clear -f $IMGFMT "$TEST_IMG" $BITMAP 2>&1 | _filter_testdir
 
diff --git a/tests/qemu-iotests/tests/parallels-checks.out b/tests/qemu-iotests/tests/parallels-checks.out
index f390ea90d4..c6d9445440 100644
--- a/tests/qemu-iotests/tests/parallels-checks.out
+++ b/tests/qemu-iotests/tests/parallels-checks.out
@@ -241,6 +241,15 @@ qemu-img: Operation add on bitmap bitmap0 failed: Bitmap name must be a UUID to
 wrote 65536/65536 bytes at offset 0
 64 KiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
 file size: 4194304
+== the bitmap is reported by qemu-img info ==
+image: TEST_DIR/t.IMGFMT
+file format: IMGFMT
+virtual size: 4 MiB (4194304 bytes)
+Format specific information:
+    bitmaps:
+        [0]:
+            name: b2c9e1a4-5d3f-4e8b-9a7c-6f0d1e2b3a45
+            granularity: 65536
 == the extension and its bitmap data are not a leak ==
 No errors were found on the image.
 == extend image by 1 cluster ==
@@ -273,6 +282,16 @@ Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=4194304
 wrote 65536/65536 bytes at offset 0
 64 KiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
 == pretend the image was not closed correctly ==
+== a read-only open still reports it, marked inconsistent ==
+image: TEST_DIR/t.IMGFMT
+file format: IMGFMT
+virtual size: 4 MiB (4194304 bytes)
+Format specific information:
+    bitmaps:
+        [0]:
+            name: b2c9e1a4-5d3f-4e8b-9a7c-6f0d1e2b3a45
+            granularity: 65536
+            inconsistent: true
 == the bitmap is stale, so it can not be used and is dropped ==
 Repairing image was not closed correctly
 qemu-img: Operation clear on bitmap b2c9e1a4-5d3f-4e8b-9a7c-6f0d1e2b3a45 failed: Bitmap 'b2c9e1a4-5d3f-4e8b-9a7c-6f0d1e2b3a45' is inconsistent and cannot be used
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 20/25] iotests: rename parallels-read-bitmap to parallels-bitmap
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (18 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 19/25] parallels: report the stored dirty bitmaps in qemu-img info Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 21/25] iotests: cover storing a parallels dirty bitmap Denis V. Lunev
                   ` (4 subsequent siblings)
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi

From: Denis V. Lunev <den@openvz.org>

The test is about to cover storing a bitmap as well, so its name stops
being accurate. Rename it on its own, as the patch which changes what a
test does should not be the one which moves it.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 .../tests/{parallels-read-bitmap => parallels-bitmap}           | 2 +-
 .../tests/{parallels-read-bitmap.out => parallels-bitmap.out}   | 0
 2 files changed, 1 insertion(+), 1 deletion(-)
 rename tests/qemu-iotests/tests/{parallels-read-bitmap => parallels-bitmap} (99%)
 rename tests/qemu-iotests/tests/{parallels-read-bitmap.out => parallels-bitmap.out} (100%)

diff --git a/tests/qemu-iotests/tests/parallels-read-bitmap b/tests/qemu-iotests/tests/parallels-bitmap
similarity index 99%
rename from tests/qemu-iotests/tests/parallels-read-bitmap
rename to tests/qemu-iotests/tests/parallels-bitmap
index f5a1f33907..814b56b32c 100755
--- a/tests/qemu-iotests/tests/parallels-read-bitmap
+++ b/tests/qemu-iotests/tests/parallels-bitmap
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Test parallels load bitmap
+# Test parallels dirty bitmaps
 #
 # Copyright (c) 2021 Virtuozzo International GmbH.
 #
diff --git a/tests/qemu-iotests/tests/parallels-read-bitmap.out b/tests/qemu-iotests/tests/parallels-bitmap.out
similarity index 100%
rename from tests/qemu-iotests/tests/parallels-read-bitmap.out
rename to tests/qemu-iotests/tests/parallels-bitmap.out
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 21/25] iotests: cover storing a parallels dirty bitmap
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (19 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 20/25] iotests: rename parallels-read-bitmap to parallels-bitmap Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 22/25] iotests: cover the qemu-img bitmap operations on parallels Denis V. Lunev
                   ` (3 subsequent siblings)
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi

From: Denis V. Lunev <den@openvz.org>

parallels-bitmap covers loading a bitmap and parsing the Format
Extension. Storing one is the other direction and is not covered at all.

Nothing compares a stored bitmap with the writes it describes: test 165
compares a hash across a VM restart, which the same code produces and
consumes, so a serialization wrong in both directions passes it. Write
known ranges and read the bitmap back over NBD, in the shapes the
storage code decides between: short ranges, a chunk dirty as a whole, a
dirty run shorter than the cluster which serializes it, and a dirty
tail.

A bitmap which does not fit is refused when it is created, as the L1
table shares the Format Extension cluster with the feature headers of
the bitmaps already there. One which fits an empty cluster does not fit
next to seven others, and those seven have to survive the refusal.

The rest is read out of the image file, as that is what the next open
will see: a removal has to reach it when the command is given rather
than when the node is closed, and the in use flag has to go with an
inactivation and come back when the node is activated again. A bitmap
which can not be written used to be lost in silence, so blkdebug fails
the write of its data cluster: the loss has to be reported while the
guest data still goes through, and the flag has to survive the failed
inactivation.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 tests/qemu-iotests/tests/parallels-bitmap     | 178 +++++++++++++++++-
 tests/qemu-iotests/tests/parallels-bitmap.out |  45 +++++
 2 files changed, 222 insertions(+), 1 deletion(-)

diff --git a/tests/qemu-iotests/tests/parallels-bitmap b/tests/qemu-iotests/tests/parallels-bitmap
index 814b56b32c..be0ba8185d 100755
--- a/tests/qemu-iotests/tests/parallels-bitmap
+++ b/tests/qemu-iotests/tests/parallels-bitmap
@@ -25,7 +25,8 @@ import uuid
 from qemu.utils import VerboseProcessError
 
 import iotests
-from iotests import qemu_nbd_popen, qemu_img, qemu_img_map, log, file_path
+from iotests import qemu_nbd_popen, qemu_img, qemu_img_create, qemu_img_map, \
+    qemu_io, log, file_path, QemuStorageDaemon
 
 iotests.script_initialize(supported_fmts=['parallels'])
 
@@ -258,3 +259,178 @@ def check_multi_l1():
 
 
 check_multi_l1()
+
+
+# Storing a bitmap. The driver has to produce the extension itself here,
+# so the images are real ones rather than hand crafted clusters.
+
+INUSE_MAGIC = 0x746F6E59
+INUSE_OFF = 0x2c
+EXT_OFF = 0x38
+TRACKS_OFF = 0x1c
+
+stored = file_path('stored')
+copy = file_path('copy')
+STORED = '9b7b8b2e-1c4a-4d3f-8e6a-1f2d3c4b5a69'
+OTHER = '11111111-2222-3333-4444-555555555555'
+
+
+def read_header(off, fmt):
+    with open(stored, 'rb') as f:
+        return struct.unpack_from(fmt, f.read(0x40), off)[0]
+
+
+def in_use():
+    return read_header(INUSE_OFF, '<I') == INUSE_MAGIC
+
+
+def bitmaps_in_file():
+    """The bitmaps the image file holds right now, node open or not."""
+    ext_off = read_header(EXT_OFF, '<Q') * 512
+    if ext_off == 0:
+        return []
+    with open(stored, 'rb') as f:
+        f.seek(ext_off)
+        ext = f.read(read_header(TRACKS_OFF, '<I') * 512)
+
+    names, pos = [], EH_SIZE
+    while pos + FH_SIZE <= len(ext):
+        magic, _, data_size, _ = struct.unpack_from('<QQII', ext, pos)
+        if magic == 0:
+            break
+        if magic == BITMAP_MAGIC:
+            names.append(struct.unpack_from('<16s', ext, pos + FH_SIZE + 8)[0])
+        pos += FH_SIZE + ((data_size + 7) & ~7)
+    return names
+
+
+def bitmaps_reported():
+    info = iotests.qemu_img_info('-f', iotests.imgfmt, stored)
+    data = info.get('format-specific', {}).get('data', {})
+    return [b['name'] for b in data.get('bitmaps', [])]
+
+
+def make_image(*bitmaps, size='8M', cluster_size=None):
+    opts = ['-o', f'cluster_size={cluster_size}'] if cluster_size else []
+    qemu_img_create('-f', iotests.imgfmt, *opts, stored, size)
+    for name in bitmaps:
+        qemu_img('bitmap', '--add', '-f', iotests.imgfmt, stored, name)
+
+
+def refuse(reason, size):
+    """Add a fine grained bitmap which the Format Extension can not hold."""
+    log(f'--- {reason}')
+    make_image(size=size, cluster_size=512)
+    res = qemu_img('bitmap', '--add', '-g', '512', '-f', iotests.imgfmt,
+                   stored, STORED, check=False)
+    log(iotests.filter_testfiles(res.stdout).strip())
+
+
+def open_node():
+    daemon = QemuStorageDaemon(qmp=True)
+    daemon.cmd('blockdev-add',
+               {'driver': iotests.imgfmt, 'node-name': 'n0',
+                'file': {'driver': 'file', 'filename': stored}})
+    return daemon
+
+
+def stored_dirty(name, size, *writes):
+    """Dirty the given ranges, then read the stored bitmap back over NBD."""
+    log(f'--- {name}')
+    make_image(STORED, size=size)
+    qemu_io('-f', iotests.imgfmt,
+            *[arg for w in writes for arg in ('-c', w)], stored)
+
+    opts = f'driver=nbd,server.type=unix,server.path={nbd_sock}' \
+           f',x-dirty-bitmap=qemu:dirty-bitmap:{STORED}'
+    with qemu_nbd_popen('--read-only', f'--socket={nbd_sock}',
+                        f'--bitmap={STORED}', '-f', iotests.imgfmt, stored):
+        mapping = qemu_img_map('--image-opts', opts)
+
+    for chunk in mapping:
+        if not chunk['data']:
+            log('    dirty %d MiB + %d KiB' % (chunk['start'] // (1 << 20),
+                                               chunk['length'] // 1024))
+
+
+# A bitmap is stored as an L1 table of clusters holding its serialized
+# bits, with an entry of one standing for a chunk which is dirty as a
+# whole. Both of those have to describe exactly what was written.
+stored_dirty('three separate ranges', '64M',
+             'write -P 0x11 0 64k', 'write -P 0x22 4M 128k',
+             'write -P 0x33 40M 64k')
+stored_dirty('the whole image', '8M', 'write -P 0x11 0 8M')
+stored_dirty('less than a cluster', '4M', 'write -P 0x11 0 1M')
+stored_dirty('the tail of the image', '8M', 'write -P 0x11 6M 2M')
+
+# The L1 table shares the Format Extension cluster with the feature
+# headers and the end of features marker, and a small cluster with a fine
+# granularity does not leave room for it. The marker needs room of its own
+# behind the L1 table, so a bitmap which fills the cluster to within a
+# header of the end can not be stored either.
+refuse('a bitmap too large for the Format Extension cluster', '4G')
+refuse('a bitmap leaving no room for the end of features marker', '104M')
+
+# The cluster holds every bitmap of the image at once, so what still fits
+# depends on what is stored there already.
+log('--- a bitmap which does not fit next to the ones already stored')
+make_image(size='64M', cluster_size=512)
+for i in range(1, 9):
+    bm_uuid = '9b7b8b2e-1c4a-4d3f-8e6a-1f2d3c4b5a%02d' % i
+    add_out = qemu_img('bitmap', '--add', '-f', iotests.imgfmt, stored,
+                       bm_uuid, check=False)
+    if add_out.returncode != 0:
+        log('    bitmap %d refused' % i)
+        log('    ' + iotests.filter_testfiles(add_out.stdout).strip())
+        break
+
+log('--- the ones which did fit are all still there')
+log('    %d bitmaps stored' % len(bitmaps_reported()))
+
+log('--- removing a bitmap reaches the image right away')
+make_image(STORED, OTHER)
+qsd = open_node()
+log(f'    stored before: {len(bitmaps_in_file())} bitmaps')
+qsd.cmd('block-dirty-bitmap-remove', {'node': 'n0', 'name': STORED})
+log(f'    stored after:  {len(bitmaps_in_file())} bitmaps, '
+    'without a clean close')
+qsd.stop()
+
+log('--- the in use flag comes back when a node is activated again')
+make_image(STORED)
+qsd = open_node()
+log(f'    in use while open:         {in_use()}')
+qsd.cmd('blockdev-set-active', {'node-name': 'n0', 'active': False})
+log(f'    in use after deactivation: {in_use()}')
+qsd.cmd('blockdev-set-active', {'node-name': 'n0', 'active': True})
+log(f'    in use after activation:   {in_use()}')
+qsd.stop()
+
+# The bitmap data cluster is allocated right behind the only data cluster,
+# so it starts at sector 4096 of a 4M image with the default cluster size.
+log('--- a bitmap which can not be written is reported, not lost in silence')
+make_image(STORED, size='4M')
+err_opts = (f'driver={iotests.imgfmt},file.driver=blkdebug,'
+            f'file.image.driver=file,file.image.filename={stored},'
+            f'file.inject-error.0.event=none,'
+            f'file.inject-error.0.iotype=write,'
+            f'file.inject-error.0.sector=4096')
+err_out = qemu_io('--image-opts', err_opts, '-c', 'write -P 0x11 0 64k',
+                  check=False)
+for err_line in iotests.filter_generated_node_ids(err_out.stdout).splitlines():
+    if 'Lost persistent bitmaps' in err_line:
+        log('    ' + err_line.strip())
+log(f'    in use after the failure:  {in_use()}')
+
+log('--- but the guest data went through, and the image says it is in use')
+read_out = qemu_io('-f', iotests.imgfmt, '-r', '-c', 'read -P 0x11 0 64k',
+                   stored)
+log(read_out.stdout, filters=[iotests.filter_generated_node_ids,
+                              iotests.filter_testfiles,
+                              iotests.filter_qemu_io])
+log('    %d bitmaps stored' % len(bitmaps_reported()))
+
+log('--- and the image is still readable')
+info_out = qemu_img('info', '-f', iotests.imgfmt, stored, check=False)
+log('image opened' if info_out.returncode == 0
+    else iotests.filter_testfiles(info_out.stdout).strip())
diff --git a/tests/qemu-iotests/tests/parallels-bitmap.out b/tests/qemu-iotests/tests/parallels-bitmap.out
index ebfc0acb4d..994c67390e 100644
--- a/tests/qemu-iotests/tests/parallels-bitmap.out
+++ b/tests/qemu-iotests/tests/parallels-bitmap.out
@@ -47,3 +47,48 @@ image opened
 Start NBD server
 dirty 32-64 GiB
 Kill NBD server
+--- three separate ranges
+Start NBD server
+Kill NBD server
+    dirty 0 MiB + 64 KiB
+    dirty 4 MiB + 128 KiB
+    dirty 40 MiB + 64 KiB
+--- the whole image
+Start NBD server
+Kill NBD server
+    dirty 0 MiB + 8192 KiB
+--- less than a cluster
+Start NBD server
+Kill NBD server
+    dirty 0 MiB + 1024 KiB
+--- the tail of the image
+Start NBD server
+Kill NBD server
+    dirty 6 MiB + 2048 KiB
+--- a bitmap too large for the Format Extension cluster
+qemu-img: Operation add on bitmap 9b7b8b2e-1c4a-4d3f-8e6a-1f2d3c4b5a69 failed: Bitmap 9b7b8b2e-1c4a-4d3f-8e6a-1f2d3c4b5a69 with granularity 512 does not fit into the Format Extension cluster: every bitmap of the image would need 16464 bytes of it, 488 are available
+--- a bitmap leaving no room for the end of features marker
+qemu-img: Operation add on bitmap 9b7b8b2e-1c4a-4d3f-8e6a-1f2d3c4b5a69 failed: Bitmap 9b7b8b2e-1c4a-4d3f-8e6a-1f2d3c4b5a69 with granularity 512 does not fit into the Format Extension cluster: every bitmap of the image would need 496 bytes of it, 488 are available
+--- a bitmap which does not fit next to the ones already stored
+    bitmap 8 refused
+    qemu-img: Operation add on bitmap 9b7b8b2e-1c4a-4d3f-8e6a-1f2d3c4b5a08 failed: Bitmap 9b7b8b2e-1c4a-4d3f-8e6a-1f2d3c4b5a08 with granularity 65536 does not fit into the Format Extension cluster: every bitmap of the image would need 536 bytes of it, 488 are available
+--- the ones which did fit are all still there
+    7 bitmaps stored
+--- removing a bitmap reaches the image right away
+    stored before: 2 bitmaps
+    stored after:  1 bitmaps, without a clean close
+--- the in use flag comes back when a node is activated again
+    in use while open:         True
+    in use after deactivation: False
+    in use after activation:   True
+--- a bitmap which can not be written is reported, not lost in silence
+    qemu-io: Lost persistent bitmaps during inactivation of node 'NODE_NAME': Can't save dirty bitmap 9b7b8b2e-1c4a-4d3f-8e6a-1f2d3c4b5a69: IO error: Input/output error
+    in use after the failure:  True
+--- but the guest data went through, and the image says it is in use
+qemu-io: warning: Dropping the Format Extension of node 'NODE_NAME', which does not look like one: Wrong parallels Format Extension magic: 0x1111111111111111, expected: 0xab234cef23dcea87
+read 65536/65536 bytes at offset 0
+64 KiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+
+    0 bitmaps stored
+--- and the image is still readable
+image opened
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 22/25] iotests: cover the qemu-img bitmap operations on parallels
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (20 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 21/25] iotests: cover storing a parallels dirty bitmap Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 23/25] iotests: cover a broken Format Extension and a combined repair Denis V. Lunev
                   ` (2 subsequent siblings)
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi

From: Denis V. Lunev <den@openvz.org>

qemu-img-bitmaps drives convert --bitmaps and every qemu-img bitmap
sub-command, but for qcow2 alone. It is built on a backing chain and a
rebase, which parallels has neither of, so half of it could not run
there and the format is left with no coverage of any of this.

Cover what applies in the parallels test instead. A copy made with
--bitmaps has to carry them, and raw has to refuse them. --merge and
--clear leave the bitmap count alone, --remove-all sweeps it to zero,
and --remove-all --add in one invocation leaves only the bitmap which
was just added.

An image which was not closed correctly is left out. parallels-checks
covers that already, next to the code which drops the bitmaps.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 tests/qemu-iotests/tests/parallels-bitmap     | 39 +++++++++++++++++++
 tests/qemu-iotests/tests/parallels-bitmap.out | 12 ++++++
 2 files changed, 51 insertions(+)

diff --git a/tests/qemu-iotests/tests/parallels-bitmap b/tests/qemu-iotests/tests/parallels-bitmap
index be0ba8185d..8ae9314114 100755
--- a/tests/qemu-iotests/tests/parallels-bitmap
+++ b/tests/qemu-iotests/tests/parallels-bitmap
@@ -273,6 +273,7 @@ stored = file_path('stored')
 copy = file_path('copy')
 STORED = '9b7b8b2e-1c4a-4d3f-8e6a-1f2d3c4b5a69'
 OTHER = '11111111-2222-3333-4444-555555555555'
+MERGED = '22222222-3333-4444-5555-666666666666'
 
 
 def read_header(off, fmt):
@@ -434,3 +435,41 @@ log('--- and the image is still readable')
 info_out = qemu_img('info', '-f', iotests.imgfmt, stored, check=False)
 log('image opened' if info_out.returncode == 0
     else iotests.filter_testfiles(info_out.stdout).strip())
+
+
+# qemu-img-bitmaps drives these for qcow2 alone, as it is built on a
+# backing chain and a rebase, which parallels has neither of.
+
+log('--- bitmaps can not be preserved into raw')
+make_image(STORED)
+raw_out = qemu_img('convert', '--bitmaps', '-f', iotests.imgfmt, '-O',
+                   'raw', stored, copy, check=False)
+log('    ' + iotests.filter_testfiles(raw_out.stdout).strip())
+
+log('--- convert --bitmaps carries the bitmaps into the copy')
+make_image(STORED, OTHER)
+qemu_io('-f', iotests.imgfmt, '-c', 'write -P 0x11 1M 64k', stored)
+qemu_img('convert', '--bitmaps', '-f', iotests.imgfmt, '-O', iotests.imgfmt,
+         stored, copy)
+copy_info = iotests.qemu_img_info('-f', iotests.imgfmt, copy)
+copy_data = copy_info.get('format-specific', {}).get('data', {})
+log('    %d bitmaps in the copy' % len(copy_data.get('bitmaps', [])))
+
+log('--- merging one bitmap into a new one')
+qemu_img('bitmap', '--add', '--merge', STORED, '-f', iotests.imgfmt,
+         stored, MERGED)
+log('    %d bitmaps stored' % len(bitmaps_reported()))
+
+log('--- clearing a bitmap keeps it in the image')
+qemu_img('bitmap', '--clear', '-f', iotests.imgfmt, stored, STORED)
+log('    %d bitmaps stored' % len(bitmaps_reported()))
+
+log('--- remove-all sweeps every bitmap of the image')
+qemu_img('bitmap', '--remove-all', '-f', iotests.imgfmt, stored)
+log('    %d bitmaps after remove-all' % len(bitmaps_reported()))
+
+log('--- remove-all with --add leaves only the new bitmap')
+make_image(STORED, OTHER)
+qemu_img('bitmap', '--remove-all', '--add', '-f', iotests.imgfmt,
+         stored, MERGED)
+log('    ' + ' '.join(bitmaps_reported()))
diff --git a/tests/qemu-iotests/tests/parallels-bitmap.out b/tests/qemu-iotests/tests/parallels-bitmap.out
index 994c67390e..df6f676106 100644
--- a/tests/qemu-iotests/tests/parallels-bitmap.out
+++ b/tests/qemu-iotests/tests/parallels-bitmap.out
@@ -92,3 +92,15 @@ read 65536/65536 bytes at offset 0
     0 bitmaps stored
 --- and the image is still readable
 image opened
+--- bitmaps can not be preserved into raw
+    qemu-img: Format driver 'raw' does not support bitmaps
+--- convert --bitmaps carries the bitmaps into the copy
+    2 bitmaps in the copy
+--- merging one bitmap into a new one
+    3 bitmaps stored
+--- clearing a bitmap keeps it in the image
+    3 bitmaps stored
+--- remove-all sweeps every bitmap of the image
+    0 bitmaps after remove-all
+--- remove-all with --add leaves only the new bitmap
+    22222222-3333-4444-5555-666666666666
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 23/25] iotests: cover a broken Format Extension and a combined repair
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (21 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 22/25] iotests: cover the qemu-img bitmap operations on parallels Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 24/25] tests: Turned on 256, 299, 304 and block-status-cache for parallels format Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 25/25] tests: Add parallels format support to image-fleecing Denis V. Lunev
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi

From: Denis V. Lunev <den@openvz.org>

An image which was not closed correctly may have had its Format
Extension cluster reused by a guest write, which is what the reuse of
those clusters is for. It has to open anyway, as the payload is intact,
and it has to stop pointing at what is no longer an extension.

The same damage in an image which was closed correctly is what an older
qemu leaves behind, as it ignores the extension in read-write mode and
truncates the file to the end of the payload on close. Both shapes of
it are covered: the cluster overwritten with something which is not an
extension, and the cluster truncated away with the header still
pointing past the end of the file. Neither keeps the image shut, and
qemu-img info is enough to tell, as it opens the image on its own.

Rebuilding the used bitmap after a leak is repaired reports the errors
which come with a BAT pointing a cluster twice, and those are the ones
qemu-img check is there to fix, so a duplicate entry next to a leak is
covered too.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 tests/qemu-iotests/tests/parallels-checks     | 80 +++++++++++++++++++
 tests/qemu-iotests/tests/parallels-checks.out | 57 +++++++++++++
 2 files changed, 137 insertions(+)

diff --git a/tests/qemu-iotests/tests/parallels-checks b/tests/qemu-iotests/tests/parallels-checks
index 84eb526f44..ca89ed5d62 100755
--- a/tests/qemu-iotests/tests/parallels-checks
+++ b/tests/qemu-iotests/tests/parallels-checks
@@ -443,6 +443,86 @@ echo "== guest data was never in doubt =="
 # Clear image
 _make_test_img $SIZE
 
+echo "== TEST BROKEN EXTENSION OF AN IMAGE WHICH WAS NOT CLOSED =="
+
+EXT_OFF_OFFSET=56
+
+echo "== add a persistent dirty bitmap =="
+$QEMU_IMG bitmap --add -f $IMGFMT "$TEST_IMG" $BITMAP 2>&1 | _filter_testdir
+
+ext_off=$(peek_file_le "$TEST_IMG" $EXT_OFF_OFFSET 8)
+
+echo "== a guest write may reuse the extension cluster, so clobber it =="
+poke_file "$TEST_IMG" $((ext_off * 512)) "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"
+
+echo "== pretend the image was not closed correctly =="
+poke_file "$TEST_IMG" "$INUSE_OFFSET" "\x59\x6e\x6f\x74"
+
+echo "== the image opens, the extension is dropped =="
+{ $QEMU_IO -c "write -P 0x11 0 64k" "$TEST_IMG"; } 2>&1 | _filter_qemu_io |
+    _filter_testdir | _filter_generated_node_ids
+
+echo "== and it stopped pointing at the broken extension =="
+echo "ext_off: $(peek_file_le "$TEST_IMG" $EXT_OFF_OFFSET 8)"
+
+echo "== an older qemu leaves the same damage behind a clean close =="
+_make_test_img $SIZE
+$QEMU_IMG bitmap --add -f $IMGFMT "$TEST_IMG" $BITMAP 2>&1 | _filter_testdir
+ext_off=$(peek_file_le "$TEST_IMG" $EXT_OFF_OFFSET 8)
+poke_file "$TEST_IMG" $((ext_off * 512)) "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"
+_img_info | _filter_generated_node_ids
+
+echo "== truncating the extension away is dropped as well =="
+_make_test_img $SIZE
+$QEMU_IMG bitmap --add -f $IMGFMT "$TEST_IMG" $BITMAP 2>&1 | _filter_testdir
+ext_off=$(peek_file_le "$TEST_IMG" $EXT_OFF_OFFSET 8)
+truncate -s $((ext_off * 512)) "$TEST_IMG"
+_img_info | _filter_generated_node_ids
+
+# Clear image
+_make_test_img $SIZE
+
+echo "== TEST REPAIR OF A DUPLICATE ENTRY NEXT TO A LEAK =="
+
+echo "== write two clusters =="
+{ $QEMU_IO -c "write -P 0x11 0 $CLUSTER_SIZE" \
+           -c "write -P 0x22 $CLUSTER_SIZE $CLUSTER_SIZE" \
+           "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
+
+echo "== point the second BAT entry at the first cluster =="
+first=$(peek_file_le "$TEST_IMG" $BAT_OFFSET 4)
+poke_file_le "$TEST_IMG" $(($BAT_OFFSET + 4)) 4 $first
+
+echo "== leak a cluster at the end of the image =="
+file_size=`stat --printf="%s" "$TEST_IMG"`
+fallocate -xl $((file_size + CLUSTER_SIZE)) "$TEST_IMG"
+
+echo "== both are repaired in one go =="
+_check_test_img -r all
+
+echo "== and the image opens read-write afterwards =="
+{ $QEMU_IO -c "read -P 0x11 0 $CLUSTER_SIZE" "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
+
+# Clear image
+_make_test_img $SIZE
+
+echo "== TEST A LEAK WHICH DOES NOT FIT AN INT =="
+
+echo "== write one cluster =="
+{ $QEMU_IO -c "write -P 0x11 0 $CLUSTER_SIZE" "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
+
+echo "== leave 3 GiB of unused space behind it =="
+truncate -s $((3 * 1024 * 1024 * 1024)) "$TEST_IMG"
+
+echo "== closing the image truncates it without complaining =="
+{ $QEMU_IO -c "read -P 0x11 0 $CLUSTER_SIZE" "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
+
+file_size=`stat --printf="%s" "$TEST_IMG"`
+echo "file size: $file_size"
+
+# Clear image
+_make_test_img $SIZE
+
 echo "== TEST A DUPLICATE IN THE LAST ALLOCATED BAT ENTRY =="
 
 echo "== write two clusters =="
diff --git a/tests/qemu-iotests/tests/parallels-checks.out b/tests/qemu-iotests/tests/parallels-checks.out
index c6d9445440..f6806f82fa 100644
--- a/tests/qemu-iotests/tests/parallels-checks.out
+++ b/tests/qemu-iotests/tests/parallels-checks.out
@@ -302,6 +302,63 @@ qemu-img: warning: Dropping inconsistent bitmap b2c9e1a4-5d3f-4e8b-9a7c-6f0d1e2b
 read 65536/65536 bytes at offset 0
 64 KiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
 Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=4194304
+== TEST BROKEN EXTENSION OF AN IMAGE WHICH WAS NOT CLOSED ==
+== add a persistent dirty bitmap ==
+== a guest write may reuse the extension cluster, so clobber it ==
+== pretend the image was not closed correctly ==
+== the image opens, the extension is dropped ==
+qemu-io: warning: Dropping the Format Extension of node 'NODE_NAME', which does not look like one: Wrong parallels Format Extension magic: 0xaaaaaaaaaaaaaaaa, expected: 0xab234cef23dcea87
+Repairing image was not closed correctly
+Repairing space leaked at the end of the image 1048576
+wrote 65536/65536 bytes at offset 0
+64 KiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+== and it stopped pointing at the broken extension ==
+ext_off: 0
+== an older qemu leaves the same damage behind a clean close ==
+Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=4194304
+qemu-img: warning: Dropping the Format Extension of node 'NODE_NAME', which does not look like one: Wrong IMGFMT Format Extension magic: 0xaaaaaaaaaaaaaaaa, expected: 0xab234cef23dcea87
+image: TEST_DIR/t.IMGFMT
+file format: IMGFMT
+virtual size: 4 MiB (4194304 bytes)
+== truncating the extension away is dropped as well ==
+Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=4194304
+qemu-img: warning: Dropping the Format Extension of node 'NODE_NAME', which does not look like one: Format Extension is outside the image file
+image: TEST_DIR/t.IMGFMT
+file format: IMGFMT
+virtual size: 4 MiB (4194304 bytes)
+Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=4194304
+== TEST REPAIR OF A DUPLICATE ENTRY NEXT TO A LEAK ==
+== write two clusters ==
+wrote 1048576/1048576 bytes at offset 0
+1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+wrote 1048576/1048576 bytes at offset 1048576
+1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+== point the second BAT entry at the first cluster ==
+== leak a cluster at the end of the image ==
+== both are repaired in one go ==
+Repairing space leaked at the end of the image 2097152
+Repairing duplicate offset in BAT entry 1
+The following inconsistencies were found and repaired:
+
+    2 leaked clusters
+    1 corruptions
+
+Double checking the fixed image now...
+No errors were found on the image.
+== and the image opens read-write afterwards ==
+read 1048576/1048576 bytes at offset 0
+1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=4194304
+== TEST A LEAK WHICH DOES NOT FIT AN INT ==
+== write one cluster ==
+wrote 1048576/1048576 bytes at offset 0
+1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+== leave 3 GiB of unused space behind it ==
+== closing the image truncates it without complaining ==
+read 1048576/1048576 bytes at offset 0
+1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+file size: 2097152
+Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=4194304
 == TEST A DUPLICATE IN THE LAST ALLOCATED BAT ENTRY ==
 == write two clusters ==
 wrote 1048576/1048576 bytes at offset 0
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 24/25] tests: Turned on 256, 299, 304 and block-status-cache for parallels format
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (22 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 23/25] iotests: cover a broken Format Extension and a combined repair Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-03 14:41 ` [PATCH v7 25/25] tests: Add parallels format support to image-fleecing Denis V. Lunev
  24 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel; +Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi, Alexander Ivanov

From: Alexander Ivanov <alexander.ivanov@virtuozzo.com>

These tests pass with parallels format. Add parallels to supporting
formats for these tests.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 tests/qemu-iotests/256                      | 2 +-
 tests/qemu-iotests/299                      | 2 +-
 tests/qemu-iotests/304                      | 2 +-
 tests/qemu-iotests/tests/block-status-cache | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/tests/qemu-iotests/256 b/tests/qemu-iotests/256
index f34af6cef7..1a4c9c6885 100755
--- a/tests/qemu-iotests/256
+++ b/tests/qemu-iotests/256
@@ -26,7 +26,7 @@ from iotests import log
 
 iotests.verify_virtio_scsi_pci_or_ccw()
 
-iotests.script_initialize(supported_fmts=['qcow2'])
+iotests.script_initialize(supported_fmts=['qcow2', 'parallels'])
 size = 64 * 1024 * 1024
 
 with iotests.FilePath('img0') as img0_path, \
diff --git a/tests/qemu-iotests/299 b/tests/qemu-iotests/299
index a7122941fd..d8c4399446 100755
--- a/tests/qemu-iotests/299
+++ b/tests/qemu-iotests/299
@@ -23,7 +23,7 @@ import iotests
 
 # The test is unrelated to formats, restrict it to qcow2 to avoid extra runs
 iotests.script_initialize(
-    supported_fmts=['qcow2'],
+    supported_fmts=['qcow2', 'parallels'],
 )
 
 nbd_sock = iotests.file_path('nbd.sock', base_dir=iotests.sock_dir)
diff --git a/tests/qemu-iotests/304 b/tests/qemu-iotests/304
index d465a519a9..03c7c1cbe3 100755
--- a/tests/qemu-iotests/304
+++ b/tests/qemu-iotests/304
@@ -23,7 +23,7 @@
 import iotests
 from iotests import qemu_img_create, qemu_img_log, file_path
 
-iotests.script_initialize(supported_fmts=['qcow2'],
+iotests.script_initialize(supported_fmts=['qcow2', 'parallels'],
                           supported_protocols=['file'],
                           require_hmp=True)
 
diff --git a/tests/qemu-iotests/tests/block-status-cache b/tests/qemu-iotests/tests/block-status-cache
index 5a7bc2c149..ade3d5b169 100755
--- a/tests/qemu-iotests/tests/block-status-cache
+++ b/tests/qemu-iotests/tests/block-status-cache
@@ -131,5 +131,5 @@ class TestBscWithNbd(iotests.QMPTestCase):
 if __name__ == '__main__':
     # The block-status cache only works on the protocol layer, so to test it,
     # we can only use the raw format
-    iotests.main(supported_fmts=['raw'],
+    iotests.main(supported_fmts=['raw', 'parallels'],
                  supported_protocols=['file'])
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v7 25/25] tests: Add parallels format support to image-fleecing
  2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
                   ` (23 preceding siblings ...)
  2026-09-03 14:41 ` [PATCH v7 24/25] tests: Turned on 256, 299, 304 and block-status-cache for parallels format Denis V. Lunev
@ 2026-09-03 14:41 ` Denis V. Lunev
  2026-09-09 14:35   ` Vladimir Sementsov-Ogievskiy
  24 siblings, 1 reply; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-03 14:41 UTC (permalink / raw)
  To: qemu-devel
  Cc: qemu-block, Denis V. Lunev, Stefan Hajnoczi,
	Vladimir Sementsov-Ogievskiy

From: Denis V. Lunev <den@openvz.org>

Use a different bitmap name for parallels images, as the format has its
own ID format and can not hold an arbitrary string.

Replace the hardcoded 'qcow2' target format with iotests.imgfmt.

Add 'parallels' to the supported formats.

Based on the original work from Alexander Ivanov.

Cc: Stefan Hajnoczi <stefanha@redhat.com>
Cc: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 tests/qemu-iotests/tests/image-fleecing | 14 ++++++++++----
 1 file changed, 10 insertions(+), 4 deletions(-)

diff --git a/tests/qemu-iotests/tests/image-fleecing b/tests/qemu-iotests/tests/image-fleecing
index b42aa6967f..0142289843 100755
--- a/tests/qemu-iotests/tests/image-fleecing
+++ b/tests/qemu-iotests/tests/image-fleecing
@@ -28,7 +28,7 @@ import iotests
 from iotests import log, qemu_img, qemu_io
 
 iotests.script_initialize(
-    supported_fmts=['qcow2'],
+    supported_fmts=['qcow2', 'parallels'],
     supported_platforms=['linux'],
     required_fmts=['copy-before-write'],
     unsupported_imgopts=['compat'],
@@ -62,12 +62,18 @@ def do_test(vm, use_cbw, use_snapshot_access_filter, base_img_path,
     if push_backup:
         assert use_cbw
 
+    # parallels stores a bitmap name as the UUID identifying it in the image
+    if iotests.imgfmt == 'parallels':
+        bitmap_name = '00000000-0000-0000-0000-000000000000'
+    else:
+        bitmap_name = 'bitmap0'
+
     log('--- Setting up images ---')
     log('')
 
     qemu_img('create', '-f', iotests.imgfmt, base_img_path, '64M')
     if bitmap:
-        qemu_img('bitmap', '--add', base_img_path, 'bitmap0')
+        qemu_img('bitmap', '--add', base_img_path, bitmap_name)
 
     if use_snapshot_access_filter:
         assert use_cbw
@@ -76,7 +82,7 @@ def do_test(vm, use_cbw, use_snapshot_access_filter, base_img_path,
         qemu_img('create', '-f', 'qcow2', fleece_img_path, '64M')
 
     if push_backup:
-        qemu_img('create', '-f', 'qcow2', target_img_path, '64M')
+        qemu_img('create', '-f', iotests.imgfmt, target_img_path, '64M')
 
     for p in patterns:
         qemu_io('-f', iotests.imgfmt,
@@ -131,7 +137,7 @@ def do_test(vm, use_cbw, use_snapshot_access_filter, base_img_path,
         }
 
         if bitmap:
-            fl_cbw['bitmap'] = {'node': src_node, 'name': 'bitmap0'}
+            fl_cbw['bitmap'] = {'node': src_node, 'name': bitmap_name}
 
         log(vm.qmp('blockdev-add', fl_cbw))
 
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 29+ messages in thread

* Re: [PATCH v7 19/25] parallels: report the stored dirty bitmaps in qemu-img info
  2026-09-03 14:41 ` [PATCH v7 19/25] parallels: report the stored dirty bitmaps in qemu-img info Denis V. Lunev
@ 2026-09-04  8:49   ` Markus Armbruster
  2026-09-13 19:50     ` Denis V. Lunev
  0 siblings, 1 reply; 29+ messages in thread
From: Markus Armbruster @ 2026-09-04  8:49 UTC (permalink / raw)
  To: Denis V. Lunev; +Cc: qemu-devel, qemu-block, Stefan Hajnoczi, Eric Blake

"Denis V. Lunev" <den@openvz.org> writes:

> From: Denis V. Lunev <den@openvz.org>
>
> Nothing tells which persistent bitmaps an image carries. qemu-img info
> says nothing about them, and the only other way to see one is to export
> the image over NBD and ask for a bitmap by name, which needs the name
> beforehand.
>
> Add ImageInfoSpecificParallels with the bitmaps and their granularity,
> in the same way as qcow2 reports the contents of its bitmap directory:
>
>     Format specific information:
>         bitmaps:
>             [0]:
>                 name: b2c9e1a4-5d3f-4e8b-9a7c-6f0d1e2b3a45
>                 granularity: 65536
>
> For running VM process the list is built on the fly from in-memory
> list. This is the best we can do. FormatExtension could be dead in
> the image while VMs are running as all bitmaps are cleared on
> non-clean VM stop.

What's "FormatExtension"?
>
> A bitmap of an image which was not closed correctly is inconsistent
> and can not be used. Report that as well, the way qcow2 reports its
> in-use flag, so such a bitmap is not listed as a valid one.

This is helpful, thanks!

> The section is left empty for an image with no persistent dirty
> bitmaps, and an empty section is not printed, so the human readable
> output of "qemu-img info" and "info block" is unchanged.

Perfectly clear now.

> Cc: Stefan Hajnoczi <stefanha@redhat.com>
> Cc: Eric Blake <eblake@redhat.com>
> Cc: Markus Armbruster <armbru@redhat.com>
> Signed-off-by: Denis V. Lunev <den@openvz.org>
> ---
>  block/parallels-ext.c                         | 26 +++++++++++
>  block/parallels.c                             | 20 ++++++++
>  block/parallels.h                             |  3 ++
>  qapi/block-core.json                          | 46 ++++++++++++++++++-
>  tests/qemu-iotests/tests/parallels-checks     |  6 +++
>  tests/qemu-iotests/tests/parallels-checks.out | 19 ++++++++
>  6 files changed, 118 insertions(+), 2 deletions(-)
>
> diff --git a/block/parallels-ext.c b/block/parallels-ext.c
> index f687f2da7c..8b8035af91 100644
> --- a/block/parallels-ext.c
> +++ b/block/parallels-ext.c
> @@ -736,3 +736,29 @@ parallels_co_remove_persistent_dirty_bitmap(BlockDriverState *bs,
>  
>      return ret;
>  }
> +
> +void parallels_get_bitmap_info_list(BlockDriverState *bs,
> +                                    ParallelsBitmapInfoList **info_list)
> +{
> +    BdrvDirtyBitmap *bitmap;
> +    ParallelsBitmapInfoList **tail = info_list;
> +
> +    *info_list = NULL;
> +
> +    FOR_EACH_DIRTY_BITMAP(bs, bitmap) {
> +        ParallelsBitmapInfo *info;
> +
> +        if (!bdrv_dirty_bitmap_get_persistence(bitmap)) {
> +            continue;
> +        }
> +
> +        info = g_new0(ParallelsBitmapInfo, 1);
> +        info->name = g_strdup(bdrv_dirty_bitmap_name(bitmap));
> +        info->granularity = bdrv_dirty_bitmap_granularity(bitmap);
> +        if (bdrv_dirty_bitmap_inconsistent(bitmap)) {
> +            info->has_inconsistent = true;
> +            info->inconsistent = true;
> +        }
> +        QAPI_LIST_APPEND(tail, info);
> +    }
> +}
> diff --git a/block/parallels.c b/block/parallels.c
> index 2a5ceb8978..e7d65d0458 100644
> --- a/block/parallels.c
> +++ b/block/parallels.c
> @@ -1624,6 +1624,25 @@ static bool parallels_is_support_dirty_bitmaps(BlockDriverState *bs)
>      return 1;
>  }
>  
> +static ImageInfoSpecific * GRAPH_RDLOCK
> +parallels_get_specific_info(BlockDriverState *bs, Error **errp)
> +{
> +    ImageInfoSpecificParallels *parallels_info;
> +    ImageInfoSpecific *spec_info;
> +
> +    parallels_info = g_new0(ImageInfoSpecificParallels, 1);
> +    parallels_get_bitmap_info_list(bs, &parallels_info->bitmaps);
> +    parallels_info->has_bitmaps = !!parallels_info->bitmaps;
> +
> +    spec_info = g_new(ImageInfoSpecific, 1);
> +    *spec_info = (ImageInfoSpecific){
> +        .type = IMAGE_INFO_SPECIFIC_KIND_PARALLELS,
> +        .u.parallels.data = parallels_info,
> +    };
> +
> +    return spec_info;
> +}
> +
>  static BlockDriver bdrv_parallels = {
>      .format_name                = "parallels",
>      .instance_size              = sizeof(BDRVParallelsState),
> @@ -1653,6 +1672,7 @@ static BlockDriver bdrv_parallels = {
>                                    parallels_co_can_store_new_dirty_bitmap,
>      .bdrv_co_remove_persistent_dirty_bitmap =
>                                    parallels_co_remove_persistent_dirty_bitmap,
> +    .bdrv_get_specific_info     = parallels_get_specific_info,
>  };
>  
>  static void bdrv_parallels_init(void)
> diff --git a/block/parallels.h b/block/parallels.h
> index 27d8c3ac83..012f47320b 100644
> --- a/block/parallels.h
> +++ b/block/parallels.h
> @@ -32,6 +32,7 @@
>  #ifndef BLOCK_PARALLELS_H
>  #define BLOCK_PARALLELS_H
>  #include "qemu/coroutine.h"
> +#include "qapi/qapi-types-block-core.h"
>  
>  #define HEADS_NUMBER 16
>  #define SEC_IN_CYL 32
> @@ -111,5 +112,7 @@ parallels_co_can_store_new_dirty_bitmap(BlockDriverState *bs, const char *name,
>  int coroutine_fn GRAPH_RDLOCK
>  parallels_co_remove_persistent_dirty_bitmap(BlockDriverState *bs,
>                                              const char *name, Error **errp);
> +void parallels_get_bitmap_info_list(BlockDriverState *bs,
> +                                    ParallelsBitmapInfoList **info_list);
>  
>  #endif
> diff --git a/qapi/block-core.json b/qapi/block-core.json
> index 199efc1e00..7499aea641 100644
> --- a/qapi/block-core.json
> +++ b/qapi/block-core.json
> @@ -188,6 +188,35 @@
>        '*extent-size-hint': 'size'
>    } }
>  
> +##
> +# @ParallelsBitmapInfo:
> +#
> +# Parallels dirty bitmap information.
> +#
> +# @name: the name of the bitmap
> +#
> +# @granularity: granularity of the bitmap in bytes
> +#
> +# @inconsistent: true if the bitmap was improperly stored and cannot
> +#     be used

I'd recommend "if the bitmap is in an inconsistent state and".

> +#
> +# Since: 11.2
> +##
> +{ 'struct': 'ParallelsBitmapInfo',
> +  'data': { 'name': 'str', 'granularity': 'uint32',
> +            '*inconsistent': 'bool' } }
> +
> +##
> +# @ImageInfoSpecificParallels:
> +#
> +# @bitmaps: A list of the persistent dirty bitmaps of the image,
> +#     including the ones which are not written out yet

In review of v6, I asked whether "not written out yet" relevant to a
user / management application.  You explained what it means, but didn't
actually answer my question.  I failed to point that out then, sorry.
Can you answer it now?

I also inquired about the relation to ImageInfoSpecificQCow2's @bitmaps,
and you told me they're basically the same (correct me if I
misunderstood).  I then replied that the documentation should be
basically the same, and you agreed.  Did you forget?

> +#
> +# Since: 11.2
> +##
> +{ 'struct': 'ImageInfoSpecificParallels',
> +  'data': { '*bitmaps': ['ParallelsBitmapInfo'] } }
> +
>  ##
>  # @ImageInfoSpecificKind:
>  #
> @@ -197,10 +226,12 @@
>  #
>  # @file: Since 8.0
>  #
> +# @parallels: Since 11.2
> +#
>  # Since: 1.7
>  ##
>  { 'enum': 'ImageInfoSpecificKind',
> -  'data': [ 'qcow2', 'vmdk', 'luks', 'rbd', 'file' ] }
> +  'data': [ 'qcow2', 'vmdk', 'luks', 'rbd', 'file', 'parallels' ] }
>  
>  ##
>  # @ImageInfoSpecificQCow2Wrapper:
> @@ -255,6 +286,16 @@
>  { 'struct': 'ImageInfoSpecificFileWrapper',
>    'data': { 'data': 'ImageInfoSpecificFile' } }
>  
> +##
> +# @ImageInfoSpecificParallelsWrapper:
> +#
> +# @data: image information specific to Parallels
> +#
> +# Since: 11.2
> +##
> +{ 'struct': 'ImageInfoSpecificParallelsWrapper',
> +  'data': { 'data': 'ImageInfoSpecificParallels' } }
> +
>  ##
>  # @ImageInfoSpecific:
>  #
> @@ -273,7 +314,8 @@
>        'vmdk': 'ImageInfoSpecificVmdkWrapper',
>        'luks': 'ImageInfoSpecificLUKSWrapper',
>        'rbd': 'ImageInfoSpecificRbdWrapper',
> -      'file': 'ImageInfoSpecificFileWrapper'
> +      'file': 'ImageInfoSpecificFileWrapper',
> +      'parallels': 'ImageInfoSpecificParallelsWrapper'
>    } }
>  
>  ##

[...]



^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH v7 25/25] tests: Add parallels format support to image-fleecing
  2026-09-03 14:41 ` [PATCH v7 25/25] tests: Add parallels format support to image-fleecing Denis V. Lunev
@ 2026-09-09 14:35   ` Vladimir Sementsov-Ogievskiy
  0 siblings, 0 replies; 29+ messages in thread
From: Vladimir Sementsov-Ogievskiy @ 2026-09-09 14:35 UTC (permalink / raw)
  To: Denis V. Lunev, qemu-devel; +Cc: qemu-block, Stefan Hajnoczi

On 03.09.26 17:41, Denis V. Lunev wrote:
> From: Denis V. Lunev<den@openvz.org>
> 
> Use a different bitmap name for parallels images, as the format has its
> own ID format and can not hold an arbitrary string.
> 
> Replace the hardcoded 'qcow2' target format with iotests.imgfmt.
> 
> Add 'parallels' to the supported formats.
> 
> Based on the original work from Alexander Ivanov.
> 
> Cc: Stefan Hajnoczi<stefanha@redhat.com>
> Cc: Vladimir Sementsov-Ogievskiy<vsementsov@yandex-team.ru>
> Signed-off-by: Denis V. Lunev<den@openvz.org>

Reviewed-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>

-- 
Best regards,
Vladimir


^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH v7 19/25] parallels: report the stored dirty bitmaps in qemu-img info
  2026-09-04  8:49   ` Markus Armbruster
@ 2026-09-13 19:50     ` Denis V. Lunev
  0 siblings, 0 replies; 29+ messages in thread
From: Denis V. Lunev @ 2026-09-13 19:50 UTC (permalink / raw)
  To: Markus Armbruster, Denis V. Lunev
  Cc: qemu-devel, qemu-block, Stefan Hajnoczi, Eric Blake

(first, but not the least sorry for late reply. It was really
strange that I have come back here several times to write
it correctly. Hope this will answer to all remaining questions.)

On 9/4/26 10:49, Markus Armbruster wrote:
> "Denis V. Lunev" <den@openvz.org> writes:
>
>> From: Denis V. Lunev <den@openvz.org>
>>
>> Nothing tells which persistent bitmaps an image carries. qemu-img info
>> says nothing about them, and the only other way to see one is to export
>> the image over NBD and ask for a bitmap by name, which needs the name
>> beforehand.
>>
>> Add ImageInfoSpecificParallels with the bitmaps and their granularity,
>> in the same way as qcow2 reports the contents of its bitmap directory:
>>
>>     Format specific information:
>>         bitmaps:
>>             [0]:
>>                 name: b2c9e1a4-5d3f-4e8b-9a7c-6f0d1e2b3a45
>>                 granularity: 65536
>>
>> For running VM process the list is built on the fly from in-memory
>> list. This is the best we can do. FormatExtension could be dead in
>> the image while VMs are running as all bitmaps are cleared on
>> non-clean VM stop.
> What's "FormatExtension"?
Format Extension is the area through which dirty bitmap is accessed.
For now this is catalog of extensions with the only implemented
extension - dirty bitmap extension. This is specified in details in
docs/interop/parallels.rst, section "Format Extension".

>> A bitmap of an image which was not closed correctly is inconsistent
>> and can not be used. Report that as well, the way qcow2 reports its
>> in-use flag, so such a bitmap is not listed as a valid one.
> This is helpful, thanks!
>
>> The section is left empty for an image with no persistent dirty
>> bitmaps, and an empty section is not printed, so the human readable
>> output of "qemu-img info" and "info block" is unchanged.
> Perfectly clear now.
>
>> Cc: Stefan Hajnoczi <stefanha@redhat.com>
>> Cc: Eric Blake <eblake@redhat.com>
>> Cc: Markus Armbruster <armbru@redhat.com>
>> Signed-off-by: Denis V. Lunev <den@openvz.org>
>> ---
>>  block/parallels-ext.c                         | 26 +++++++++++
>>  block/parallels.c                             | 20 ++++++++
>>  block/parallels.h                             |  3 ++
>>  qapi/block-core.json                          | 46 ++++++++++++++++++-
>>  tests/qemu-iotests/tests/parallels-checks     |  6 +++
>>  tests/qemu-iotests/tests/parallels-checks.out | 19 ++++++++
>>  6 files changed, 118 insertions(+), 2 deletions(-)
>>
>> diff --git a/block/parallels-ext.c b/block/parallels-ext.c
>> index f687f2da7c..8b8035af91 100644
>> --- a/block/parallels-ext.c
>> +++ b/block/parallels-ext.c
>> @@ -736,3 +736,29 @@ parallels_co_remove_persistent_dirty_bitmap(BlockDriverState *bs,
>>  
>>      return ret;
>>  }
>> +
>> +void parallels_get_bitmap_info_list(BlockDriverState *bs,
>> +                                    ParallelsBitmapInfoList **info_list)
>> +{
>> +    BdrvDirtyBitmap *bitmap;
>> +    ParallelsBitmapInfoList **tail = info_list;
>> +
>> +    *info_list = NULL;
>> +
>> +    FOR_EACH_DIRTY_BITMAP(bs, bitmap) {
>> +        ParallelsBitmapInfo *info;
>> +
>> +        if (!bdrv_dirty_bitmap_get_persistence(bitmap)) {
>> +            continue;
>> +        }
>> +
>> +        info = g_new0(ParallelsBitmapInfo, 1);
>> +        info->name = g_strdup(bdrv_dirty_bitmap_name(bitmap));
>> +        info->granularity = bdrv_dirty_bitmap_granularity(bitmap);
>> +        if (bdrv_dirty_bitmap_inconsistent(bitmap)) {
>> +            info->has_inconsistent = true;
>> +            info->inconsistent = true;
>> +        }
>> +        QAPI_LIST_APPEND(tail, info);
>> +    }
>> +}
>> diff --git a/block/parallels.c b/block/parallels.c
>> index 2a5ceb8978..e7d65d0458 100644
>> --- a/block/parallels.c
>> +++ b/block/parallels.c
>> @@ -1624,6 +1624,25 @@ static bool parallels_is_support_dirty_bitmaps(BlockDriverState *bs)
>>      return 1;
>>  }
>>  
>> +static ImageInfoSpecific * GRAPH_RDLOCK
>> +parallels_get_specific_info(BlockDriverState *bs, Error **errp)
>> +{
>> +    ImageInfoSpecificParallels *parallels_info;
>> +    ImageInfoSpecific *spec_info;
>> +
>> +    parallels_info = g_new0(ImageInfoSpecificParallels, 1);
>> +    parallels_get_bitmap_info_list(bs, &parallels_info->bitmaps);
>> +    parallels_info->has_bitmaps = !!parallels_info->bitmaps;
>> +
>> +    spec_info = g_new(ImageInfoSpecific, 1);
>> +    *spec_info = (ImageInfoSpecific){
>> +        .type = IMAGE_INFO_SPECIFIC_KIND_PARALLELS,
>> +        .u.parallels.data = parallels_info,
>> +    };
>> +
>> +    return spec_info;
>> +}
>> +
>>  static BlockDriver bdrv_parallels = {
>>      .format_name                = "parallels",
>>      .instance_size              = sizeof(BDRVParallelsState),
>> @@ -1653,6 +1672,7 @@ static BlockDriver bdrv_parallels = {
>>                                    parallels_co_can_store_new_dirty_bitmap,
>>      .bdrv_co_remove_persistent_dirty_bitmap =
>>                                    parallels_co_remove_persistent_dirty_bitmap,
>> +    .bdrv_get_specific_info     = parallels_get_specific_info,
>>  };
>>  
>>  static void bdrv_parallels_init(void)
>> diff --git a/block/parallels.h b/block/parallels.h
>> index 27d8c3ac83..012f47320b 100644
>> --- a/block/parallels.h
>> +++ b/block/parallels.h
>> @@ -32,6 +32,7 @@
>>  #ifndef BLOCK_PARALLELS_H
>>  #define BLOCK_PARALLELS_H
>>  #include "qemu/coroutine.h"
>> +#include "qapi/qapi-types-block-core.h"
>>  
>>  #define HEADS_NUMBER 16
>>  #define SEC_IN_CYL 32
>> @@ -111,5 +112,7 @@ parallels_co_can_store_new_dirty_bitmap(BlockDriverState *bs, const char *name,
>>  int coroutine_fn GRAPH_RDLOCK
>>  parallels_co_remove_persistent_dirty_bitmap(BlockDriverState *bs,
>>                                              const char *name, Error **errp);
>> +void parallels_get_bitmap_info_list(BlockDriverState *bs,
>> +                                    ParallelsBitmapInfoList **info_list);
>>  
>>  #endif
>> diff --git a/qapi/block-core.json b/qapi/block-core.json
>> index 199efc1e00..7499aea641 100644
>> --- a/qapi/block-core.json
>> +++ b/qapi/block-core.json
>> @@ -188,6 +188,35 @@
>>        '*extent-size-hint': 'size'
>>    } }
>>  
>> +##
>> +# @ParallelsBitmapInfo:
>> +#
>> +# Parallels dirty bitmap information.
>> +#
>> +# @name: the name of the bitmap
>> +#
>> +# @granularity: granularity of the bitmap in bytes
>> +#
>> +# @inconsistent: true if the bitmap was improperly stored and cannot
>> +#     be used
> I'd recommend "if the bitmap is in an inconsistent state and".
Ok

>> +#
>> +# Since: 11.2
>> +##
>> +{ 'struct': 'ParallelsBitmapInfo',
>> +  'data': { 'name': 'str', 'granularity': 'uint32',
>> +            '*inconsistent': 'bool' } }
>> +
>> +##
>> +# @ImageInfoSpecificParallels:
>> +#
>> +# @bitmaps: A list of the persistent dirty bitmaps of the image,
>> +#     including the ones which are not written out yet
> In review of v6, I asked whether "not written out yet" relevant to a
> user / management application.  You explained what it means, but didn't
> actually answer my question.  I failed to point that out then, sorry.
> Can you answer it now?
No. Dropped in v8.

Right now the main pattern of usage of Parallels image driver
is working with existing images, which are still available in
some places. In theory people could start VM directly from these
drivers but this is would be very strange story even for me.

Thus the following patterns are to be supported as the first
priority:
* qemu-img check
* qemu-img convert
* qemu-img info
All these commands opens images by itself and handles things
by themselves. That is why QAPI change here is collateral. It
is not possible to show ImageSpecificInfo without this change.

Nothing here could observe the distinction.

> I also inquired about the relation to ImageInfoSpecificQCow2's @bitmaps,
> and you told me they're basically the same (correct me if I
> misunderstood).  I then replied that the documentation should be
> basically the same, and you agreed.  Did you forget?
This is my fault and I have missed this is the v7.

    # @bitmaps: A list of parallels bitmap details

without the "(since ...)": qcow2 needs it because @bitmaps arrived in
4.0 while ImageInfoSpecificQCow2 is Since: 1.7, and
ImageInfoSpecificParallels is itself Since: 11.2.

Also in the diff: the struct description lost its stray "dirty", and
ParallelsBitmapInfo moved next to Qcow2BitmapInfo, out of the
ImageInfoSpecificFOO run - the v6 ordering you asked for.

Thank you for your time,
    Den

P.S. Can you pls drop 2 words into thread for
   
 https://lore.kernel.org/qemu-devel/20260824133729.1141990-1-den@openvz.org/
     If you believe that proposed API change is no go, I'd go rewrite
     code with online 'check' command. This problem is real and the
     damage is very big, which is not just single cluster lost.


^ permalink raw reply	[flat|nested] 29+ messages in thread

end of thread, other threads:[~2026-09-13 19:51 UTC | newest]

Thread overview: 29+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-03 14:41 [PATCH v7 00/25] parallels: Add full dirty bitmap support Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 01/25] parallels: Set s->used_bmap to NULL in parallels_free_used_bitmap() Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 02/25] parallels: split inactivation out and add the activation counterpart Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 03/25] iotests: cover inactivating a read-only node Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 04/25] parallels: Make mark_used() a global function Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 05/25] parallels: Limit search in parallels_mark_used to the last marked cluster Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 06/25] parallels: Move host clusters allocation to a separate function Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 07/25] parallels: do not let the check die on what it is meant to report Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 08/25] parallels: Create used bitmap even if checks needed Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 09/25] parallels: Drop unused clusters at the end of the image Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 10/25] parallels: Remove unnecessary data_end field Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 11/25] parallels: Add dirty bitmaps saving Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 12/25] parallels: Let image extensions work in RW mode Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 13/25] parallels: Handle L1 entries equal to one Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 14/25] iotests: cover the Format Extension against the leak check Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 15/25] iotests: run the persistent dirty bitmap test on parallels Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 16/25] parallels: reject a bitmap L1 entry outside the data area Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 17/25] parallels: do not trust the bitmaps of an image which was not closed Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 18/25] parallels: implement removing a stored dirty bitmap Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 19/25] parallels: report the stored dirty bitmaps in qemu-img info Denis V. Lunev
2026-09-04  8:49   ` Markus Armbruster
2026-09-13 19:50     ` Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 20/25] iotests: rename parallels-read-bitmap to parallels-bitmap Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 21/25] iotests: cover storing a parallels dirty bitmap Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 22/25] iotests: cover the qemu-img bitmap operations on parallels Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 23/25] iotests: cover a broken Format Extension and a combined repair Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 24/25] tests: Turned on 256, 299, 304 and block-status-cache for parallels format Denis V. Lunev
2026-09-03 14:41 ` [PATCH v7 25/25] tests: Add parallels format support to image-fleecing Denis V. Lunev
2026-09-09 14:35   ` Vladimir Sementsov-Ogievskiy

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.