@samitouri / QOSamiQemu / commits / 3f3f5cdb92

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>

Denis V. Lunev committed Jul 16, 2026 at 13:22 UTC 3f3f5cdb929e8621699f92ee6e5018e1e6ea5bf2
3 files changed +50 -9
migration/block-dirty-bitmap.c
+15 -7
@@ -812,13 +812,6 @@ static int dirty_bitmap_load_start(QEMUFile *f, DBMLoadState *s)
812 error_report("Bitmap with the same name ('%s') already exists on "
813 "destination", bdrv_dirty_bitmap_name(s->bitmap));
814 return -EINVAL;
815 - } else {
816 - s->bitmap = bdrv_create_dirty_bitmap(s->bs, granularity,
817 - s->bitmap_name, &local_err);
818 - if (!s->bitmap) {
819 - error_report_err(local_err);
820 - return -EINVAL;
821 - }
815 }
816
817 if (flags & DIRTY_BITMAP_MIG_START_FLAG_RESERVED_MASK) {
@@ -835,6 +828,21 @@ static int dirty_bitmap_load_start(QEMUFile *f, DBMLoadState *s)
828 persistent = flags & DIRTY_BITMAP_MIG_START_FLAG_PERSISTENT;
829 }
830
831 + /* Not bdrv_is_writable(): nodes stay inactive until migration ends. */
832 + if (persistent && bdrv_is_read_only(s->bs)) {
833 + error_report("Cannot make migrated bitmap '%s' persistent "
834 + "on read-only node '%s'", s->bitmap_name,
835 + bdrv_get_node_name(s->bs));
836 + return -EINVAL;
837 + }
838 +
839 + s->bitmap = bdrv_create_dirty_bitmap(s->bs, granularity,
840 + s->bitmap_name, &local_err);
841 + if (!s->bitmap) {
842 + error_report_err(local_err);
843 + return -EINVAL;
844 + }
845 +
846 if (persistent) {
847 bdrv_dirty_bitmap_set_persistence(s->bitmap, true);
848 }
tests/qemu-iotests/tests/migrate-bitmaps-test
+33
@@ -206,6 +206,39 @@ class TestDirtyBitmapMigration(iotests.QMPTestCase):
206 self.vm_b.launch()
207 self.check_bitmap(self.vm_b, sha256 if persistent else False)
208
209 + def test_migration_to_readonly_destination(self):
210 + granularity = 512
211 + mig_caps = [{'capability': 'events', 'state': True},
212 + {'capability': 'dirty-bitmaps', 'state': True}]
213 +
214 + self.vm_b.add_incoming("defer")
215 + self.vm_b.add_drive(disk_b, 'read-only=on')
216 +
217 + self.add_bitmap(self.vm_a, granularity, True)
218 + self.vm_a.hmp_qemu_io('drive0', 'write 0 4096')
219 +
220 + self.vm_a.cmd('migrate-set-capabilities', capabilities=mig_caps)
221 + self.vm_a.cmd('migrate', uri=mig_cmd)
222 + while True:
223 + event = self.vm_a.event_wait('MIGRATION')
224 + if event['data']['status'] == 'completed':
225 + break
226 + self.vm_a.shutdown()
227 +
228 + self.vm_b.launch()
229 + self.vm_b.cmd('migrate-set-capabilities', capabilities=mig_caps)
230 + self.vm_b.cmd('migrate-incoming', uri=incoming_cmd)
231 + while True:
232 + event = self.vm_b.event_wait('MIGRATION')
233 + if event['data']['status'] in ('completed', 'failed'):
234 + break
235 +
236 + self.assert_qmp(event, 'data/status', 'failed')
237 +
238 + # A failed incoming load makes the destination process exit on
239 + # its own; reap it so tearDown()'s shutdown() is a clean no-op.
240 + self.vm_b.wait()
241 +
242
243 def inject_test_case(klass, suffix, method, *args, **kwargs):
244 mc = operator.methodcaller(method, *args, **kwargs)
tests/qemu-iotests/tests/migrate-bitmaps-test.out
+2 -2
@@ -1,5 +1,5 @@
1 -.....................................
1 +......................................
2 ----------------------------------------------------------------------
3 -Ran 37 tests
3 +Ran 38 tests
4
5 OK