block: flush cached qcow2 metadata on device pause

The qcow2 backend caches L2 table and refcount updates in memory and
only writes them back on a guest-initiated flush, clean shutdown or
drop. A paused VM therefore leaves the on-disk image without the
mappings for any cluster allocated since the last guest flush: the
data clusters are present in the file, but nothing references them.

Anything that reads the image while the VM is paused sees those
writes as missing. Copying the disk alongside vm.snapshot (the
documented snapshot workflow) captures a stale image, and live
migration releases the disk locks after pausing so the destination
reopens the file with the same stale metadata. In both cases writes
the guest has completed, and may later read back, silently disappear.

Add a MetadataSync capability trait with a no-op default, fold it
into FullDiskFile, implement it for the qcow2 backend as a metadata
cache flush, and call it from the virtio-block pause path after
in-flight requests have drained. Pause is the quiesce point both
flows rely on, and it is a cold path, so the extra flush does not
affect runtime I/O.

Reproduced by writing to a qcow2 disk from the guest with O_DIRECT
and no explicit flush, pausing the VM and copying the image: qemu-img
map on the copy shows no mapped clusters and reads return zeros. With
this change the copy contains every completed write. A unit test
covers the same sequence at the format level: a completed write is
invisible to a fresh reader until sync_metadata, and visible after.

Signed-off-by: CMGS <ilskdw@gmail.com>
This commit is contained in:
CMGS
2026-07-19 14:45:55 +08:00
committed by Rob Bradford
parent 320c96a9e2
commit b68e7f3d91
8 changed files with 96 additions and 7 deletions

View File

@@ -1327,6 +1327,14 @@ impl Pausable for Block {
let result = self
.wait_for_active_requests()
.map_err(MigratableError::Pause)
.and_then(|()| {
// Flush cached format metadata (e.g. qcow2 L2/refcount tables) so
// the on-disk image is self-consistent while paused: snapshot
// copies and migration disk-lock handoff read the file directly.
self.disk_image.sync_metadata().map_err(|e| {
MigratableError::Pause(anyhow::Error::new(e).context("sync disk metadata"))
})
})
.and_then(|()| self.common.pause());
self.draining_active_requests.store(false, Ordering::SeqCst);