Commit 49edb98a81 for qemu.org

commit 49edb98a8114499df1f5957065ad99e5d3939f08
Author: Jean-Louis Dupond <jean-louis@dupond.be>
Date:   Wed Aug 26 12:10:05 2026 +0200

    qcow2: add functionality to repair bitmaps

    In some cases, it might happen that bitmaps become corrupt.
    For example when adding/removing bitmaps on a live image.

    Of course this should not happen, but in case this happens, the image is
    corrupt and even cannot be opened anymore. You'll get something like the
    following:
    qemu-img: Could not open 'disk.qcow2': Bitmap '' doesn't satisfy the constraints

    So the image becomes useless, and cannot be repaired. This while in fact
    only (one) bitmap entry is corrupt, and the rest of the data is just
    intact.

    This commit adds a way to fix this corruption, by just replacing the
    bitmap list in the qcow2 image with the valid bitmaps, and dropping the
    bitmaps that are corrupt.

    $ qemu-img check disk.qcow2
    qemu-img: Check failed: Invalid argument
    qemu-img: Lost persistent bitmaps during inactivation of node '#block147': Bitmap '' doesn't satisfy the constraints

    $ qemu-img check -r all disk.qcow2
    qcow2_free_clusters failed: Invalid argument
    Leaked cluster 3 refcount=1 reference=0
    Leaked cluster 26 refcount=1 reference=0
    ERROR cluster 983214 refcount=0 reference=1
    Rebuilding refcount structure
    Repairing cluster 1 refcount=1 reference=0
    Repairing cluster 2 refcount=1 reference=0
    Repairing cluster 32768 refcount=1 reference=0
    ....
    Repairing cluster 983214 refcount=1 reference=0
    The following inconsistencies were found and repaired:

        2 leaked clusters
        3 corruptions

    Double checking the fixed image now...
    No errors were found on the image.
    983056/1048576 = 93.75% allocated, 0.05% fragmented, 0.00% compressed clusters
    Image end offset: 64437878784

    And the image is valid again!
    Worst case you lose all bitmaps, but at least the image and the data
    itself is useable again.

    Signed-off-by: Jean-Louis Dupond <jean-louis@dupond.be>
    Message-ID: <20260826101005.2345883-2-jean-louis@dupond.be>
    Reviewed-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
    Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>

diff --git a/block/qcow2-bitmap.c b/block/qcow2-bitmap.c
index ac5a724588..853181da63 100644
--- a/block/qcow2-bitmap.c
+++ b/block/qcow2-bitmap.c
@@ -100,6 +100,9 @@ typedef enum BitmapType {
     BT_DIRTY_TRACKING_BITMAP = 1
 } BitmapType;

+static int GRAPH_RDLOCK
+update_ext_header_and_dir(BlockDriverState *bs, Qcow2BitmapList *bm_list);
+
 static inline bool can_write(BlockDriverState *bs)
 {
     return !bdrv_is_read_only(bs) && !(bdrv_get_flags(bs) & BDRV_O_INACTIVE);
@@ -655,12 +658,14 @@ fail:
 int coroutine_fn
 qcow2_check_bitmaps_refcounts(BlockDriverState *bs, BdrvCheckResult *res,
                               void **refcount_table,
-                              int64_t *refcount_table_size)
+                              int64_t *refcount_table_size,
+                              BdrvCheckMode fix)
 {
     int ret;
     BDRVQcow2State *s = bs->opaque;
     Qcow2BitmapList *bm_list;
-    Qcow2Bitmap *bm;
+    Qcow2Bitmap *bm, *bm_next;
+    int bm_list_fixed = 0;

     if (s->nb_bitmaps == 0) {
         return 0;
@@ -677,12 +682,21 @@ qcow2_check_bitmaps_refcounts(BlockDriverState *bs, BdrvCheckResult *res,
                                s->bitmap_directory_size, NULL);
     if (bm_list == NULL) {
         res->corruptions++;
+
+        if (fix & BDRV_FIX_ERRORS) {
+            ret = update_ext_header_and_dir(bs, NULL);
+            if (ret >= 0) {
+                res->corruptions_fixed++;
+            }
+            return ret;
+        }
         return -EINVAL;
     }

-    QSIMPLEQ_FOREACH(bm, bm_list, entry) {
+    QSIMPLEQ_FOREACH_SAFE(bm, bm_list, entry, bm_next) {
         uint64_t *bitmap_table = NULL;
         int i;
+        int nb_corrupted_entries = 0;

         ret = qcow2_inc_refcounts_imrt(bs, res,
                                        refcount_table, refcount_table_size,
@@ -695,6 +709,12 @@ qcow2_check_bitmaps_refcounts(BlockDriverState *bs, BdrvCheckResult *res,
         ret = bitmap_table_load(bs, &bm->table, &bitmap_table);
         if (ret < 0) {
             res->corruptions++;
+            if (fix & BDRV_FIX_ERRORS) {
+                QSIMPLEQ_REMOVE(bm_list, bm, Qcow2Bitmap, entry);
+                bitmap_free(bm);
+                bm_list_fixed++;
+                continue;
+            }
             goto out;
         }

@@ -704,6 +724,7 @@ qcow2_check_bitmaps_refcounts(BlockDriverState *bs, BdrvCheckResult *res,

             if (check_table_entry(entry, s->cluster_size) < 0) {
                 res->corruptions++;
+                nb_corrupted_entries++;
                 continue;
             }

@@ -720,9 +741,23 @@ qcow2_check_bitmaps_refcounts(BlockDriverState *bs, BdrvCheckResult *res,
             }
         }

+        if ((fix & BDRV_FIX_ERRORS) && nb_corrupted_entries > 0) {
+            QSIMPLEQ_REMOVE(bm_list, bm, Qcow2Bitmap, entry);
+            bitmap_free(bm);
+            bm_list_fixed += nb_corrupted_entries;
+        }
+
         g_free(bitmap_table);
     }

+    /* If fixing, update the bitmap directory with the repaired list */
+    if ((fix & BDRV_FIX_ERRORS) && bm_list_fixed > 0) {
+        ret = update_ext_header_and_dir(bs, bm_list);
+        if (ret >= 0) {
+            res->corruptions_fixed += bm_list_fixed;
+        }
+    }
+
 out:
     bitmap_list_free(bm_list);

diff --git a/block/qcow2-refcount.c b/block/qcow2-refcount.c
index 6512cda407..df7f6d2f23 100644
--- a/block/qcow2-refcount.c
+++ b/block/qcow2-refcount.c
@@ -2305,7 +2305,8 @@ calculate_refcounts(BlockDriverState *bs, BdrvCheckResult *res,
     }

     /* bitmaps */
-    ret = qcow2_check_bitmaps_refcounts(bs, res, refcount_table, nb_clusters);
+    ret = qcow2_check_bitmaps_refcounts(bs, res, refcount_table,
+                                        nb_clusters, fix);
     if (ret < 0) {
         return ret;
     }
diff --git a/block/qcow2.c b/block/qcow2.c
index 1543255eba..0358bc04f3 100644
--- a/block/qcow2.c
+++ b/block/qcow2.c
@@ -1927,11 +1927,19 @@ qcow2_do_open(BlockDriverState *bs, QDict *options, int flags,
     if (!(bdrv_get_flags(bs) & BDRV_O_INACTIVE)) {
         /* It's case 1, 2 or 3.2. Or 3.1 which is BUG in management layer. */
         bool header_updated;
-        if (!qcow2_load_dirty_bitmaps(bs, &header_updated, errp)) {
-            ret = -EINVAL;
-            goto fail;
+        Error *local_err = NULL;
+        if (!qcow2_load_dirty_bitmaps(bs, &header_updated, &local_err)) {
+            /*
+             * Allow this to fail in check mode
+             * because otherwise we can't open the image at all.
+             */
+            if (!(flags & BDRV_O_CHECK)) {
+                ret = -EINVAL;
+                error_propagate(errp, local_err);
+                goto fail;
+            }
+            error_reportf_err(local_err, "Could not load dirty bitmaps: ");
         }
-
         update_header = update_header && !header_updated;
     }

diff --git a/block/qcow2.h b/block/qcow2.h
index ce517040c4..ba9fe68130 100644
--- a/block/qcow2.h
+++ b/block/qcow2.h
@@ -1038,7 +1038,8 @@ void qcow2_cache_discard(Qcow2Cache *c, void *table);
 int coroutine_fn GRAPH_RDLOCK
 qcow2_check_bitmaps_refcounts(BlockDriverState *bs, BdrvCheckResult *res,
                               void **refcount_table,
-                              int64_t *refcount_table_size);
+                              int64_t *refcount_table_size,
+                              BdrvCheckMode fix);

 bool coroutine_fn GRAPH_RDLOCK
 qcow2_load_dirty_bitmaps(BlockDriverState *bs, bool *header_updated,