Commit 3f3f5cdb92 for qemu.org
commit 3f3f5cdb929e8621699f92ee6e5018e1e6ea5bf2
Author: Denis V. Lunev <den@openvz.org>
Date: Thu Jul 16 13:22:41 2026 +0200
migration/block-dirty-bitmap: reject bitmap load onto ro node
dirty_bitmap_load_start() creates an incoming migrated bitmap with
bdrv_create_dirty_bitmap() and, if the source marked it persistent,
calls bdrv_dirty_bitmap_set_persistence() without checking whether
the destination node can be written to. Same gap as
qmp_block_dirty_bitmap_add(), reached via incoming migration: a
persistent bitmap for a read-only destination (e.g. a migrated
CD-ROM-class attachment with dirty-bitmaps migration enabled) ends
up writable in memory on a node that can never store it.
Reject it the same way, with one difference from the QMP path:
every destination node is BDRV_O_INACTIVE until migration completes,
so bdrv_is_writable() would reject every incoming persistent
bitmap, not just read-only ones. Check bdrv_is_read_only() alone.
Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Eric Blake <eblake@redhat.com>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
CC: John Snow <jsnow@redhat.com>
CC: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Message-ID: <20260716112242.3000035-3-den@openvz.org>
Reviewed-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
diff --git a/migration/block-dirty-bitmap.c b/migration/block-dirty-bitmap.c
index cba54e25cd..1b8f39c12b 100644
--- a/migration/block-dirty-bitmap.c
+++ b/migration/block-dirty-bitmap.c
@@ -812,13 +812,6 @@ static int dirty_bitmap_load_start(QEMUFile *f, DBMLoadState *s)
error_report("Bitmap with the same name ('%s') already exists on "
"destination", bdrv_dirty_bitmap_name(s->bitmap));
return -EINVAL;
- } else {
- s->bitmap = bdrv_create_dirty_bitmap(s->bs, granularity,
- s->bitmap_name, &local_err);
- if (!s->bitmap) {
- error_report_err(local_err);
- return -EINVAL;
- }
}
if (flags & DIRTY_BITMAP_MIG_START_FLAG_RESERVED_MASK) {
@@ -835,6 +828,21 @@ static int dirty_bitmap_load_start(QEMUFile *f, DBMLoadState *s)
persistent = flags & DIRTY_BITMAP_MIG_START_FLAG_PERSISTENT;
}
+ /* Not bdrv_is_writable(): nodes stay inactive until migration ends. */
+ if (persistent && bdrv_is_read_only(s->bs)) {
+ error_report("Cannot make migrated bitmap '%s' persistent "
+ "on read-only node '%s'", s->bitmap_name,
+ bdrv_get_node_name(s->bs));
+ return -EINVAL;
+ }
+
+ s->bitmap = bdrv_create_dirty_bitmap(s->bs, granularity,
+ s->bitmap_name, &local_err);
+ if (!s->bitmap) {
+ error_report_err(local_err);
+ return -EINVAL;
+ }
+
if (persistent) {
bdrv_dirty_bitmap_set_persistence(s->bitmap, true);
}
diff --git a/tests/qemu-iotests/tests/migrate-bitmaps-test b/tests/qemu-iotests/tests/migrate-bitmaps-test
index 8fb4099201..cb9154ca8d 100755
--- a/tests/qemu-iotests/tests/migrate-bitmaps-test
+++ b/tests/qemu-iotests/tests/migrate-bitmaps-test
@@ -206,6 +206,39 @@ class TestDirtyBitmapMigration(iotests.QMPTestCase):
self.vm_b.launch()
self.check_bitmap(self.vm_b, sha256 if persistent else False)
+ def test_migration_to_readonly_destination(self):
+ granularity = 512
+ mig_caps = [{'capability': 'events', 'state': True},
+ {'capability': 'dirty-bitmaps', 'state': True}]
+
+ self.vm_b.add_incoming("defer")
+ self.vm_b.add_drive(disk_b, 'read-only=on')
+
+ self.add_bitmap(self.vm_a, granularity, True)
+ self.vm_a.hmp_qemu_io('drive0', 'write 0 4096')
+
+ self.vm_a.cmd('migrate-set-capabilities', capabilities=mig_caps)
+ self.vm_a.cmd('migrate', uri=mig_cmd)
+ while True:
+ event = self.vm_a.event_wait('MIGRATION')
+ if event['data']['status'] == 'completed':
+ break
+ self.vm_a.shutdown()
+
+ self.vm_b.launch()
+ self.vm_b.cmd('migrate-set-capabilities', capabilities=mig_caps)
+ self.vm_b.cmd('migrate-incoming', uri=incoming_cmd)
+ while True:
+ event = self.vm_b.event_wait('MIGRATION')
+ if event['data']['status'] in ('completed', 'failed'):
+ break
+
+ self.assert_qmp(event, 'data/status', 'failed')
+
+ # A failed incoming load makes the destination process exit on
+ # its own; reap it so tearDown()'s shutdown() is a clean no-op.
+ self.vm_b.wait()
+
def inject_test_case(klass, suffix, method, *args, **kwargs):
mc = operator.methodcaller(method, *args, **kwargs)
diff --git a/tests/qemu-iotests/tests/migrate-bitmaps-test.out b/tests/qemu-iotests/tests/migrate-bitmaps-test.out
index cafb8161f7..73e375a9d7 100644
--- a/tests/qemu-iotests/tests/migrate-bitmaps-test.out
+++ b/tests/qemu-iotests/tests/migrate-bitmaps-test.out
@@ -1,5 +1,5 @@
-.....................................
+......................................
----------------------------------------------------------------------
-Ran 37 tests
+Ran 38 tests
OK