block: qcow: Thread sparse configuration to QCOW2 constructors

Add sparse parameter to QcowFile constructors and propagate it from
device_manager through QcowDiskSync. This makes the sparse configuration
available throughout the QCOW2 implementation for controlling allocation
and deallocation behavior.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
This commit is contained in:
Anatol Belski 2026-02-01 23:35:03 +01:00 committed by Rob Bradford
parent 46e6ecddfe
commit 0a287793df
3 changed files with 54 additions and 37 deletions

View file

@ -35,7 +35,7 @@ use crate::qcow::refcount::RefCount;
use crate::qcow::vec_cache::{CacheMap, Cacheable, VecCache};
/// Nesting depth limit for disk formats that can open other disk files.
const MAX_NESTING_DEPTH: u32 = 10;
pub(super) const MAX_NESTING_DEPTH: u32 = 10;
#[sorted]
#[derive(Debug, Error)]
@ -788,6 +788,7 @@ impl BackingFile {
backing_file_config: Option<&BackingFileConfig>,
direct_io: bool,
max_nesting_depth: u32,
sparse: bool,
) -> Result<Option<Self>> {
let Some(config) = backing_file_config else {
return Ok(None);
@ -821,7 +822,7 @@ impl BackingFile {
}
ImageType::Qcow2 => {
let backing_qcow =
QcowFile::from_with_nesting_depth(raw_file, max_nesting_depth - 1)
QcowFile::from_with_nesting_depth(raw_file, max_nesting_depth - 1, sparse)
.map_err(|e| Error::BackingFileOpen(Box::new(e)))?;
let size = backing_qcow.virtual_size();
(Box::new(backing_qcow), size)
@ -902,6 +903,8 @@ pub struct QcowFile {
// removal of references to them have been synced to disk.
avail_clusters: Vec<u64>,
backing_file: Option<BackingFile>,
#[allow(dead_code)] // Used in later commits for sparse-aware deallocation
sparse: bool,
}
impl QcowFile {
@ -909,12 +912,16 @@ impl QcowFile {
///
/// Additionally, max nesting depth of this qcow2 image will be set to default value 10.
pub fn from(file: RawFile) -> Result<QcowFile> {
Self::from_with_nesting_depth(file, MAX_NESTING_DEPTH)
Self::from_with_nesting_depth(file, MAX_NESTING_DEPTH, true)
}
/// Creates a QcowFile from `file` and with a max nesting depth. File must be a valid qcow2
/// image.
pub fn from_with_nesting_depth(mut file: RawFile, max_nesting_depth: u32) -> Result<QcowFile> {
pub fn from_with_nesting_depth(
mut file: RawFile,
max_nesting_depth: u32,
sparse: bool,
) -> Result<QcowFile> {
let header = QcowHeader::new(&mut file)?;
// Only v2 and v3 files are supported.
@ -940,8 +947,12 @@ impl QcowFile {
let direct_io = file.is_direct();
let backing_file =
BackingFile::new(header.backing_file.as_ref(), direct_io, max_nesting_depth)?;
let backing_file = BackingFile::new(
header.backing_file.as_ref(),
direct_io,
max_nesting_depth,
sparse,
)?;
// Validate refcount order to be 0..6
let refcount_bits: u64 = 0x01u64
@ -1064,6 +1075,7 @@ impl QcowFile {
unref_clusters: Vec::new(),
avail_clusters: Vec::new(),
backing_file,
sparse,
};
// Check that the L1 and refcount tables fit in a 64bit address space.
@ -1093,9 +1105,9 @@ impl QcowFile {
}
/// Creates a new QcowFile at the given path.
pub fn new(file: RawFile, version: u32, virtual_size: u64) -> Result<QcowFile> {
pub fn new(file: RawFile, version: u32, virtual_size: u64, sparse: bool) -> Result<QcowFile> {
let header = QcowHeader::create_for_size_and_path(version, virtual_size, None)?;
QcowFile::new_from_header(file, &header)
QcowFile::new_from_header(file, &header, sparse)
}
/// Creates a new QcowFile at the given path with a backing file.
@ -1104,6 +1116,7 @@ impl QcowFile {
version: u32,
backing_file_size: u64,
backing_config: &BackingFileConfig,
sparse: bool,
) -> Result<QcowFile> {
let mut header = QcowHeader::create_for_size_and_path(
version,
@ -1113,15 +1126,15 @@ impl QcowFile {
if let Some(backing_file) = &mut header.backing_file {
backing_file.format = backing_config.format;
}
QcowFile::new_from_header(file, &header)
QcowFile::new_from_header(file, &header, sparse)
// backing_file is loaded by new_from_header -> Self::from() based on the header
}
fn new_from_header(mut file: RawFile, header: &QcowHeader) -> Result<QcowFile> {
fn new_from_header(mut file: RawFile, header: &QcowHeader, sparse: bool) -> Result<QcowFile> {
file.rewind().map_err(Error::SeekingFile)?;
header.write_to(&mut file)?;
let mut qcow = Self::from(file)?;
let mut qcow = Self::from_with_nesting_depth(file, MAX_NESTING_DEPTH, sparse)?;
// Set the refcount for each refcount table cluster.
let cluster_size = 0x01u64 << qcow.header.cluster_bits;
@ -2597,7 +2610,7 @@ where
match dst_type {
ImageType::Qcow2 => {
let mut dst_writer = QcowFile::new(dst_file, 3, src_size)?;
let mut dst_writer = QcowFile::new(dst_file, 3, src_size, true)?;
convert_reader_writer(reader, &mut dst_writer, src_size)
}
ImageType::Raw => {
@ -2625,7 +2638,7 @@ pub fn convert(
match src_type {
ImageType::Qcow2 => {
let mut src_reader =
QcowFile::from_with_nesting_depth(src_file, src_max_nesting_depth)?;
QcowFile::from_with_nesting_depth(src_file, src_max_nesting_depth, true)?;
convert_reader(&mut src_reader, dst_file, dst_type)
}
ImageType::Raw => {
@ -2747,7 +2760,7 @@ mod unit_tests {
F: FnMut(QcowFile),
{
let tmp: RawFile = RawFile::new(TempFile::new().unwrap().into_file(), direct);
let qcow_file = QcowFile::new(tmp, 3, file_size).unwrap();
let qcow_file = QcowFile::new(tmp, 3, file_size, true).unwrap();
testfn(qcow_file); // File closed when the function exits.
}
@ -2987,7 +3000,7 @@ mod unit_tests {
.expect("Failed to write header to shm.");
disk_file.rewind().unwrap();
// The maximum nesting depth is 0, which means backing file is not allowed.
QcowFile::from_with_nesting_depth(disk_file, 0).unwrap();
QcowFile::from_with_nesting_depth(disk_file, 0, true).unwrap();
}
#[test]
@ -3002,7 +3015,7 @@ mod unit_tests {
.expect("Failed to write header to shm.");
disk_file.rewind().unwrap();
// The maximum nesting depth is 0, which means backing file is not allowed.
let res = QcowFile::from_with_nesting_depth(disk_file, 0);
let res = QcowFile::from_with_nesting_depth(disk_file, 0, true);
assert!(matches!(res.unwrap_err(), Error::MaxNestingDepthExceeded));
}
@ -3033,6 +3046,7 @@ mod unit_tests {
false,
),
MAX_NESTING_DEPTH,
true,
)
.expect_err("Opening qcow file with itself as backing file should fail.");
@ -3450,7 +3464,7 @@ mod unit_tests {
let new_size = 600 * 1024 * 1024; // 600 MB
let tmp: RawFile = RawFile::new(TempFile::new().unwrap().into_file(), false);
let mut q = QcowFile::new(tmp, 3, initial_size).unwrap();
let mut q = QcowFile::new(tmp, 3, initial_size, true).unwrap();
let original_l1_size = q.header().l1_size;
assert_eq!(q.virtual_size(), initial_size);
@ -3503,7 +3517,7 @@ mod unit_tests {
{
let backing_raw = RawFile::new(backing_temp.as_file().try_clone().unwrap(), false);
let _backing_qcow = QcowFile::new(backing_raw, 3, backing_size).unwrap();
let _backing_qcow = QcowFile::new(backing_raw, 3, backing_size, true).unwrap();
}
let overlay_file = TempFile::new().unwrap();
@ -3513,7 +3527,8 @@ mod unit_tests {
format: Some(ImageType::Qcow2),
};
let mut overlay =
QcowFile::new_from_backing(overlay_raw, 3, backing_size, &backing_config).unwrap();
QcowFile::new_from_backing(overlay_raw, 3, backing_size, &backing_config, true)
.unwrap();
assert_eq!(overlay.virtual_size(), backing_size);
@ -3532,7 +3547,7 @@ mod unit_tests {
{
let backing_raw = RawFile::new(backing_temp.as_file().try_clone().unwrap(), false);
let mut backing_qcow = QcowFile::new(backing_raw, 3, backing_size).unwrap();
let mut backing_qcow = QcowFile::new(backing_raw, 3, backing_size, true).unwrap();
let data = b"BACKING_DATA";
backing_qcow.rewind().unwrap();
backing_qcow.write_all(data).unwrap();
@ -3552,7 +3567,8 @@ mod unit_tests {
};
let overlay_size = backing_size * 2; // 2x the backing size
let mut overlay =
QcowFile::new_from_backing(overlay_raw, 3, overlay_size, &backing_config).unwrap();
QcowFile::new_from_backing(overlay_raw, 3, overlay_size, &backing_config, true)
.unwrap();
assert_eq!(overlay.virtual_size(), overlay_size);

View file

@ -13,7 +13,7 @@ use vmm_sys_util::eventfd::EventFd;
use crate::async_io::{
AsyncIo, AsyncIoError, AsyncIoResult, BorrowedDiskFd, DiskFile, DiskFileError, DiskFileResult,
};
use crate::qcow::{QcowFile, RawFile, Result as QcowResult};
use crate::qcow::{MAX_NESTING_DEPTH, QcowFile, RawFile, Result as QcowResult};
use crate::{AsyncAdaptor, BlockBackend};
pub struct QcowDiskSync {
@ -29,19 +29,15 @@ pub struct QcowDiskSync {
}
impl QcowDiskSync {
pub fn new(file: File, direct_io: bool, backing_files: bool) -> QcowResult<Self> {
if backing_files {
Ok(QcowDiskSync {
qcow_file: Arc::new(Mutex::new(QcowFile::from(RawFile::new(file, direct_io))?)),
})
} else {
Ok(QcowDiskSync {
qcow_file: Arc::new(Mutex::new(QcowFile::from_with_nesting_depth(
RawFile::new(file, direct_io),
0,
)?)),
})
}
pub fn new(file: File, direct_io: bool, backing_files: bool, sparse: bool) -> QcowResult<Self> {
let max_nesting_depth = if backing_files { MAX_NESTING_DEPTH } else { 0 };
Ok(QcowDiskSync {
qcow_file: Arc::new(Mutex::new(QcowFile::from_with_nesting_depth(
RawFile::new(file, direct_io),
max_nesting_depth,
sparse,
)?)),
})
}
}

View file

@ -2724,8 +2724,13 @@ impl DeviceManager {
ImageType::Qcow2 => {
info!("Using synchronous QCOW2 disk file");
Box::new(
QcowDiskSync::new(file, disk_cfg.direct, disk_cfg.backing_files)
.map_err(DeviceManagerError::CreateQcowDiskSync)?,
QcowDiskSync::new(
file,
disk_cfg.direct,
disk_cfg.backing_files,
disk_cfg.sparse,
)
.map_err(DeviceManagerError::CreateQcowDiskSync)?,
) as Box<dyn DiskFile>
}
ImageType::Vhdx => {