Files
cloud-hypervisor/block/src/disk_file.rs
CMGS b68e7f3d91 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>
2026-07-19 18:26:37 +00:00

182 lines
6.8 KiB
Rust

// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0
//! Composable disk capability traits for the block crate.
//!
//! Small traits define individual capabilities:
//!
//! - [`DiskSize`] - reported capacity (logical size)
//! - [`PhysicalSize`] - host allocation size
//! - [`DiskFd`] - backing file descriptor access
//! - [`Geometry`] - sector/cluster geometry (default 512B)
//! - [`SparseCapable`] - sparse and zero flag support
//! - [`Resizable`] - online resize
//! - [`MetadataSync`] - flush of format metadata cached in memory
//!
//! [`DiskFile`] is a supertrait that bundles the universal capabilities
//! (`DiskSize` + `Geometry`). [`FullDiskFile`] adds all optional
//! capabilities. [`AsyncDiskFile`] extends `DiskFile` with async I/O
//! construction for virtio queue workers. [`AsyncFullDiskFile`]
//! combines both axes.
//!
//! ```text
//! DiskFile: DiskSize + Geometry + Sync
//! / \
//! FullDiskFile: AsyncDiskFile:
//! DiskFile + PhysicalSize + DiskFile + Unpin
//! DiskFd + SparseCapable + try_clone, create_async_io
//! Resizable + MetadataSync
//! \ /
//! AsyncFullDiskFile: FullDiskFile + AsyncDiskFile
//! ```
//!
//! Readonly accessors take `&self`. Only [`Resizable::resize`] requires
//! `&mut self`. Errors are returned as [`BlockResult`].
use std::fmt::Debug;
use crate::async_io::{AsyncIo, BorrowedDiskFd};
use crate::{BlockResult, DiskTopology};
/// Reported capacity of a disk image.
pub trait DiskSize: Send + Debug {
/// Virtual size of the disk image in bytes (reported capacity).
fn logical_size(&self) -> BlockResult<u64>;
}
/// Host allocation size of a file-backed disk image.
pub trait PhysicalSize: Send + Debug {
/// Actual bytes occupied on the host filesystem.
fn physical_size(&self) -> BlockResult<u64>;
}
/// Backing file descriptor access for disk images backed by a file.
pub trait DiskFd: Send + Debug {
/// Borrows the underlying file descriptor.
fn fd(&self) -> BorrowedDiskFd<'_>;
}
/// Sector and cluster geometry of a disk image.
///
/// Default returns `DiskTopology::default()` (512B logical/physical).
pub trait Geometry: Send + Debug {
/// Returns the disk topology.
fn topology(&self) -> DiskTopology {
DiskTopology::default()
}
}
/// Sparse and zero flag support for thin provisioned disk images.
pub trait SparseCapable: Send + Debug {
/// Indicates support for sparse operations (punch hole, write zeroes, discard).
fn supports_sparse_operations(&self) -> bool {
false
}
/// Indicates support for a metadata level zero flag optimization in
/// virtio `VIRTIO_BLK_T_WRITE_ZEROES` requests. When true, the format
/// can mark regions as reading zeros via a metadata bit rather than
/// writing actual zero bytes to disk.
fn supports_zero_flag(&self) -> bool {
false
}
}
/// Live disk resize support.
///
/// Implementations may return an error if the backend does not
/// support resizing (e.g. fixed size formats).
pub trait Resizable: Send + Debug {
/// Resizes the disk image to the given size in bytes, if the backend supports it.
fn resize(&mut self, size: u64) -> BlockResult<()>;
}
/// Flush of format metadata cached in memory.
///
/// Default is a no-op for formats that keep no metadata cache
/// (e.g. raw, fixed vhd).
pub trait MetadataSync: Send + Debug {
/// Flushes format metadata cached in memory (e.g. qcow2 L2/refcount
/// tables) to the underlying file.
///
/// Called on device pause so that an externally copied or reopened
/// image is self-consistent without requiring a guest-initiated
/// flush.
fn sync_metadata(&self) -> BlockResult<()> {
Ok(())
}
}
/// Supertrait bundling universal disk capabilities.
///
/// Every disk format implements `DiskSize` and `Geometry`.
/// `Sync` is required so that `Arc<dyn DiskFile>` can be shared
/// across threads for concurrent readonly access.
pub trait DiskFile: DiskSize + Geometry + Sync {}
/// Full capability disk file trait.
///
/// Bundles all optional capabilities on top of [`DiskFile`]:
/// file descriptor access, physical size, sparse operations, resize,
/// and metadata sync. Used by consumers that need feature negotiation
/// without async I/O (e.g. vhost user block).
pub trait FullDiskFile:
DiskFile + PhysicalSize + DiskFd + SparseCapable + Resizable + MetadataSync
{
}
/// Blanket implementation: any type implementing all constituent traits
/// automatically satisfies [`FullDiskFile`].
impl<T: DiskFile + PhysicalSize + DiskFd + SparseCapable + Resizable + MetadataSync> FullDiskFile
for T
{
}
/// Extended disk file trait for virtio queue workers.
///
/// Adds cloning and async I/O construction on top of [`DiskFile`].
/// `Unpin` is required so trait objects can be moved freely.
pub trait AsyncDiskFile: DiskFile + Unpin {
/// Creates an independent handle for a queue worker.
///
/// The clone shares internally reference counted state (e.g.
/// `Arc<Metadata>`) with the original, but owns its own file
/// descriptor and I/O completion resources. Each virtio queue
/// gets one clone so that workers can operate in parallel
/// without contending on I/O state.
///
/// Returns `Box<dyn AsyncDiskFile>` (not `AsyncFullDiskFile`)
/// because clones only serve as data plane handles for queue
/// workers. The original remains the control plane for feature
/// negotiation and configuration.
fn try_clone(&self) -> BlockResult<Box<dyn AsyncDiskFile>>;
/// Constructs a per queue async I/O engine.
///
/// # Arguments
///
/// * `ring_depth` - maximum number of in flight I/O operations.
/// Callers typically pass the virtio queue size. Must be greater
/// than zero. Backends that do not use an async ring (e.g. sync
/// fallback implementations) may ignore this value.
fn create_async_io(&self, ring_depth: u32) -> BlockResult<Box<dyn AsyncIo>>;
}
/// Full capability async disk file trait.
///
/// Combines [`FullDiskFile`] (all optional capabilities) with
/// [`AsyncDiskFile`] (async I/O construction). This is the top level
/// trait for virtio block devices that need both feature negotiation
/// and async queue workers.
///
/// The type narrowing on [`AsyncDiskFile::try_clone`] is intentional:
/// clones only serve as data plane handles for queue workers, while
/// the original `AsyncFullDiskFile` handle remains the control plane
/// for feature negotiation and configuration.
pub trait AsyncFullDiskFile: FullDiskFile + AsyncDiskFile {}
/// Blanket implementation: any type implementing both [`FullDiskFile`]
/// and [`AsyncDiskFile`] automatically satisfies [`AsyncFullDiskFile`].
impl<T: FullDiskFile + AsyncDiskFile> AsyncFullDiskFile for T {}