All of lore.kernel.org
 help / color / mirror / Atom feed
From: "Denis V. Lunev" <den@openvz.org>
To: qemu-block@nongnu.org
Cc: qemu-devel@nongnu.org, "Denis V. Lunev" <den@openvz.org>,
	Stefan Hajnoczi <stefanha@redhat.com>
Subject: [PULL 18/29] parallels: Add dirty bitmaps saving
Date: Fri, 11 Sep 2026 01:42:11 +0200	[thread overview]
Message-ID: <20260910234222.3039975-19-den@openvz.org> (raw)
In-Reply-To: <20260910234222.3039975-1-den@openvz.org>

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



  parent reply	other threads:[~2026-09-11  1:00 UTC|newest]

Thread overview: 31+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-10 23:41 [PULL 00/29] parallels: persistent dirty bitmaps and Format Extension hardening Denis V. Lunev
2026-09-10 23:41 ` [PULL 01/29] parallels: fix out-of-bounds read in format extension parsing Denis V. Lunev
2026-09-10 23:41 ` [PULL 02/29] parallels: validate dirty bitmap granularity Denis V. Lunev
2026-09-10 23:41 ` [PULL 03/29] parallels: bound the bitmap L1 table against the bitmap size Denis V. Lunev
2026-09-10 23:41 ` [PULL 04/29] parallels: reject a Format Extension outside the image file Denis V. Lunev
2026-09-10 23:41 ` [PULL 05/29] parallels: allocate the Format Extension cluster gracefully Denis V. Lunev
2026-09-10 23:41 ` [PULL 06/29] parallels: fix GSList leak on the format extension success path Denis V. Lunev
2026-09-10 23:42 ` [PULL 07/29] iotests: cover the Parallels format extension parser Denis V. Lunev
2026-09-10 23:42 ` [PULL 08/29] parallels: Set s->used_bmap to NULL in parallels_free_used_bitmap() Denis V. Lunev
2026-09-10 23:42 ` [PULL 09/29] parallels: split inactivation out and add the activation counterpart Denis V. Lunev
2026-09-10 23:42 ` [PULL 10/29] iotests: cover inactivating a read-only node Denis V. Lunev
2026-09-10 23:42 ` [PULL 11/29] parallels: Make mark_used() a global function Denis V. Lunev
2026-09-10 23:42 ` [PULL 12/29] parallels: Limit search in parallels_mark_used to the last marked cluster Denis V. Lunev
2026-09-10 23:42 ` [PULL 13/29] parallels: Move host clusters allocation to a separate function Denis V. Lunev
2026-09-10 23:42 ` [PULL 14/29] parallels: do not let the check die on what it is meant to report Denis V. Lunev
2026-09-10 23:42 ` [PULL 15/29] parallels: Create used bitmap even if checks needed Denis V. Lunev
2026-09-10 23:42 ` [PULL 16/29] parallels: Drop unused clusters at the end of the image Denis V. Lunev
2026-09-10 23:42 ` [PULL 17/29] parallels: Remove unnecessary data_end field Denis V. Lunev
2026-09-10 23:42 ` Denis V. Lunev [this message]
2026-09-10 23:42 ` [PULL 19/29] parallels: Let image extensions work in RW mode Denis V. Lunev
2026-09-10 23:42 ` [PULL 20/29] parallels: Handle L1 entries equal to one Denis V. Lunev
2026-09-10 23:42 ` [PULL 21/29] iotests: cover the Format Extension against the leak check Denis V. Lunev
2026-09-10 23:42 ` [PULL 22/29] iotests: run the persistent dirty bitmap test on parallels Denis V. Lunev
2026-09-10 23:42 ` [PULL 23/29] parallels: reject a bitmap L1 entry outside the data area Denis V. Lunev
2026-09-10 23:42 ` [PULL 24/29] parallels: do not trust the bitmaps of an image which was not closed Denis V. Lunev
2026-09-10 23:42 ` [PULL 25/29] parallels: implement removing a stored dirty bitmap Denis V. Lunev
2026-09-10 23:42 ` [PULL 26/29] iotests: rename parallels-read-bitmap to parallels-bitmap Denis V. Lunev
2026-09-10 23:42 ` [PULL 27/29] iotests: cover a broken Format Extension and a combined repair Denis V. Lunev
2026-09-10 23:42 ` [PULL 28/29] tests: Turned on 256, 299, 304 and block-status-cache for parallels format Denis V. Lunev
2026-09-10 23:42 ` [PULL 29/29] tests: Add parallels format support to image-fleecing Denis V. Lunev
2026-09-11 10:51 ` [PULL 00/29] parallels: persistent dirty bitmaps and Format Extension hardening Richard Henderson

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260910234222.3039975-19-den@openvz.org \
    --to=den@openvz.org \
    --cc=qemu-block@nongnu.org \
    --cc=qemu-devel@nongnu.org \
    --cc=stefanha@redhat.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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.