mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
Cover the boundary positive case where backing_file_offset plus backing_file_size equals the cluster size, which the spec allows and the new bound check must accept. Assisted-by: Claude:Opus-4.7 Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
4904 lines
182 KiB
Rust
4904 lines
182 KiB
Rust
// Copyright 2018 The Chromium OS Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE-BSD-3-Clause file.
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
|
|
|
pub(crate) mod backing;
|
|
pub(crate) mod decoder;
|
|
mod header;
|
|
pub(crate) mod metadata;
|
|
pub(crate) mod qcow_raw_file;
|
|
mod raw_file;
|
|
mod refcount;
|
|
mod util;
|
|
mod vec_cache;
|
|
|
|
use std::cmp::{max, min};
|
|
use std::fmt::{Debug, Formatter, Result as FmtResult};
|
|
use std::fs::{OpenOptions, read_link};
|
|
use std::io::{self, Read, Seek, SeekFrom, Write};
|
|
use std::mem::size_of;
|
|
use std::os::fd::{AsRawFd, RawFd};
|
|
use std::path::Path;
|
|
use std::str;
|
|
|
|
#[cfg(test)]
|
|
use header::{
|
|
AUTOCLEAR_FEATURES_OFFSET, DEFAULT_REFCOUNT_ORDER, HEADER_EXT_BACKING_FORMAT, HEADER_EXT_END,
|
|
V2_BARE_HEADER_SIZE, V3_BARE_HEADER_SIZE,
|
|
};
|
|
pub use header::{
|
|
BackingFileConfig, CompressionType, ImageType, IncompatFeatures, MissingFeatureError,
|
|
QcowHeader,
|
|
};
|
|
use header::{
|
|
COMPATIBLE_FEATURES_LAZY_REFCOUNTS, MAX_CLUSTER_BITS, MAX_QCOW_FILE_SIZE,
|
|
MAX_RAM_POINTER_TABLE_SIZE, MIN_CLUSTER_BITS, QCOW_MAGIC, max_refcount_clusters,
|
|
offset_is_cluster_boundary,
|
|
};
|
|
use libc::{EINVAL, EIO, ENOSPC};
|
|
use log::{error, warn};
|
|
use metadata::ClusterReadMapping;
|
|
use qcow_raw_file::{BeUint, QcowRawFile};
|
|
pub use raw_file::RawFile;
|
|
use refcount::RefCount;
|
|
use remain::sorted;
|
|
use thiserror::Error;
|
|
pub(crate) use util::MAX_NESTING_DEPTH;
|
|
use util::{
|
|
L1_TABLE_OFFSET_MASK, L2_TABLE_OFFSET_MASK, div_round_up_u32, div_round_up_u64, l1_entry_make,
|
|
l2_entry_compressed_cluster_layout, l2_entry_is_compressed, l2_entry_is_empty,
|
|
l2_entry_is_zero, l2_entry_make_std, l2_entry_make_zero, l2_entry_make_zero_plain,
|
|
l2_entry_std_cluster_addr,
|
|
};
|
|
use vec_cache::{CacheMap, Cacheable, VecCache};
|
|
use vmm_sys_util::file_traits::{FileSetLen, FileSync};
|
|
use vmm_sys_util::seek_hole::SeekHole;
|
|
use vmm_sys_util::write_zeroes::{PunchHole, WriteZeroesAt};
|
|
|
|
use super::common::decompress_cluster;
|
|
use crate::BlockBackend;
|
|
use crate::error::{BlockError, BlockErrorKind, BlockResult};
|
|
|
|
#[sorted]
|
|
#[derive(Debug, Error)]
|
|
pub enum Error {
|
|
#[error("Backing file I/O error: {0}")]
|
|
BackingFileIo(String /* path */, #[source] io::Error),
|
|
#[error("Backing file offset {0:#x} with zero size")]
|
|
BackingFileOffsetWithoutSize(u64),
|
|
#[error("Backing file open error: {0}")]
|
|
BackingFileOpen(String /* path */, #[source] Box<Error>),
|
|
#[error(
|
|
"Backing file name at offset {0:#x} length {1:#x} lies outside first cluster of {2:#x}"
|
|
)]
|
|
BackingFileOutsideFirstCluster(u64, u32, u64),
|
|
#[error("Backing file name at offset {0:#x} length {1:#x} overlaps header of size {2:#x}")]
|
|
BackingFileOverlapsHeader(u64, u32, u32),
|
|
#[error("Backing file size {0:#x} with zero offset")]
|
|
BackingFileSizeWithoutOffset(u32),
|
|
#[error("Backing file support is disabled")]
|
|
BackingFilesDisabled,
|
|
#[error("Backing file name is too long: {0} bytes over")]
|
|
BackingFileTooLong(usize),
|
|
#[error("Image is marked corrupt and cannot be opened for writing")]
|
|
CorruptImage,
|
|
#[error("Failed to evict cache")]
|
|
EvictingCache(#[source] io::Error),
|
|
#[error("File larger than max of {MAX_QCOW_FILE_SIZE}: {0}")]
|
|
FileTooBig(u64),
|
|
#[error("Failed to get file size")]
|
|
GettingFileSize(#[source] io::Error),
|
|
#[error("Failed to get refcount")]
|
|
GettingRefcount(#[source] refcount::Error),
|
|
#[error("Failed to parse filename")]
|
|
InvalidBackingFileName(#[source] str::Utf8Error),
|
|
#[error("Invalid cluster index")]
|
|
InvalidClusterIndex,
|
|
#[error("Invalid cluster size")]
|
|
InvalidClusterSize,
|
|
#[error("Invalid index")]
|
|
InvalidIndex,
|
|
#[error("Invalid L1 table offset")]
|
|
InvalidL1TableOffset,
|
|
#[error("Invalid L1 table size: {0}")]
|
|
InvalidL1TableSize(u32),
|
|
#[error("Invalid magic")]
|
|
InvalidMagic,
|
|
#[error("Invalid offset: {0}")]
|
|
InvalidOffset(u64),
|
|
#[error("Invalid refcount table offset")]
|
|
InvalidRefcountTableOffset,
|
|
#[error("Invalid refcount table size: {0}")]
|
|
InvalidRefcountTableSize(u64),
|
|
#[error("Maximum disk nesting depth exceeded")]
|
|
MaxNestingDepthExceeded,
|
|
#[error("No free clusters")]
|
|
NoFreeClusters,
|
|
#[error("No refcount clusters")]
|
|
NoRefcountClusters,
|
|
#[error("Not enough space for refcounts")]
|
|
NotEnoughSpaceForRefcounts,
|
|
#[error("Failed to open file {0}")]
|
|
OpeningFile(#[source] io::Error),
|
|
#[error("Failed to read data")]
|
|
ReadingData(#[source] io::Error),
|
|
#[error("Failed to read header")]
|
|
ReadingHeader(#[source] io::Error),
|
|
#[error("Failed to read pointers")]
|
|
ReadingPointers(#[source] io::Error),
|
|
#[error("Failed to read ref count block")]
|
|
ReadingRefCountBlock(#[source] refcount::Error),
|
|
#[error("Failed to read ref counts")]
|
|
ReadingRefCounts(#[source] io::Error),
|
|
#[error("Failed to rebuild ref counts")]
|
|
RebuildingRefCounts(#[source] io::Error),
|
|
#[error("Refcount overflow")]
|
|
RefcountOverflow(#[source] refcount::Error),
|
|
#[error("Refcount table offset past file end")]
|
|
RefcountTableOffEnd,
|
|
#[error("Too many clusters specified for refcount")]
|
|
RefcountTableTooLarge,
|
|
#[error("Failed to resize")]
|
|
ResizeIo(#[source] io::Error),
|
|
#[error("Resize not supported with backing file")]
|
|
ResizeWithBackingFile,
|
|
#[error("Failed to seek file")]
|
|
SeekingFile(#[source] io::Error),
|
|
#[error("Failed to set file size")]
|
|
SettingFileSize(#[source] io::Error),
|
|
#[error("Failed to set refcount refcount")]
|
|
SettingRefcountRefcount(#[source] io::Error),
|
|
#[error("Shrinking QCOW images is not supported")]
|
|
ShrinkNotSupported,
|
|
#[error("Size too small for number of clusters")]
|
|
SizeTooSmallForNumberOfClusters,
|
|
#[error("Failed to sync header")]
|
|
SyncingHeader(#[source] io::Error),
|
|
#[error("L1 entry table too large: {0}")]
|
|
TooManyL1Entries(u64),
|
|
#[error("Ref count table too large: {0}")]
|
|
TooManyRefcounts(u64),
|
|
#[error("Unsupported backing file format: {0}")]
|
|
UnsupportedBackingFileFormat(String),
|
|
#[error("Unsupported compression type")]
|
|
UnsupportedCompressionType,
|
|
#[error("Unsupported qcow2 feature(s)")]
|
|
UnsupportedFeature(#[source] MissingFeatureError),
|
|
#[error("Unsupported refcount order")]
|
|
UnsupportedRefcountOrder,
|
|
#[error("Unsupported version: {0}")]
|
|
UnsupportedVersion(u32),
|
|
#[error("Failed to write data")]
|
|
WritingData(#[source] io::Error),
|
|
#[error("Failed to write header")]
|
|
WritingHeader(#[source] io::Error),
|
|
}
|
|
|
|
pub type Result<T> = std::result::Result<T, Error>;
|
|
|
|
/// Concrete backing file variants.
|
|
pub(crate) enum BackingKind {
|
|
/// Raw backing file.
|
|
Raw(RawFile),
|
|
/// QCOW2 backing parsed into metadata and raw file.
|
|
Qcow {
|
|
inner: Box<metadata::QcowState>,
|
|
backing: Option<Box<BackingFile>>,
|
|
},
|
|
/// Full QcowFile used as backing, only in tests.
|
|
#[cfg(test)]
|
|
QcowFile(Box<QcowFile>),
|
|
}
|
|
/// Backing file wrapper
|
|
pub(crate) struct BackingFile {
|
|
kind: BackingKind,
|
|
virtual_size: u64,
|
|
}
|
|
|
|
impl BackingFile {
|
|
fn new(
|
|
backing_file_config: Option<&BackingFileConfig>,
|
|
direct_io: bool,
|
|
max_nesting_depth: u32,
|
|
sparse: bool,
|
|
) -> BlockResult<Option<Self>> {
|
|
let Some(config) = backing_file_config else {
|
|
return Ok(None);
|
|
};
|
|
|
|
// Check nesting depth - applies to any backing file
|
|
if max_nesting_depth == 0 {
|
|
return Err(BlockError::new(
|
|
BlockErrorKind::Overflow,
|
|
Error::MaxNestingDepthExceeded,
|
|
));
|
|
}
|
|
|
|
let backing_raw_file = OpenOptions::new()
|
|
.read(true)
|
|
.open(&config.path)
|
|
.map_err(|e| {
|
|
BlockError::new(
|
|
BlockErrorKind::Io,
|
|
Error::BackingFileIo(config.path.clone(), e),
|
|
)
|
|
})?;
|
|
|
|
let mut raw_file = RawFile::new(backing_raw_file, direct_io);
|
|
|
|
// Determine backing file format from header extension or auto-detect
|
|
let backing_format = match config.format {
|
|
Some(format) => format,
|
|
None => detect_image_type(&mut raw_file)?,
|
|
};
|
|
|
|
let (kind, virtual_size) = match backing_format {
|
|
ImageType::Raw => {
|
|
let size = raw_file.seek(SeekFrom::End(0)).map_err(|e| {
|
|
BlockError::new(
|
|
BlockErrorKind::Io,
|
|
Error::BackingFileIo(config.path.clone(), e),
|
|
)
|
|
})?;
|
|
raw_file.rewind().map_err(|e| {
|
|
BlockError::new(
|
|
BlockErrorKind::Io,
|
|
Error::BackingFileIo(config.path.clone(), e),
|
|
)
|
|
})?;
|
|
(BackingKind::Raw(raw_file), size)
|
|
}
|
|
ImageType::Qcow2 => {
|
|
let (inner, nested_backing, _sparse) =
|
|
parse_qcow(raw_file, max_nesting_depth - 1, sparse).map_err(|e| {
|
|
let kind = e.kind();
|
|
let source = e
|
|
.into_source()
|
|
.and_then(|s| s.downcast::<Error>().ok())
|
|
.map(|qcow_err| Error::BackingFileOpen(config.path.clone(), qcow_err));
|
|
match source {
|
|
Some(err) => BlockError::new(kind, err),
|
|
None => BlockError::from_kind(kind),
|
|
}
|
|
})?;
|
|
let size = inner.header.size;
|
|
(
|
|
BackingKind::Qcow {
|
|
inner: Box::new(inner),
|
|
backing: nested_backing.map(Box::new),
|
|
},
|
|
size,
|
|
)
|
|
}
|
|
};
|
|
|
|
Ok(Some(Self { kind, virtual_size }))
|
|
}
|
|
|
|
/// Consume and return the kind and virtual size.
|
|
pub(crate) fn into_kind(self) -> (BackingKind, u64) {
|
|
(self.kind, self.virtual_size)
|
|
}
|
|
|
|
/// Read from backing file, returning zeros for any portion beyond backing file size.
|
|
#[inline]
|
|
pub(crate) fn read_at(&mut self, address: u64, buf: &mut [u8]) -> std::io::Result<()> {
|
|
if address >= self.virtual_size {
|
|
buf.fill(0);
|
|
return Ok(());
|
|
}
|
|
|
|
let available = (self.virtual_size - address) as usize;
|
|
let (target, overflow) = if available >= buf.len() {
|
|
(buf, &mut [][..])
|
|
} else {
|
|
buf.split_at_mut(available)
|
|
};
|
|
Self::read_at_inner(&mut self.kind, address, target)?;
|
|
overflow.fill(0);
|
|
Ok(())
|
|
}
|
|
|
|
fn read_at_inner(kind: &mut BackingKind, address: u64, buf: &mut [u8]) -> std::io::Result<()> {
|
|
match kind {
|
|
BackingKind::Raw(file) => {
|
|
file.seek(SeekFrom::Start(address))?;
|
|
file.read_exact(buf)
|
|
}
|
|
#[cfg(test)]
|
|
BackingKind::QcowFile(qcow) => {
|
|
qcow.seek(SeekFrom::Start(address))?;
|
|
qcow.read_exact(buf)
|
|
}
|
|
BackingKind::Qcow { inner, backing } => {
|
|
let has_backing = backing.is_some();
|
|
let cluster_size = inner.raw_file.cluster_size();
|
|
let mut pos = 0usize;
|
|
while pos < buf.len() {
|
|
let curr_addr = address + pos as u64;
|
|
let intra = inner.raw_file.cluster_offset(curr_addr) as usize;
|
|
let count = min(buf.len() - pos, cluster_size as usize - intra);
|
|
let mapping = inner.map_cluster_read(curr_addr, count, has_backing)?;
|
|
match mapping {
|
|
ClusterReadMapping::Zero { length } => {
|
|
buf[pos..pos + length as usize].fill(0);
|
|
}
|
|
ClusterReadMapping::Allocated {
|
|
offset: host_off,
|
|
length,
|
|
} => {
|
|
inner.raw_file.file_mut().seek(SeekFrom::Start(host_off))?;
|
|
inner
|
|
.raw_file
|
|
.file_mut()
|
|
.read_exact(&mut buf[pos..pos + length as usize])?;
|
|
}
|
|
ClusterReadMapping::Compressed {
|
|
host_offset,
|
|
compressed_size,
|
|
cluster_offset,
|
|
length,
|
|
} => {
|
|
let mut compressed = vec![0u8; compressed_size];
|
|
inner
|
|
.raw_file
|
|
.file_mut()
|
|
.seek(SeekFrom::Start(host_offset))?;
|
|
inner.raw_file.file_mut().read_exact(&mut compressed)?;
|
|
let decompressed = decompress_cluster(
|
|
&compressed,
|
|
cluster_size as usize,
|
|
&*inner.header.get_decoder(),
|
|
)?;
|
|
buf[pos..pos + length].copy_from_slice(
|
|
&decompressed[cluster_offset..cluster_offset + length],
|
|
);
|
|
}
|
|
ClusterReadMapping::Backing {
|
|
offset: backing_off,
|
|
length,
|
|
} => {
|
|
if let Some(bf) = backing.as_mut() {
|
|
bf.read_at(backing_off, &mut buf[pos..pos + length as usize])?;
|
|
} else {
|
|
buf[pos..pos + length as usize].fill(0);
|
|
}
|
|
}
|
|
}
|
|
pos += count;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Debug for BackingFile {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
|
f.debug_struct("BackingFile").finish()
|
|
}
|
|
}
|
|
|
|
/// Parses and validates a QCOW2 image file, returning the metadata, backing
|
|
/// file and sparse flag.
|
|
///
|
|
/// Used by [`QcowFile`] and [`QcowDisk`] constructors.
|
|
pub(crate) fn parse_qcow(
|
|
mut file: RawFile,
|
|
max_nesting_depth: u32,
|
|
sparse: bool,
|
|
) -> BlockResult<(metadata::QcowState, Option<BackingFile>, bool)> {
|
|
let mut header = QcowHeader::new(&mut file).map_err(|e| {
|
|
let kind = match &e {
|
|
Error::InvalidMagic
|
|
| Error::BackingFileTooLong(_)
|
|
| Error::InvalidBackingFileName(_) => BlockErrorKind::InvalidFormat,
|
|
Error::UnsupportedFeature(_) | Error::UnsupportedCompressionType => {
|
|
BlockErrorKind::UnsupportedFeature
|
|
}
|
|
_ => BlockErrorKind::Io,
|
|
};
|
|
BlockError::new(kind, e)
|
|
})?;
|
|
|
|
// Only v2 and v3 files are supported.
|
|
if header.version != 2 && header.version != 3 {
|
|
return Err(BlockError::new(
|
|
BlockErrorKind::UnsupportedFeature,
|
|
Error::UnsupportedVersion(header.version),
|
|
));
|
|
}
|
|
|
|
// Make sure that the L1 table fits in RAM.
|
|
if u64::from(header.l1_size) > MAX_RAM_POINTER_TABLE_SIZE {
|
|
return Err(BlockError::new(
|
|
BlockErrorKind::InvalidFormat,
|
|
Error::InvalidL1TableSize(header.l1_size),
|
|
));
|
|
}
|
|
|
|
let cluster_bits: u32 = header.cluster_bits;
|
|
if !(MIN_CLUSTER_BITS..=MAX_CLUSTER_BITS).contains(&cluster_bits) {
|
|
return Err(BlockError::new(
|
|
BlockErrorKind::InvalidFormat,
|
|
Error::InvalidClusterSize,
|
|
));
|
|
}
|
|
let cluster_size = 0x01u64 << cluster_bits;
|
|
|
|
// Limit the total size of the disk.
|
|
if header.size > MAX_QCOW_FILE_SIZE {
|
|
return Err(BlockError::new(
|
|
BlockErrorKind::InvalidFormat,
|
|
Error::FileTooBig(header.size),
|
|
));
|
|
}
|
|
|
|
let direct_io = file.is_direct();
|
|
// QCOW2 relative backing paths are resolved from the image that stores
|
|
// them. Resolve only the config passed to BackingFile::new(), leaving the
|
|
// header copy unchanged so the original backing filename is preserved.
|
|
let backing_file_config = header.backing_file.as_ref().map(|config| {
|
|
let mut config = config.clone();
|
|
if !Path::new(&config.path).is_absolute()
|
|
&& let Ok(disk_path) = read_link(format!("/proc/self/fd/{}", file.as_raw_fd()))
|
|
&& disk_path.exists()
|
|
&& let Some(parent) = disk_path.parent()
|
|
{
|
|
config.path = parent.join(&config.path).to_string_lossy().into_owned();
|
|
}
|
|
config
|
|
});
|
|
|
|
let backing_file = BackingFile::new(
|
|
backing_file_config.as_ref(),
|
|
direct_io,
|
|
max_nesting_depth,
|
|
sparse,
|
|
)?;
|
|
|
|
// Validate refcount order to be 0..6
|
|
let refcount_bits: u64 = 0x01u64.checked_shl(header.refcount_order).ok_or_else(|| {
|
|
BlockError::new(
|
|
BlockErrorKind::UnsupportedFeature,
|
|
Error::UnsupportedRefcountOrder,
|
|
)
|
|
})?;
|
|
if refcount_bits > 64 {
|
|
return Err(BlockError::new(
|
|
BlockErrorKind::UnsupportedFeature,
|
|
Error::UnsupportedRefcountOrder,
|
|
));
|
|
}
|
|
|
|
// Need at least one refcount cluster
|
|
if header.refcount_table_clusters == 0 {
|
|
return Err(BlockError::new(
|
|
BlockErrorKind::InvalidFormat,
|
|
Error::NoRefcountClusters,
|
|
));
|
|
}
|
|
offset_is_cluster_boundary(header.l1_table_offset, header.cluster_bits)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::CorruptImage, e))?;
|
|
offset_is_cluster_boundary(header.snapshots_offset, header.cluster_bits)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::CorruptImage, e))?;
|
|
// refcount table must be a cluster boundary, and within the file's virtual or actual size.
|
|
offset_is_cluster_boundary(header.refcount_table_offset, header.cluster_bits)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::CorruptImage, e))?;
|
|
let file_size = file
|
|
.metadata()
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::GettingFileSize(e)))?
|
|
.len();
|
|
if header.refcount_table_offset > max(file_size, header.size) {
|
|
return Err(BlockError::new(
|
|
BlockErrorKind::CorruptImage,
|
|
Error::RefcountTableOffEnd,
|
|
));
|
|
}
|
|
|
|
// The first cluster should always have a non-zero refcount, so if it is 0,
|
|
// this is an old file with broken refcounts, which requires a rebuild.
|
|
let mut refcount_rebuild_required = true;
|
|
file.seek(SeekFrom::Start(header.refcount_table_offset))
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?;
|
|
let first_refblock_addr = u64::read_be(&mut file)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ReadingHeader(e)))?;
|
|
if first_refblock_addr != 0 {
|
|
file.seek(SeekFrom::Start(first_refblock_addr))
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?;
|
|
let first_cluster_refcount = u16::read_be(&mut file)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ReadingHeader(e)))?;
|
|
if first_cluster_refcount != 0 {
|
|
refcount_rebuild_required = false;
|
|
}
|
|
}
|
|
|
|
if (header.compatible_features & COMPATIBLE_FEATURES_LAZY_REFCOUNTS) != 0 {
|
|
refcount_rebuild_required = true;
|
|
}
|
|
|
|
let mut raw_file = QcowRawFile::from(file, cluster_size, refcount_bits)
|
|
.ok_or_else(|| BlockError::new(BlockErrorKind::InvalidFormat, Error::InvalidClusterSize))?;
|
|
let is_writable = raw_file.file().is_writable();
|
|
|
|
if header.is_corrupt() {
|
|
if is_writable {
|
|
return Err(BlockError::new(
|
|
BlockErrorKind::CorruptImage,
|
|
Error::CorruptImage,
|
|
));
|
|
}
|
|
let path = read_link(format!("/proc/self/fd/{}", raw_file.file().as_raw_fd()))
|
|
.map_or_else(|_| "<unknown>".to_string(), |p| p.display().to_string());
|
|
warn!("QCOW2 image is marked corrupt, opening read-only: {path}");
|
|
}
|
|
|
|
// Image already has dirty bit set. Refcounts may be invalid.
|
|
if IncompatFeatures::from_bits_truncate(header.incompatible_features)
|
|
.contains(IncompatFeatures::DIRTY)
|
|
{
|
|
log::warn!("QCOW2 image not cleanly closed, rebuilding refcounts");
|
|
refcount_rebuild_required = true;
|
|
}
|
|
|
|
// Skip refcount rebuilding for readonly files.
|
|
if refcount_rebuild_required && is_writable {
|
|
QcowFile::rebuild_refcounts(&mut raw_file, header.clone())?;
|
|
}
|
|
|
|
let entries_per_cluster = cluster_size / size_of::<u64>() as u64;
|
|
let num_clusters = div_round_up_u64(header.size, cluster_size);
|
|
let num_l2_clusters = div_round_up_u64(num_clusters, entries_per_cluster);
|
|
let l1_clusters = div_round_up_u64(num_l2_clusters, entries_per_cluster);
|
|
let header_clusters = div_round_up_u64(size_of::<QcowHeader>() as u64, cluster_size);
|
|
if num_l2_clusters > MAX_RAM_POINTER_TABLE_SIZE {
|
|
return Err(BlockError::new(
|
|
BlockErrorKind::CorruptImage,
|
|
Error::TooManyL1Entries(num_l2_clusters),
|
|
));
|
|
}
|
|
let l1_table = VecCache::from_vec(
|
|
raw_file
|
|
.read_pointer_table(
|
|
header.l1_table_offset,
|
|
num_l2_clusters,
|
|
Some(L1_TABLE_OFFSET_MASK),
|
|
)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ReadingHeader(e)))?,
|
|
);
|
|
|
|
let num_clusters = div_round_up_u64(header.size, cluster_size);
|
|
let refcount_clusters = max_refcount_clusters(
|
|
header.refcount_order,
|
|
cluster_size as u32,
|
|
(num_clusters + l1_clusters + num_l2_clusters + header_clusters) as u32,
|
|
);
|
|
// Check that the given header doesn't have a suspiciously sized refcount table.
|
|
if u64::from(header.refcount_table_clusters) > 2 * refcount_clusters {
|
|
return Err(BlockError::new(
|
|
BlockErrorKind::CorruptImage,
|
|
Error::RefcountTableTooLarge,
|
|
));
|
|
}
|
|
if l1_clusters + refcount_clusters > MAX_RAM_POINTER_TABLE_SIZE {
|
|
return Err(BlockError::new(
|
|
BlockErrorKind::InvalidFormat,
|
|
Error::TooManyRefcounts(refcount_clusters),
|
|
));
|
|
}
|
|
let refcount_block_entries = cluster_size * 8 / refcount_bits;
|
|
let mut refcounts = RefCount::new(
|
|
&mut raw_file,
|
|
header.refcount_table_offset,
|
|
refcount_clusters,
|
|
refcount_block_entries,
|
|
cluster_size,
|
|
refcount_bits,
|
|
)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ReadingRefCounts(e)))?;
|
|
|
|
let l2_entries = cluster_size / size_of::<u64>() as u64;
|
|
|
|
// Check that the L1 and refcount tables fit in a 64bit address space.
|
|
let l1_index = (header.size / cluster_size) / l2_entries;
|
|
header
|
|
.l1_table_offset
|
|
.checked_add(l1_index * size_of::<u64>() as u64)
|
|
.ok_or_else(|| {
|
|
BlockError::new(BlockErrorKind::CorruptImage, Error::InvalidL1TableOffset)
|
|
})?;
|
|
header
|
|
.refcount_table_offset
|
|
.checked_add(u64::from(header.refcount_table_clusters) * cluster_size)
|
|
.ok_or_else(|| {
|
|
BlockError::new(
|
|
BlockErrorKind::CorruptImage,
|
|
Error::InvalidRefcountTableOffset,
|
|
)
|
|
})?;
|
|
|
|
// Find available (refcount == 0) clusters for the free list.
|
|
let file_size = raw_file
|
|
.file_mut()
|
|
.metadata()
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::GettingFileSize(e)))?
|
|
.len();
|
|
let mut avail_clusters = Vec::new();
|
|
for i in (0..file_size).step_by(cluster_size as usize) {
|
|
let refcount = refcounts
|
|
.get_cluster_refcount(&mut raw_file, i)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::GettingRefcount(e)))?;
|
|
if refcount == 0 {
|
|
avail_clusters.push(i);
|
|
}
|
|
}
|
|
|
|
if is_writable {
|
|
if !IncompatFeatures::from_bits_truncate(header.incompatible_features)
|
|
.contains(IncompatFeatures::DIRTY)
|
|
{
|
|
header
|
|
.set_dirty_bit(raw_file.file_mut(), true)
|
|
.map_err(|e| {
|
|
BlockError::new(
|
|
BlockErrorKind::Io,
|
|
Error::WritingHeader(io::Error::other(e)),
|
|
)
|
|
})?;
|
|
}
|
|
|
|
header
|
|
.clear_autoclear_features(raw_file.file_mut())
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
|
|
}
|
|
|
|
let inner = metadata::QcowState {
|
|
raw_file,
|
|
header,
|
|
l1_table,
|
|
l2_entries,
|
|
l2_cache: CacheMap::new(100),
|
|
refcounts,
|
|
avail_clusters,
|
|
unref_clusters: Vec::new(),
|
|
};
|
|
|
|
Ok((inner, backing_file, sparse))
|
|
}
|
|
|
|
/// Represents a qcow2 file. This is a sparse file format maintained by the qemu project.
|
|
/// Full documentation of the format can be found in the qemu repository.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # use block::formats::qcow::internal::{QcowFile, RawFile};
|
|
/// # use std::io::{Read, Seek, SeekFrom};
|
|
/// # fn test(file: std::fs::File) -> std::io::Result<()> {
|
|
/// let mut raw_img = RawFile::new(file, false);
|
|
/// let mut q = QcowFile::from(raw_img).expect("Can't open qcow file");
|
|
/// let mut buf = [0u8; 12];
|
|
/// q.seek(SeekFrom::Start(10 as u64))?;
|
|
/// q.read(&mut buf[..])?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
#[derive(Debug)]
|
|
pub struct QcowFile {
|
|
raw_file: QcowRawFile,
|
|
header: QcowHeader,
|
|
l1_table: VecCache<u64>,
|
|
l2_entries: u64,
|
|
l2_cache: CacheMap<VecCache<u64>>,
|
|
refcounts: RefCount,
|
|
current_offset: u64,
|
|
unref_clusters: Vec<u64>, // List of freshly unreferenced clusters.
|
|
// List of unreferenced clusters available to be used. unref clusters become available once the
|
|
// removal of references to them have been synced to disk.
|
|
avail_clusters: Vec<u64>,
|
|
backing_file: Option<BackingFile>,
|
|
sparse: bool,
|
|
}
|
|
|
|
impl QcowFile {
|
|
/// Creates a QcowFile from `file`. File must be a valid qcow2 image.
|
|
///
|
|
/// Additionally, max nesting depth of this qcow2 image will be set to default value 10.
|
|
pub fn from(file: RawFile) -> BlockResult<QcowFile> {
|
|
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(
|
|
file: RawFile,
|
|
max_nesting_depth: u32,
|
|
sparse: bool,
|
|
) -> BlockResult<QcowFile> {
|
|
let (inner, backing_file, sparse) = parse_qcow(file, max_nesting_depth, sparse)?;
|
|
let metadata::QcowState {
|
|
raw_file,
|
|
header,
|
|
l1_table,
|
|
l2_entries,
|
|
l2_cache,
|
|
refcounts,
|
|
avail_clusters,
|
|
unref_clusters,
|
|
} = inner;
|
|
Ok(QcowFile {
|
|
raw_file,
|
|
header,
|
|
l1_table,
|
|
l2_entries,
|
|
l2_cache,
|
|
refcounts,
|
|
current_offset: 0,
|
|
unref_clusters,
|
|
avail_clusters,
|
|
backing_file,
|
|
sparse,
|
|
})
|
|
}
|
|
|
|
/// Creates a new QcowFile at the given path.
|
|
pub fn new(
|
|
file: RawFile,
|
|
version: u32,
|
|
virtual_size: u64,
|
|
sparse: bool,
|
|
) -> BlockResult<QcowFile> {
|
|
let header =
|
|
QcowHeader::create_for_size_and_path(version, virtual_size, None).map_err(|e| {
|
|
let kind = match &e {
|
|
Error::BackingFileTooLong(_) => BlockErrorKind::InvalidFormat,
|
|
_ => BlockErrorKind::Io,
|
|
};
|
|
BlockError::new(kind, e)
|
|
})?;
|
|
QcowFile::new_from_header(file, &header, sparse)
|
|
}
|
|
|
|
/// Creates a new QcowFile at the given path with a backing file.
|
|
pub fn new_from_backing(
|
|
file: RawFile,
|
|
version: u32,
|
|
backing_file_size: u64,
|
|
backing_config: &BackingFileConfig,
|
|
sparse: bool,
|
|
) -> BlockResult<QcowFile> {
|
|
let mut header = QcowHeader::create_for_size_and_path(
|
|
version,
|
|
backing_file_size,
|
|
Some(&backing_config.path),
|
|
)
|
|
.map_err(|e| {
|
|
let kind = match &e {
|
|
Error::BackingFileTooLong(_) => BlockErrorKind::InvalidFormat,
|
|
_ => BlockErrorKind::Io,
|
|
};
|
|
BlockError::new(kind, e)
|
|
})?;
|
|
if let Some(backing_file) = &mut header.backing_file {
|
|
backing_file.format = backing_config.format;
|
|
}
|
|
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,
|
|
sparse: bool,
|
|
) -> BlockResult<QcowFile> {
|
|
file.rewind()
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?;
|
|
header
|
|
.write_to(&mut file)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
|
|
|
|
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;
|
|
let refcount_table_base = qcow.header.refcount_table_offset;
|
|
let end_cluster_addr =
|
|
refcount_table_base + u64::from(qcow.header.refcount_table_clusters) * cluster_size;
|
|
|
|
let mut cluster_addr = 0;
|
|
while cluster_addr < end_cluster_addr {
|
|
let mut unref_clusters = qcow.set_cluster_refcount(cluster_addr, 1).map_err(|e| {
|
|
BlockError::new(BlockErrorKind::Io, Error::SettingRefcountRefcount(e))
|
|
})?;
|
|
qcow.unref_clusters.append(&mut unref_clusters);
|
|
cluster_addr += cluster_size;
|
|
}
|
|
|
|
Ok(qcow)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub fn set_backing_file(&mut self, backing: Option<Box<Self>>) {
|
|
self.backing_file = backing.map(|b| {
|
|
let virtual_size = b.virtual_size();
|
|
BackingFile {
|
|
kind: BackingKind::QcowFile(b),
|
|
virtual_size,
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Returns the `QcowHeader` for this file.
|
|
pub fn header(&self) -> &QcowHeader {
|
|
&self.header
|
|
}
|
|
|
|
/// Returns the L1 lookup table for this file. This is only useful for debugging.
|
|
pub fn l1_table(&self) -> &[u64] {
|
|
self.l1_table.get_values()
|
|
}
|
|
|
|
/// Returns an L2_table of cluster addresses, only used for debugging.
|
|
pub fn l2_table(&mut self, l1_index: usize) -> BlockResult<Option<&[u64]>> {
|
|
let l2_addr_disk = *self
|
|
.l1_table
|
|
.get(l1_index)
|
|
.ok_or_else(|| BlockError::new(BlockErrorKind::OutOfBounds, Error::InvalidIndex))?;
|
|
|
|
if l2_addr_disk == 0 {
|
|
// Reading from an unallocated cluster will return zeros.
|
|
return Ok(None);
|
|
}
|
|
|
|
if !self.l2_cache.contains_key(l1_index) {
|
|
// Not in the cache.
|
|
let table = VecCache::from_vec(
|
|
Self::read_l2_cluster(&mut self.raw_file, l2_addr_disk)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ReadingPointers(e)))?,
|
|
);
|
|
let l1_table = &self.l1_table;
|
|
let raw_file = &mut self.raw_file;
|
|
self.l2_cache
|
|
.insert(l1_index, table, |index, evicted| {
|
|
raw_file.write_pointer_table_direct(l1_table[index], evicted.iter())
|
|
})
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::EvictingCache(e)))?;
|
|
}
|
|
|
|
// The index must exist as it was just inserted if it didn't already.
|
|
Ok(Some(self.l2_cache.get(l1_index).unwrap().get_values()))
|
|
}
|
|
|
|
/// Returns the refcount table for this file. This is only useful for debugging.
|
|
pub fn ref_table(&self) -> &[u64] {
|
|
self.refcounts.ref_table()
|
|
}
|
|
|
|
/// Returns the `index`th refcount block from the file.
|
|
pub fn refcount_block(&mut self, index: usize) -> BlockResult<Option<&[u64]>> {
|
|
self.refcounts
|
|
.refcount_block(&mut self.raw_file, index)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ReadingRefCountBlock(e)))
|
|
}
|
|
|
|
/// Returns the first cluster in the file with a 0 refcount. Used for testing.
|
|
pub fn first_zero_refcount(&mut self) -> BlockResult<Option<u64>> {
|
|
let file_size = self
|
|
.raw_file
|
|
.file_mut()
|
|
.metadata()
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::GettingFileSize(e)))?
|
|
.len();
|
|
let cluster_size = 0x01u64 << self.header.cluster_bits;
|
|
|
|
let mut cluster_addr = 0;
|
|
while cluster_addr < file_size {
|
|
let cluster_refcount = self
|
|
.refcounts
|
|
.get_cluster_refcount(&mut self.raw_file, cluster_addr)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::GettingRefcount(e)))?;
|
|
if cluster_refcount == 0 {
|
|
return Ok(Some(cluster_addr));
|
|
}
|
|
cluster_addr += cluster_size;
|
|
}
|
|
Ok(None)
|
|
}
|
|
|
|
/// Resize the virtual size of the QCOW2 image.
|
|
///
|
|
/// This supports growing the image, including growing the L1 table
|
|
/// if needed. Shrinking is not supported, as it could lead to data
|
|
/// loss. Not supported when a backing file is present in that case
|
|
/// an error is returned.
|
|
pub fn resize(&mut self, new_size: u64) -> BlockResult<()> {
|
|
let current_size = self.virtual_size();
|
|
|
|
if new_size == current_size {
|
|
return Ok(());
|
|
}
|
|
|
|
if new_size < current_size {
|
|
return Err(BlockError::new(
|
|
BlockErrorKind::UnsupportedFeature,
|
|
Error::ShrinkNotSupported,
|
|
));
|
|
}
|
|
|
|
if self.backing_file.is_some() {
|
|
return Err(BlockError::new(
|
|
BlockErrorKind::UnsupportedFeature,
|
|
Error::ResizeWithBackingFile,
|
|
));
|
|
}
|
|
|
|
// Grow the L1 table if needed
|
|
let cluster_size = self.raw_file.cluster_size();
|
|
let entries_per_cluster = cluster_size / size_of::<u64>() as u64;
|
|
let new_clusters = div_round_up_u64(new_size, cluster_size);
|
|
let needed_l1_entries = div_round_up_u64(new_clusters, entries_per_cluster) as u32;
|
|
|
|
if needed_l1_entries > self.header.l1_size {
|
|
self.grow_l1_table(needed_l1_entries)?;
|
|
}
|
|
|
|
self.header.size = new_size;
|
|
|
|
self.raw_file
|
|
.file_mut()
|
|
.rewind()
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?;
|
|
self.header
|
|
.write_to(self.raw_file.file_mut())
|
|
.map_err(|e| match e {
|
|
Error::WritingHeader(io_err) => {
|
|
BlockError::new(BlockErrorKind::Io, Error::ResizeIo(io_err))
|
|
}
|
|
other => BlockError::new(BlockErrorKind::Io, other),
|
|
})?;
|
|
|
|
self.raw_file
|
|
.file_mut()
|
|
.sync_all()
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SyncingHeader(e)))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Grow the L1 table to accommodate at least `new_l1_size` entries.
|
|
///
|
|
/// This allocates a new L1 table at file end (guaranteeing contiguity),
|
|
/// copies existing entries, updates refcounts, and atomically switches
|
|
/// to the new table.
|
|
fn grow_l1_table(&mut self, new_l1_size: u32) -> BlockResult<()> {
|
|
let old_l1_size = self.header.l1_size;
|
|
let old_l1_offset = self.header.l1_table_offset;
|
|
let cluster_size = self.raw_file.cluster_size();
|
|
|
|
let new_l1_bytes = new_l1_size as u64 * size_of::<u64>() as u64;
|
|
let new_l1_clusters = div_round_up_u64(new_l1_bytes, cluster_size);
|
|
|
|
// Allocate contiguous clusters at file end for new L1 table
|
|
let file_size = self
|
|
.raw_file
|
|
.file_mut()
|
|
.seek(SeekFrom::End(0))
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ResizeIo(e)))?;
|
|
let new_l1_offset = self.raw_file.cluster_address(file_size + cluster_size - 1);
|
|
|
|
// Extend file to fit all L1 clusters
|
|
let new_file_end = new_l1_offset + new_l1_clusters * cluster_size;
|
|
self.raw_file
|
|
.file_mut()
|
|
.set_len(new_file_end)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SettingFileSize(e)))?;
|
|
|
|
// Set refcounts for the contiguous range
|
|
for i in 0..new_l1_clusters {
|
|
self.set_cluster_refcount(new_l1_offset + i * cluster_size, 1)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ResizeIo(e)))?;
|
|
}
|
|
|
|
let mut new_l1_data = vec![0u64; new_l1_size as usize];
|
|
let old_entries = self.l1_table.get_values();
|
|
new_l1_data[..old_entries.len()].copy_from_slice(old_entries);
|
|
|
|
for (i, l2_addr) in new_l1_data.iter_mut().enumerate() {
|
|
if *l2_addr != 0 && i < old_entries.len() {
|
|
let refcount = self
|
|
.refcounts
|
|
.get_cluster_refcount(&mut self.raw_file, *l2_addr)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::GettingRefcount(e)))?;
|
|
*l2_addr = l1_entry_make(*l2_addr, refcount == 1);
|
|
}
|
|
}
|
|
|
|
// Write the new L1 table to the file.
|
|
self.raw_file
|
|
.write_pointer_table_direct(new_l1_offset, new_l1_data.iter())
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ResizeIo(e)))?;
|
|
|
|
self.raw_file
|
|
.file_mut()
|
|
.sync_all()
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SyncingHeader(e)))?;
|
|
|
|
self.header.l1_size = new_l1_size;
|
|
self.header.l1_table_offset = new_l1_offset;
|
|
|
|
self.raw_file
|
|
.file_mut()
|
|
.rewind()
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?;
|
|
self.header
|
|
.write_to(self.raw_file.file_mut())
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
|
|
|
|
self.raw_file
|
|
.file_mut()
|
|
.sync_all()
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SyncingHeader(e)))?;
|
|
|
|
// Free old L1 table clusters
|
|
let old_l1_bytes = old_l1_size as u64 * size_of::<u64>() as u64;
|
|
let old_l1_clusters = div_round_up_u64(old_l1_bytes, cluster_size);
|
|
for i in 0..old_l1_clusters {
|
|
let cluster_addr = old_l1_offset + i * cluster_size;
|
|
let _ = self.set_cluster_refcount(cluster_addr, 0);
|
|
}
|
|
|
|
// Update L1 table cache
|
|
self.l1_table.extend(new_l1_size as usize);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Rebuild the reference count tables.
|
|
fn rebuild_refcounts(raw_file: &mut QcowRawFile, header: QcowHeader) -> BlockResult<()> {
|
|
fn add_ref(
|
|
refcounts: &mut [u64],
|
|
cluster_size: u64,
|
|
cluster_address: u64,
|
|
max_refcount: u64,
|
|
refcount_bits: u64,
|
|
) -> Result<()> {
|
|
let idx = (cluster_address / cluster_size) as usize;
|
|
if idx >= refcounts.len() {
|
|
return Err(Error::InvalidClusterIndex);
|
|
}
|
|
if refcounts[idx] >= max_refcount {
|
|
return Err(Error::RefcountOverflow(refcount::Error::RefcountOverflow {
|
|
value: refcounts[idx] + 1,
|
|
max: max_refcount,
|
|
refcount_bits,
|
|
}));
|
|
}
|
|
refcounts[idx] += 1;
|
|
Ok(())
|
|
}
|
|
|
|
// Add a reference to the first cluster (header plus extensions).
|
|
fn set_header_refcount(
|
|
refcounts: &mut [u64],
|
|
cluster_size: u64,
|
|
max_refcount: u64,
|
|
refcount_bits: u64,
|
|
) -> Result<()> {
|
|
add_ref(refcounts, cluster_size, 0, max_refcount, refcount_bits)
|
|
}
|
|
|
|
// Add references to the L1 table clusters.
|
|
fn set_l1_refcounts(
|
|
refcounts: &mut [u64],
|
|
header: &QcowHeader,
|
|
cluster_size: u64,
|
|
max_refcount: u64,
|
|
refcount_bits: u64,
|
|
) -> Result<()> {
|
|
let entries_per_cluster = cluster_size / size_of::<u64>() as u64;
|
|
let l1_clusters = div_round_up_u64(u64::from(header.l1_size), entries_per_cluster);
|
|
let l1_table_offset = header.l1_table_offset;
|
|
for i in 0..l1_clusters {
|
|
add_ref(
|
|
refcounts,
|
|
cluster_size,
|
|
l1_table_offset + i * cluster_size,
|
|
max_refcount,
|
|
refcount_bits,
|
|
)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// Traverse the L1 and L2 tables to find all reachable data clusters.
|
|
fn set_data_refcounts(
|
|
refcounts: &mut [u64],
|
|
header: &QcowHeader,
|
|
cluster_size: u64,
|
|
raw_file: &mut QcowRawFile,
|
|
max_refcount: u64,
|
|
refcount_bits: u64,
|
|
) -> Result<()> {
|
|
let l1_table = raw_file
|
|
.read_pointer_table(
|
|
header.l1_table_offset,
|
|
u64::from(header.l1_size),
|
|
Some(L1_TABLE_OFFSET_MASK),
|
|
)
|
|
.map_err(Error::ReadingPointers)?;
|
|
for l1_index in 0..header.l1_size as usize {
|
|
let l2_addr_disk = *l1_table.get(l1_index).ok_or(Error::InvalidIndex)?;
|
|
if l2_addr_disk != 0 {
|
|
// Add a reference to the L2 table cluster itself.
|
|
add_ref(
|
|
refcounts,
|
|
cluster_size,
|
|
l2_addr_disk,
|
|
max_refcount,
|
|
refcount_bits,
|
|
)?;
|
|
|
|
// Read the L2 table and find all referenced data clusters.
|
|
let l2_table = raw_file
|
|
.read_pointer_table(
|
|
l2_addr_disk,
|
|
cluster_size / size_of::<u64>() as u64,
|
|
Some(L2_TABLE_OFFSET_MASK),
|
|
)
|
|
.map_err(Error::ReadingPointers)?;
|
|
for data_cluster_addr in l2_table {
|
|
if data_cluster_addr != 0 {
|
|
add_ref(
|
|
refcounts,
|
|
cluster_size,
|
|
data_cluster_addr,
|
|
max_refcount,
|
|
refcount_bits,
|
|
)?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Add references to the top-level refcount table clusters.
|
|
fn set_refcount_table_refcounts(
|
|
refcounts: &mut [u64],
|
|
header: &QcowHeader,
|
|
cluster_size: u64,
|
|
max_refcount: u64,
|
|
refcount_bits: u64,
|
|
) -> Result<()> {
|
|
let refcount_table_offset = header.refcount_table_offset;
|
|
for i in 0..u64::from(header.refcount_table_clusters) {
|
|
add_ref(
|
|
refcounts,
|
|
cluster_size,
|
|
refcount_table_offset + i * cluster_size,
|
|
max_refcount,
|
|
refcount_bits,
|
|
)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// Allocate clusters for refblocks.
|
|
// This needs to be done last so that we have the correct refcounts for all other
|
|
// clusters.
|
|
fn alloc_refblocks(
|
|
refcounts: &mut [u64],
|
|
cluster_size: u64,
|
|
refblock_clusters: u64,
|
|
max_refcount: u64,
|
|
refcount_bits: u64,
|
|
) -> Result<Vec<u64>> {
|
|
let mut ref_table = vec![0; refblock_clusters as usize];
|
|
let mut first_free_cluster: u64 = 0;
|
|
for refblock_addr in &mut ref_table {
|
|
loop {
|
|
if first_free_cluster >= refcounts.len() as u64 {
|
|
return Err(Error::NotEnoughSpaceForRefcounts);
|
|
}
|
|
if refcounts[first_free_cluster as usize] == 0 {
|
|
break;
|
|
}
|
|
first_free_cluster += 1;
|
|
}
|
|
|
|
*refblock_addr = first_free_cluster * cluster_size;
|
|
add_ref(
|
|
refcounts,
|
|
cluster_size,
|
|
*refblock_addr,
|
|
max_refcount,
|
|
refcount_bits,
|
|
)?;
|
|
|
|
first_free_cluster += 1;
|
|
}
|
|
|
|
Ok(ref_table)
|
|
}
|
|
|
|
// Write the updated reference count blocks and reftable.
|
|
fn write_refblocks(
|
|
refcounts: &[u64],
|
|
mut header: QcowHeader,
|
|
ref_table: &[u64],
|
|
raw_file: &mut QcowRawFile,
|
|
refcount_block_entries: u64,
|
|
) -> Result<()> {
|
|
// Rewrite the header with lazy refcounts enabled while we are rebuilding the tables.
|
|
header.compatible_features |= COMPATIBLE_FEATURES_LAZY_REFCOUNTS;
|
|
raw_file.file_mut().rewind().map_err(Error::SeekingFile)?;
|
|
header.write_to(raw_file.file_mut())?;
|
|
|
|
for (i, refblock_addr) in ref_table.iter().enumerate() {
|
|
// Write a block of refcounts to the location indicated by refblock_addr.
|
|
let refblock_start = i * (refcount_block_entries as usize);
|
|
let refblock_end = min(
|
|
refcounts.len(),
|
|
refblock_start + refcount_block_entries as usize,
|
|
);
|
|
let refblock = &refcounts[refblock_start..refblock_end];
|
|
raw_file
|
|
.write_refcount_block(*refblock_addr, refblock)
|
|
.map_err(Error::WritingHeader)?;
|
|
|
|
// If this is the last (partial) cluster, pad it out to a full refblock cluster.
|
|
if refblock.len() < refcount_block_entries as usize {
|
|
let refblock_padding =
|
|
vec![0u64; refcount_block_entries as usize - refblock.len()];
|
|
let byte_offset =
|
|
refblock.len() as u64 * raw_file.cluster_size() / refcount_block_entries;
|
|
raw_file
|
|
.write_refcount_block(*refblock_addr + byte_offset, &refblock_padding)
|
|
.map_err(Error::WritingHeader)?;
|
|
}
|
|
}
|
|
|
|
// Rewrite the top-level refcount table.
|
|
raw_file
|
|
.write_pointer_table_direct(header.refcount_table_offset, ref_table.iter())
|
|
.map_err(Error::WritingHeader)?;
|
|
|
|
// Rewrite the header again, now with lazy refcounts disabled.
|
|
header.compatible_features &= !COMPATIBLE_FEATURES_LAZY_REFCOUNTS;
|
|
raw_file.file_mut().rewind().map_err(Error::SeekingFile)?;
|
|
header.write_to(raw_file.file_mut())?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
let cluster_size = raw_file.cluster_size();
|
|
|
|
let file_size = raw_file
|
|
.file_mut()
|
|
.metadata()
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::GettingFileSize(e)))?
|
|
.len();
|
|
|
|
let refcount_bits = 1u64 << header.refcount_order;
|
|
let max_refcount = if refcount_bits == 64 {
|
|
u64::MAX
|
|
} else {
|
|
(1u64 << refcount_bits) - 1
|
|
};
|
|
let refcount_block_entries = cluster_size * 8 / refcount_bits;
|
|
let pointers_per_cluster = cluster_size / size_of::<u64>() as u64;
|
|
let data_clusters = div_round_up_u64(header.size, cluster_size);
|
|
let l2_clusters = div_round_up_u64(data_clusters, pointers_per_cluster);
|
|
let l1_clusters = div_round_up_u64(l2_clusters, pointers_per_cluster);
|
|
let header_clusters = div_round_up_u64(size_of::<QcowHeader>() as u64, cluster_size);
|
|
let max_clusters = data_clusters + l2_clusters + l1_clusters + header_clusters;
|
|
let mut max_valid_cluster_index = max_clusters;
|
|
let refblock_clusters = div_round_up_u64(max_valid_cluster_index, refcount_block_entries);
|
|
let reftable_clusters = div_round_up_u64(refblock_clusters, pointers_per_cluster);
|
|
// Account for refblocks and the ref table size needed to address them.
|
|
let refblocks_for_refs = div_round_up_u64(
|
|
refblock_clusters + reftable_clusters,
|
|
refcount_block_entries,
|
|
);
|
|
let reftable_clusters_for_refs =
|
|
div_round_up_u64(refblocks_for_refs, refcount_block_entries);
|
|
max_valid_cluster_index += refblock_clusters + reftable_clusters;
|
|
max_valid_cluster_index += refblocks_for_refs + reftable_clusters_for_refs;
|
|
|
|
if max_valid_cluster_index > MAX_RAM_POINTER_TABLE_SIZE {
|
|
return Err(BlockError::new(
|
|
BlockErrorKind::CorruptImage,
|
|
Error::InvalidRefcountTableSize(max_valid_cluster_index),
|
|
));
|
|
}
|
|
|
|
let max_valid_cluster_offset = max_valid_cluster_index * cluster_size;
|
|
if max_valid_cluster_offset < file_size - cluster_size {
|
|
return Err(BlockError::new(
|
|
BlockErrorKind::CorruptImage,
|
|
Error::InvalidRefcountTableSize(max_valid_cluster_offset),
|
|
));
|
|
}
|
|
|
|
let mut refcounts = vec![0; max_valid_cluster_index as usize];
|
|
|
|
// Find all references clusters and rebuild refcounts.
|
|
set_header_refcount(&mut refcounts, cluster_size, max_refcount, refcount_bits)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
|
|
set_l1_refcounts(
|
|
&mut refcounts,
|
|
&header,
|
|
cluster_size,
|
|
max_refcount,
|
|
refcount_bits,
|
|
)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
|
|
set_data_refcounts(
|
|
&mut refcounts,
|
|
&header,
|
|
cluster_size,
|
|
raw_file,
|
|
max_refcount,
|
|
refcount_bits,
|
|
)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
|
|
set_refcount_table_refcounts(
|
|
&mut refcounts,
|
|
&header,
|
|
cluster_size,
|
|
max_refcount,
|
|
refcount_bits,
|
|
)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
|
|
|
|
// Allocate clusters to store the new reference count blocks.
|
|
let ref_table = alloc_refblocks(
|
|
&mut refcounts,
|
|
cluster_size,
|
|
refblock_clusters,
|
|
max_refcount,
|
|
refcount_bits,
|
|
)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
|
|
|
|
// Write updated reference counts and point the reftable at them.
|
|
write_refblocks(
|
|
&refcounts,
|
|
header,
|
|
&ref_table,
|
|
raw_file,
|
|
refcount_block_entries,
|
|
)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, e))
|
|
}
|
|
|
|
// Limits the range so that it doesn't exceed the virtual size of the file.
|
|
fn limit_range_file(&self, address: u64, count: usize) -> usize {
|
|
if address.checked_add(count as u64).is_none() || address > self.virtual_size() {
|
|
return 0;
|
|
}
|
|
min(count as u64, self.virtual_size() - address) as usize
|
|
}
|
|
|
|
// Limits the range so that it doesn't overflow the end of a cluster.
|
|
fn limit_range_cluster(&self, address: u64, count: usize) -> usize {
|
|
let offset: u64 = self.raw_file.cluster_offset(address);
|
|
let limit = self.raw_file.cluster_size() - offset;
|
|
min(count as u64, limit) as usize
|
|
}
|
|
|
|
// Gets the maximum virtual size of this image.
|
|
fn virtual_size(&self) -> u64 {
|
|
self.header.size
|
|
}
|
|
|
|
// Gets the offset of `address` in the L1 table.
|
|
fn l1_table_index(&self, address: u64) -> u64 {
|
|
(address / self.raw_file.cluster_size()) / self.l2_entries
|
|
}
|
|
|
|
// Gets the offset of `address` in the L2 table.
|
|
fn l2_table_index(&self, address: u64) -> u64 {
|
|
(address / self.raw_file.cluster_size()) % self.l2_entries
|
|
}
|
|
|
|
/// Attempts to set the corrupt bit, logging failures without propagating them.
|
|
///
|
|
/// This is "best effort" because the write may fail due to various reasons like
|
|
/// disk full, readonly storage, etc. This method is called just before returning
|
|
/// EIO to the caller. The error is not propagated because the original corruption
|
|
/// error is more important to return to the call site than a secondary I/O
|
|
/// failure from marking the image.
|
|
fn set_corrupt_bit_best_effort(&mut self) {
|
|
if let Err(e) = self.header.set_corrupt_bit(self.raw_file.file_mut()) {
|
|
warn!("Failed to persist corrupt bit: {e}");
|
|
}
|
|
}
|
|
|
|
// Decompress the cluster, return EIO on failure
|
|
fn decompress_l2_cluster(&mut self, l2_entry: u64) -> std::io::Result<Vec<u8>> {
|
|
let (compressed_cluster_addr, compressed_cluster_size) =
|
|
l2_entry_compressed_cluster_layout(l2_entry, self.header.cluster_bits);
|
|
// Read compressed cluster from raw file
|
|
self.raw_file
|
|
.file_mut()
|
|
.seek(SeekFrom::Start(compressed_cluster_addr))?;
|
|
let mut compressed_cluster = vec![0; compressed_cluster_size];
|
|
self.raw_file
|
|
.file_mut()
|
|
.read_exact(&mut compressed_cluster)?;
|
|
let decoder = self.header.get_decoder();
|
|
// Decompress
|
|
let cluster_size = self.raw_file.cluster_size() as usize;
|
|
let mut decompressed_cluster = vec![0; cluster_size];
|
|
let decompressed_size = decoder
|
|
.decode(&compressed_cluster, &mut decompressed_cluster)
|
|
.map_err(|_| {
|
|
self.set_corrupt_bit_best_effort();
|
|
io::Error::from_raw_os_error(EIO)
|
|
})?;
|
|
if decompressed_size as u64 != self.raw_file.cluster_size() {
|
|
self.set_corrupt_bit_best_effort();
|
|
return Err(std::io::Error::from_raw_os_error(EIO));
|
|
}
|
|
Ok(decompressed_cluster)
|
|
}
|
|
|
|
fn file_read(
|
|
&mut self,
|
|
address: u64,
|
|
count: usize,
|
|
buf: &mut [u8],
|
|
) -> std::io::Result<Option<()>> {
|
|
let err_inval = std::io::Error::from_raw_os_error(EINVAL);
|
|
if address >= self.virtual_size() {
|
|
return Err(err_inval);
|
|
}
|
|
|
|
let l1_index = self.l1_table_index(address) as usize;
|
|
let l2_addr_disk = *self
|
|
.l1_table
|
|
.get(l1_index)
|
|
.ok_or_else(|| std::io::Error::from_raw_os_error(EINVAL))?;
|
|
|
|
if l2_addr_disk == 0 {
|
|
// Reading from an unallocated cluster will return zeros.
|
|
return Ok(None);
|
|
}
|
|
|
|
let l2_index = self.l2_table_index(address) as usize;
|
|
|
|
self.cache_l2_cluster(l1_index, l2_addr_disk, false)?;
|
|
|
|
let l2_entry = self.l2_cache.get(l1_index).unwrap()[l2_index];
|
|
if l2_entry_is_empty(l2_entry) {
|
|
// Reading from an unallocated cluster will return zeros.
|
|
return Ok(None);
|
|
} else if l2_entry_is_compressed(l2_entry) {
|
|
// Compressed cluster.
|
|
// Read it, decompress, then return slice from decompressed data.
|
|
let mut decompressed_cluster = self.decompress_l2_cluster(l2_entry)?;
|
|
decompressed_cluster.resize(self.raw_file.cluster_size() as usize, 0);
|
|
let start = self.raw_file.cluster_offset(address) as usize;
|
|
let end = start.checked_add(count);
|
|
if end.is_none() || end.unwrap() > decompressed_cluster.len() {
|
|
return Err(err_inval);
|
|
}
|
|
buf[..count].copy_from_slice(&decompressed_cluster[start..end.unwrap()]);
|
|
} else if l2_entry_is_zero(l2_entry) {
|
|
// Cluster with zero flag reads as zeros without accessing disk.
|
|
buf[..count].fill(0);
|
|
return Ok(Some(()));
|
|
} else {
|
|
let cluster_addr = l2_entry_std_cluster_addr(l2_entry);
|
|
if cluster_addr & (self.raw_file.cluster_size() - 1) != 0 {
|
|
self.set_corrupt_bit_best_effort();
|
|
return Err(io::Error::from_raw_os_error(EIO));
|
|
}
|
|
let start = cluster_addr + self.raw_file.cluster_offset(address);
|
|
let raw_file = self.raw_file.file_mut();
|
|
raw_file.seek(SeekFrom::Start(start))?;
|
|
raw_file.read_exact(buf)?;
|
|
}
|
|
Ok(Some(()))
|
|
}
|
|
|
|
// Gets the offset of the given guest address in the host file. If L1, L2, or data clusters need
|
|
// to be allocated, they will be.
|
|
fn file_offset_write(&mut self, address: u64) -> std::io::Result<u64> {
|
|
if address >= self.virtual_size() {
|
|
return Err(std::io::Error::from_raw_os_error(EINVAL));
|
|
}
|
|
|
|
let l1_index = self.l1_table_index(address) as usize;
|
|
let l2_addr_disk = *self
|
|
.l1_table
|
|
.get(l1_index)
|
|
.ok_or_else(|| std::io::Error::from_raw_os_error(EINVAL))?;
|
|
let l2_index = self.l2_table_index(address) as usize;
|
|
|
|
let mut set_refcounts = Vec::new();
|
|
|
|
if let Some(new_addr) = self.cache_l2_cluster(l1_index, l2_addr_disk, true)? {
|
|
// The cluster refcount starts at one meaning it is used but doesn't need COW.
|
|
set_refcounts.push((new_addr, 1));
|
|
}
|
|
|
|
let l2_entry = self.l2_cache.get(l1_index).unwrap()[l2_index];
|
|
let cluster_addr = if l2_entry_is_compressed(l2_entry) {
|
|
// Writing to compressed cluster.
|
|
|
|
// Allocate new cluster, decompress into new cluster, then use
|
|
// offset of new cluster.
|
|
let decompressed_cluster = self.decompress_l2_cluster(l2_entry)?;
|
|
let cluster_addr = self.append_data_cluster(None)?;
|
|
self.update_cluster_addr(l1_index, l2_index, cluster_addr, &mut set_refcounts)?;
|
|
self.raw_file
|
|
.file_mut()
|
|
.seek(SeekFrom::Start(cluster_addr))?;
|
|
let nwritten = self.raw_file.file_mut().write(&decompressed_cluster)?;
|
|
if nwritten != decompressed_cluster.len() {
|
|
self.set_corrupt_bit_best_effort();
|
|
return Err(std::io::Error::from_raw_os_error(EIO));
|
|
}
|
|
|
|
// Decrement refcount for each cluster spanned by the old compressed data
|
|
self.deallocate_compressed_cluster(l2_entry)?;
|
|
|
|
cluster_addr
|
|
} else if l2_entry_is_empty(l2_entry) || l2_entry_is_zero(l2_entry) {
|
|
let cluster_addr = if l2_entry_is_zero(l2_entry) {
|
|
self.append_zeroed_data_cluster()?
|
|
} else {
|
|
let initial_data = if let Some(backing) = self.backing_file.as_mut() {
|
|
let cluster_size = self.raw_file.cluster_size();
|
|
let cluster_begin = address - (address % cluster_size);
|
|
let mut cluster_data = vec![0u8; cluster_size as usize];
|
|
backing.read_at(cluster_begin, &mut cluster_data)?;
|
|
Some(cluster_data)
|
|
} else {
|
|
None
|
|
};
|
|
self.append_data_cluster(initial_data)?
|
|
};
|
|
// Need to allocate a data cluster
|
|
self.update_cluster_addr(l1_index, l2_index, cluster_addr, &mut set_refcounts)?;
|
|
cluster_addr
|
|
} else {
|
|
let cluster_addr = l2_entry_std_cluster_addr(l2_entry);
|
|
if cluster_addr & (self.raw_file.cluster_size() - 1) != 0 {
|
|
self.set_corrupt_bit_best_effort();
|
|
return Err(io::Error::from_raw_os_error(EIO));
|
|
}
|
|
cluster_addr
|
|
};
|
|
|
|
for (addr, count) in set_refcounts {
|
|
self.set_cluster_refcount_track_freed(addr, count)?;
|
|
}
|
|
|
|
Ok(cluster_addr + self.raw_file.cluster_offset(address))
|
|
}
|
|
|
|
// Updates the l1 and l2 tables to point to the new `cluster_addr`.
|
|
fn update_cluster_addr(
|
|
&mut self,
|
|
l1_index: usize,
|
|
l2_index: usize,
|
|
cluster_addr: u64,
|
|
set_refcounts: &mut Vec<(u64, u64)>,
|
|
) -> io::Result<()> {
|
|
if !self.l2_cache.get(l1_index).unwrap().dirty() {
|
|
// Free the previously used cluster if one exists. Modified tables are always
|
|
// witten to new clusters so the L1 table can be committed to disk after they
|
|
// are and L1 never points at an invalid table.
|
|
// The index must be valid from when it was inserted.
|
|
let addr = self.l1_table[l1_index];
|
|
if addr != 0 {
|
|
self.unref_clusters.push(addr);
|
|
set_refcounts.push((addr, 0));
|
|
}
|
|
|
|
// Allocate a new cluster to store the L2 table and update the L1 table to point
|
|
// to the new table. The cluster will be written when the cache is flushed, no
|
|
// need to copy the data now.
|
|
let new_addr: u64 = self.get_new_cluster(None)?;
|
|
// The cluster refcount starts at one indicating it is used but doesn't need
|
|
// COW.
|
|
set_refcounts.push((new_addr, 1));
|
|
self.l1_table[l1_index] = new_addr;
|
|
}
|
|
// 'unwrap' is OK because it was just added.
|
|
self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = l2_entry_make_std(cluster_addr);
|
|
Ok(())
|
|
}
|
|
|
|
// Allocate a new cluster and return its offset within the raw file.
|
|
fn get_new_cluster(&mut self, initial_data: Option<Vec<u8>>) -> std::io::Result<u64> {
|
|
// First use a pre allocated cluster if one is available.
|
|
if let Some(free_cluster) = self.avail_clusters.pop() {
|
|
if free_cluster == 0 {
|
|
self.set_corrupt_bit_best_effort();
|
|
return Err(io::Error::from_raw_os_error(EIO));
|
|
}
|
|
if let Some(initial_data) = initial_data {
|
|
self.raw_file.write_cluster(free_cluster, &initial_data)?;
|
|
} else {
|
|
self.raw_file.zero_cluster(free_cluster)?;
|
|
}
|
|
return Ok(free_cluster);
|
|
}
|
|
|
|
let max_valid_cluster_offset = self.refcounts.max_valid_cluster_offset();
|
|
if let Some(new_cluster) = self.raw_file.add_cluster_end(max_valid_cluster_offset)? {
|
|
if new_cluster == 0 {
|
|
self.set_corrupt_bit_best_effort();
|
|
return Err(io::Error::from_raw_os_error(EIO));
|
|
}
|
|
if let Some(initial_data) = initial_data {
|
|
self.raw_file.write_cluster(new_cluster, &initial_data)?;
|
|
}
|
|
Ok(new_cluster)
|
|
} else {
|
|
error!("No free clusters in get_new_cluster()");
|
|
Err(std::io::Error::from_raw_os_error(ENOSPC))
|
|
}
|
|
}
|
|
|
|
// Allocate and initialize a new data cluster. Returns the offset of the
|
|
// cluster into the file on success.
|
|
fn append_data_cluster(&mut self, initial_data: Option<Vec<u8>>) -> std::io::Result<u64> {
|
|
let new_addr: u64 = self.get_new_cluster(initial_data)?;
|
|
// The cluster refcount starts at one indicating it is used but doesn't need COW.
|
|
self.set_cluster_refcount_track_freed(new_addr, 1)?;
|
|
Ok(new_addr)
|
|
}
|
|
|
|
// Allocate and initialize a zeroed data cluster without building a cluster-sized buffer.
|
|
fn append_zeroed_data_cluster(&mut self) -> std::io::Result<u64> {
|
|
let new_addr: u64 = self.get_new_cluster(None)?;
|
|
let cluster_size = self.raw_file.cluster_size() as usize;
|
|
self.raw_file
|
|
.file_mut()
|
|
.write_zeroes_at(new_addr, cluster_size)?;
|
|
// The cluster refcount starts at one indicating it is used but doesn't need COW.
|
|
self.set_cluster_refcount_track_freed(new_addr, 1)?;
|
|
Ok(new_addr)
|
|
}
|
|
|
|
// Returns true if the cluster containing `address` is already allocated.
|
|
fn cluster_allocated(&mut self, address: u64) -> std::io::Result<bool> {
|
|
if address >= self.virtual_size() {
|
|
return Err(std::io::Error::from_raw_os_error(EINVAL));
|
|
}
|
|
|
|
let l1_index = self.l1_table_index(address) as usize;
|
|
let l2_addr_disk = *self
|
|
.l1_table
|
|
.get(l1_index)
|
|
.ok_or_else(|| std::io::Error::from_raw_os_error(EINVAL))?;
|
|
let l2_index = self.l2_table_index(address) as usize;
|
|
|
|
if l2_addr_disk == 0 {
|
|
// Empty overlay metadata means "consult backing" when a backing
|
|
// file exists; otherwise it is a hole in this image.
|
|
return Ok(self.backing_file.is_some());
|
|
}
|
|
|
|
self.cache_l2_cluster(l1_index, l2_addr_disk, false)?;
|
|
|
|
let l2_entry = self.l2_cache.get(l1_index).unwrap()[l2_index];
|
|
if l2_entry_is_empty(l2_entry) {
|
|
// Empty cluster with backing has existing data to seek in the backing file.
|
|
Ok(self.backing_file.is_some())
|
|
} else if l2_entry_is_compressed(l2_entry) {
|
|
Ok(true)
|
|
} else if l2_entry_is_zero(l2_entry) {
|
|
// Zero flagged cluster is a logical hole. It reads as zeros with no data to seek.
|
|
Ok(false)
|
|
} else {
|
|
Ok(true)
|
|
}
|
|
}
|
|
|
|
// Find the first guest address greater than or equal to `address` whose allocation state
|
|
// matches `allocated`.
|
|
fn find_allocated_cluster(
|
|
&mut self,
|
|
address: u64,
|
|
allocated: bool,
|
|
) -> std::io::Result<Option<u64>> {
|
|
let size = self.virtual_size();
|
|
if address >= size {
|
|
return Ok(None);
|
|
}
|
|
|
|
// If offset is already within a hole, return it.
|
|
if self.cluster_allocated(address)? == allocated {
|
|
return Ok(Some(address));
|
|
}
|
|
|
|
// Skip to the next cluster boundary.
|
|
let cluster_size = self.raw_file.cluster_size();
|
|
let mut cluster_addr = (address / cluster_size + 1) * cluster_size;
|
|
|
|
// Search for clusters with the desired allocation state.
|
|
while cluster_addr < size {
|
|
if self.cluster_allocated(cluster_addr)? == allocated {
|
|
return Ok(Some(cluster_addr));
|
|
}
|
|
cluster_addr += cluster_size;
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
// Deallocate compressed cluster and all related clusters spanned by compressed data.
|
|
fn deallocate_compressed_cluster(&mut self, l2_entry: u64) -> std::io::Result<()> {
|
|
let (compressed_cluster_addr, compressed_cluster_size) =
|
|
l2_entry_compressed_cluster_layout(l2_entry, self.header.cluster_bits);
|
|
|
|
// Calculate the end of the compressed data region
|
|
let compressed_clusters_end = self.raw_file.cluster_address(
|
|
compressed_cluster_addr // Start of compressed data
|
|
+ compressed_cluster_size as u64 // Add size to get end address
|
|
+ self.raw_file.cluster_size()
|
|
- 1, // Catch possibly partially used last cluster
|
|
);
|
|
|
|
// Decrement refcount for each cluster spanned by the compressed data
|
|
let mut addr = self.raw_file.cluster_address(compressed_cluster_addr);
|
|
while addr < compressed_clusters_end {
|
|
let refcount = self
|
|
.refcounts
|
|
.get_cluster_refcount(&mut self.raw_file, addr)
|
|
.map_err(|e| {
|
|
if matches!(e, refcount::Error::RefblockUnaligned(_)) {
|
|
self.set_corrupt_bit_best_effort();
|
|
}
|
|
io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
format!("failed to get cluster refcount: {e}"),
|
|
)
|
|
})?;
|
|
if refcount > 0 {
|
|
self.set_cluster_refcount_track_freed(addr, refcount - 1)?;
|
|
}
|
|
addr += self.raw_file.cluster_size();
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Deallocate the storage for the cluster starting at `address`.
|
|
// If `zero_marker` is true, preserve WRITE_ZEROES semantics with a logical-zero
|
|
// entry instead of allowing backing data to reappear through an empty entry.
|
|
fn deallocate_cluster(&mut self, address: u64, zero_marker: bool) -> std::io::Result<()> {
|
|
if address >= self.virtual_size() {
|
|
return Err(std::io::Error::from_raw_os_error(EINVAL));
|
|
}
|
|
|
|
let l1_index = self.l1_table_index(address) as usize;
|
|
let l2_addr_disk = *self
|
|
.l1_table
|
|
.get(l1_index)
|
|
.ok_or_else(|| std::io::Error::from_raw_os_error(EINVAL))?;
|
|
let l2_index = self.l2_table_index(address) as usize;
|
|
let write_zero_marker = zero_marker && self.backing_file.is_some();
|
|
let dealloc_entry = if write_zero_marker {
|
|
l2_entry_make_zero_plain()
|
|
} else {
|
|
0
|
|
};
|
|
|
|
if l2_addr_disk == 0 {
|
|
// With a backing file, an empty L2 entry means "consult backing".
|
|
// WRITE_ZEROES needs a logical-zero marker instead.
|
|
if write_zero_marker {
|
|
if let Some(new_addr) = self.cache_l2_cluster(l1_index, l2_addr_disk, true)? {
|
|
self.set_cluster_refcount_track_freed(new_addr, 1)?;
|
|
}
|
|
self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = dealloc_entry;
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
self.cache_l2_cluster(l1_index, l2_addr_disk, false)?;
|
|
|
|
let l2_entry = self.l2_cache.get(l1_index).unwrap()[l2_index];
|
|
if l2_entry_is_empty(l2_entry) {
|
|
// With a backing file, empty means "consult backing"; preserve
|
|
// WRITE_ZEROES semantics with an explicit zero marker.
|
|
if write_zero_marker {
|
|
self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = dealloc_entry;
|
|
}
|
|
return Ok(());
|
|
}
|
|
// Compressed clusters cannot use the zero flag optimization, thus fully deallocate instead.
|
|
// Their layout may also use bit 0, so classify them before zero-flagged standard entries.
|
|
if l2_entry_is_compressed(l2_entry) {
|
|
self.deallocate_compressed_cluster(l2_entry)?;
|
|
self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = dealloc_entry;
|
|
return Ok(());
|
|
}
|
|
if l2_entry_is_zero(l2_entry) {
|
|
return Ok(());
|
|
}
|
|
|
|
let cluster_addr = l2_entry_std_cluster_addr(l2_entry);
|
|
|
|
// Decrement the refcount.
|
|
let refcount = self
|
|
.refcounts
|
|
.get_cluster_refcount(&mut self.raw_file, cluster_addr)
|
|
.map_err(|e| {
|
|
if matches!(e, refcount::Error::RefblockUnaligned(_)) {
|
|
self.set_corrupt_bit_best_effort();
|
|
}
|
|
io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
format!("failed to get cluster refcount: {e}"),
|
|
)
|
|
})?;
|
|
if refcount == 0 {
|
|
return Err(std::io::Error::from_raw_os_error(EINVAL));
|
|
}
|
|
|
|
if self.sparse {
|
|
// Fully deallocate to reclaim storage space.
|
|
let new_refcount = refcount - 1;
|
|
self.set_cluster_refcount_track_freed(cluster_addr, new_refcount)?;
|
|
|
|
// Rewrite the L2 entry to remove the cluster mapping (full deallocation).
|
|
self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = dealloc_entry;
|
|
|
|
if new_refcount == 0 {
|
|
let cluster_size = self.raw_file.cluster_size();
|
|
// This cluster is no longer in use; deallocate the storage.
|
|
// The underlying FS may not support FALLOC_FL_PUNCH_HOLE,
|
|
// so don't treat an error as fatal. Future reads will return zeros anyways.
|
|
let _ = self
|
|
.raw_file
|
|
.file_mut()
|
|
.punch_hole(cluster_addr, cluster_size);
|
|
self.unref_clusters.push(cluster_addr);
|
|
}
|
|
} else {
|
|
// Zero flag optimization - mark cluster as reading zeros without deallocating.
|
|
// Only safe if refcount == 1 (no other references to this cluster).
|
|
if refcount == 1 {
|
|
// Single reference - safe to use zero flag optimization
|
|
self.l2_cache.get_mut(l1_index).unwrap()[l2_index] =
|
|
l2_entry_make_zero(cluster_addr);
|
|
} else {
|
|
// Multiple references - must decrement refcount and unmap this entry.
|
|
// Cannot use zero flag because other L2 entries still need the real data.
|
|
self.set_cluster_refcount_track_freed(cluster_addr, refcount - 1)?;
|
|
self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = dealloc_entry;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn deallocate_bytes(&mut self, address: u64, length: usize) -> std::io::Result<()> {
|
|
self.deallocate_bytes_impl(address, length, false)
|
|
}
|
|
|
|
// Apply WRITE_ZEROES semantics for `length` bytes starting at `address`.
|
|
fn write_zeroes_bytes(&mut self, address: u64, length: usize) -> std::io::Result<()> {
|
|
self.deallocate_bytes_impl(address, length, true)
|
|
}
|
|
|
|
fn deallocate_bytes_impl(
|
|
&mut self,
|
|
address: u64,
|
|
length: usize,
|
|
zero_marker: bool,
|
|
) -> std::io::Result<()> {
|
|
let write_count: usize = self.limit_range_file(address, length);
|
|
|
|
let mut nwritten: usize = 0;
|
|
while nwritten < write_count {
|
|
let curr_addr = address + nwritten as u64;
|
|
let count = self.limit_range_cluster(curr_addr, write_count - nwritten);
|
|
|
|
if count == self.raw_file.cluster_size() as usize {
|
|
// Full cluster - deallocate the storage.
|
|
self.deallocate_cluster(curr_addr, zero_marker)?;
|
|
} else {
|
|
// Partial cluster - zero out the relevant bytes if it was allocated.
|
|
// Any space in unallocated clusters can be left alone, since
|
|
// unallocated clusters already read back as zeroes.
|
|
let offset = self.file_offset_write(curr_addr)?;
|
|
// Partial cluster - zero it out.
|
|
self.raw_file.file_mut().write_zeroes_at(offset, count)?;
|
|
}
|
|
|
|
nwritten += count;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// Reads an L2 cluster from the disk, returning an error if the file can't be read or if any
|
|
// cluster is compressed.
|
|
fn read_l2_cluster(raw_file: &mut QcowRawFile, cluster_addr: u64) -> std::io::Result<Vec<u64>> {
|
|
let l2_table = raw_file.read_pointer_cluster(cluster_addr, None)?;
|
|
Ok(l2_table)
|
|
}
|
|
|
|
// Put an L2 cluster to the cache with evicting less-used cluster
|
|
// The new cluster may be allocated if necessary
|
|
// (may_alloc argument is true and l2_addr_disk == 0)
|
|
fn cache_l2_cluster(
|
|
&mut self,
|
|
l1_index: usize,
|
|
l2_addr_disk: u64,
|
|
may_alloc: bool,
|
|
) -> std::io::Result<Option<u64>> {
|
|
let mut new_cluster: Option<u64> = None;
|
|
if !self.l2_cache.contains_key(l1_index) {
|
|
// Not in the cache.
|
|
let l2_table = if may_alloc && l2_addr_disk == 0 {
|
|
// Allocate a new cluster to store the L2 table and update the L1 table to point
|
|
// to the new table.
|
|
let new_addr: u64 = self.get_new_cluster(None)?;
|
|
new_cluster = Some(new_addr);
|
|
self.l1_table[l1_index] = new_addr;
|
|
VecCache::new(self.l2_entries as usize)
|
|
} else {
|
|
let cluster_size = self.raw_file.cluster_size();
|
|
if l2_addr_disk & (cluster_size - 1) != 0 {
|
|
self.set_corrupt_bit_best_effort();
|
|
return Err(io::Error::from_raw_os_error(EIO));
|
|
}
|
|
VecCache::from_vec(Self::read_l2_cluster(&mut self.raw_file, l2_addr_disk)?)
|
|
};
|
|
let l1_table = &self.l1_table;
|
|
let raw_file = &mut self.raw_file;
|
|
self.l2_cache.insert(l1_index, l2_table, |index, evicted| {
|
|
raw_file.write_pointer_table_direct(l1_table[index], evicted.iter())
|
|
})?;
|
|
}
|
|
Ok(new_cluster)
|
|
}
|
|
|
|
// Set the refcount for a cluster and add any unreferenced clusters to the unref list.
|
|
fn set_cluster_refcount_track_freed(
|
|
&mut self,
|
|
address: u64,
|
|
refcount: u64,
|
|
) -> std::io::Result<()> {
|
|
let mut newly_unref = self.set_cluster_refcount(address, refcount)?;
|
|
self.unref_clusters.append(&mut newly_unref);
|
|
Ok(())
|
|
}
|
|
|
|
// Set the refcount for a cluster with the given address.
|
|
// Returns a list of any refblocks that can be reused, this happens when a refblock is moved,
|
|
// the old location can be reused.
|
|
fn set_cluster_refcount(&mut self, address: u64, refcount: u64) -> std::io::Result<Vec<u64>> {
|
|
let mut added_clusters = Vec::new();
|
|
let mut unref_clusters = Vec::new();
|
|
let mut refcount_set = false;
|
|
let mut new_cluster = None;
|
|
|
|
while !refcount_set {
|
|
match self.refcounts.set_cluster_refcount(
|
|
&mut self.raw_file,
|
|
address,
|
|
refcount,
|
|
new_cluster.take(),
|
|
) {
|
|
Ok(None) => {
|
|
refcount_set = true;
|
|
}
|
|
Ok(Some(freed_cluster)) => {
|
|
// Recursively set the freed refcount block's refcount to 0
|
|
let mut freed = self.set_cluster_refcount(freed_cluster, 0)?;
|
|
unref_clusters.append(&mut freed);
|
|
refcount_set = true;
|
|
}
|
|
Err(refcount::Error::EvictingRefCounts(e)) => {
|
|
return Err(e);
|
|
}
|
|
Err(refcount::Error::InvalidIndex) => {
|
|
self.set_corrupt_bit_best_effort();
|
|
return Err(std::io::Error::from_raw_os_error(EINVAL));
|
|
}
|
|
Err(refcount::Error::NeedCluster(addr)) => {
|
|
// Read the address and call set_cluster_refcount again.
|
|
new_cluster = Some((
|
|
addr,
|
|
VecCache::from_vec(self.raw_file.read_refcount_block(addr)?),
|
|
));
|
|
}
|
|
Err(refcount::Error::NeedNewCluster) => {
|
|
// Allocate the cluster and call set_cluster_refcount again.
|
|
let addr = self.get_new_cluster(None)?;
|
|
added_clusters.push(addr);
|
|
new_cluster = Some((
|
|
addr,
|
|
VecCache::new(self.refcounts.refcounts_per_block() as usize),
|
|
));
|
|
}
|
|
Err(refcount::Error::ReadingRefCounts(e)) => {
|
|
return Err(e);
|
|
}
|
|
Err(refcount::Error::RefcountOverflow { .. }) => {
|
|
return Err(std::io::Error::from_raw_os_error(EINVAL));
|
|
}
|
|
Err(refcount::Error::RefblockUnaligned(_)) => {
|
|
self.set_corrupt_bit_best_effort();
|
|
return Err(io::Error::from_raw_os_error(EIO));
|
|
}
|
|
}
|
|
}
|
|
|
|
for addr in added_clusters {
|
|
self.set_cluster_refcount(addr, 1)?;
|
|
}
|
|
Ok(unref_clusters)
|
|
}
|
|
|
|
fn sync_caches(&mut self) -> std::io::Result<()> {
|
|
// Write out all dirty L2 tables.
|
|
for (l1_index, l2_table) in self.l2_cache.iter_mut().filter(|(_k, v)| v.dirty()) {
|
|
// The index must be valid from when we inserted it.
|
|
let addr = self.l1_table[*l1_index];
|
|
if addr != 0 {
|
|
self.raw_file
|
|
.write_pointer_table_direct(addr, l2_table.iter())?;
|
|
} else {
|
|
self.set_corrupt_bit_best_effort();
|
|
return Err(std::io::Error::from_raw_os_error(EINVAL));
|
|
}
|
|
l2_table.mark_clean();
|
|
}
|
|
// Write the modified refcount blocks.
|
|
self.refcounts.flush_blocks(&mut self.raw_file)?;
|
|
// Make sure metadata(file len) and all data clusters are written.
|
|
self.raw_file.file_mut().sync_all()?;
|
|
|
|
// Push L1 table and refcount table last as all the clusters they point to are now
|
|
// guaranteed to be valid.
|
|
let mut sync_required = if self.l1_table.dirty() {
|
|
// Write L1 table with OFLAG_COPIED bits
|
|
let refcounts = &mut self.refcounts;
|
|
self.raw_file.write_pointer_table(
|
|
self.header.l1_table_offset,
|
|
self.l1_table.iter(),
|
|
|raw_file, l2_addr| {
|
|
if l2_addr == 0 {
|
|
Ok(0)
|
|
} else {
|
|
let refcount = refcounts
|
|
.get_cluster_refcount(raw_file, l2_addr)
|
|
.map_err(|e| std::io::Error::other(Error::GettingRefcount(e)))?;
|
|
Ok(l1_entry_make(l2_addr, refcount == 1))
|
|
}
|
|
},
|
|
)?;
|
|
self.l1_table.mark_clean();
|
|
true
|
|
} else {
|
|
false
|
|
};
|
|
sync_required |= self.refcounts.flush_table(&mut self.raw_file)?;
|
|
if sync_required {
|
|
self.raw_file.file_mut().sync_data()?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl AsRawFd for QcowFile {
|
|
fn as_raw_fd(&self) -> RawFd {
|
|
self.raw_file.as_raw_fd()
|
|
}
|
|
}
|
|
|
|
impl Drop for QcowFile {
|
|
fn drop(&mut self) {
|
|
let _ = self.sync_caches();
|
|
if self.raw_file.file().is_writable() {
|
|
let _ = self.header.set_dirty_bit(self.raw_file.file_mut(), false);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Read for QcowFile {
|
|
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
|
let address: u64 = self.current_offset;
|
|
let read_count: usize = self.limit_range_file(address, buf.len());
|
|
|
|
let mut nread: usize = 0;
|
|
while nread < read_count {
|
|
let curr_addr = address + nread as u64;
|
|
let count = self.limit_range_cluster(curr_addr, read_count - nread);
|
|
|
|
if (self.file_read(curr_addr, count, &mut buf[nread..(nread + count)])?).is_some() {
|
|
// Data is successfully read from the cluster
|
|
} else if let Some(backing) = self.backing_file.as_mut() {
|
|
backing.read_at(curr_addr, &mut buf[nread..(nread + count)])?;
|
|
} else {
|
|
// Previously unwritten region, return zeros
|
|
buf[nread..(nread + count)].fill(0);
|
|
}
|
|
|
|
nread += count;
|
|
}
|
|
self.current_offset += read_count as u64;
|
|
Ok(read_count)
|
|
}
|
|
}
|
|
|
|
impl Seek for QcowFile {
|
|
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
|
|
let new_offset: Option<u64> = match pos {
|
|
SeekFrom::Start(off) => Some(off),
|
|
SeekFrom::End(off) => {
|
|
if off < 0 {
|
|
0i64.checked_sub(off)
|
|
.and_then(|increment| self.virtual_size().checked_sub(increment as u64))
|
|
} else {
|
|
self.virtual_size().checked_add(off as u64)
|
|
}
|
|
}
|
|
SeekFrom::Current(off) => {
|
|
if off < 0 {
|
|
0i64.checked_sub(off)
|
|
.and_then(|increment| self.current_offset.checked_sub(increment as u64))
|
|
} else {
|
|
self.current_offset.checked_add(off as u64)
|
|
}
|
|
}
|
|
};
|
|
|
|
if let Some(o) = new_offset
|
|
&& o <= self.virtual_size()
|
|
{
|
|
self.current_offset = o;
|
|
return Ok(o);
|
|
}
|
|
Err(std::io::Error::from_raw_os_error(EINVAL))
|
|
}
|
|
}
|
|
|
|
impl Write for QcowFile {
|
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
|
let address: u64 = self.current_offset;
|
|
let write_count: usize = self.limit_range_file(address, buf.len());
|
|
|
|
let mut nwritten: usize = 0;
|
|
while nwritten < write_count {
|
|
let curr_addr = address + nwritten as u64;
|
|
let offset = self.file_offset_write(curr_addr)?;
|
|
let count = self.limit_range_cluster(curr_addr, write_count - nwritten);
|
|
|
|
self.raw_file.file_mut().seek(SeekFrom::Start(offset))?;
|
|
let count = self
|
|
.raw_file
|
|
.file_mut()
|
|
.write(&buf[nwritten..(nwritten + count)])?;
|
|
|
|
nwritten += count;
|
|
}
|
|
self.current_offset += write_count as u64;
|
|
Ok(write_count)
|
|
}
|
|
|
|
fn flush(&mut self) -> std::io::Result<()> {
|
|
self.sync_caches()?;
|
|
self.avail_clusters.append(&mut self.unref_clusters);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl FileSync for QcowFile {
|
|
fn fsync(&mut self) -> std::io::Result<()> {
|
|
self.flush()
|
|
}
|
|
}
|
|
|
|
impl FileSetLen for QcowFile {
|
|
fn set_len(&self, _len: u64) -> std::io::Result<()> {
|
|
Err(std::io::Error::other(
|
|
"set_len() not supported for QcowFile",
|
|
))
|
|
}
|
|
}
|
|
|
|
impl PunchHole for QcowFile {
|
|
fn punch_hole(&mut self, offset: u64, length: u64) -> std::io::Result<()> {
|
|
let mut remaining = length;
|
|
let mut offset = offset;
|
|
while remaining > 0 {
|
|
let chunk_length = min(remaining, usize::MAX as u64) as usize;
|
|
self.deallocate_bytes(offset, chunk_length)?;
|
|
remaining -= chunk_length as u64;
|
|
offset += chunk_length as u64;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl WriteZeroesAt for QcowFile {
|
|
fn write_zeroes_at(&mut self, offset: u64, length: usize) -> io::Result<usize> {
|
|
self.write_zeroes_bytes(offset, length)?;
|
|
Ok(length)
|
|
}
|
|
}
|
|
|
|
impl SeekHole for QcowFile {
|
|
fn seek_hole(&mut self, offset: u64) -> io::Result<Option<u64>> {
|
|
match self.find_allocated_cluster(offset, false) {
|
|
Err(e) => Err(e),
|
|
Ok(None) => {
|
|
if offset < self.virtual_size() {
|
|
Ok(Some(self.seek(SeekFrom::End(0))?))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
Ok(Some(o)) => {
|
|
self.seek(SeekFrom::Start(o))?;
|
|
Ok(Some(o))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn seek_data(&mut self, offset: u64) -> io::Result<Option<u64>> {
|
|
match self.find_allocated_cluster(offset, true) {
|
|
Err(e) => Err(e),
|
|
Ok(None) => Ok(None),
|
|
Ok(Some(o)) => {
|
|
self.seek(SeekFrom::Start(o))?;
|
|
Ok(Some(o))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl BlockBackend for QcowFile {
|
|
fn logical_size(&self) -> std::result::Result<u64, crate::Error> {
|
|
Ok(self.virtual_size())
|
|
}
|
|
|
|
fn physical_size(&self) -> std::result::Result<u64, crate::Error> {
|
|
self.raw_file
|
|
.physical_size()
|
|
.map_err(crate::Error::GetFileMetadata)
|
|
}
|
|
}
|
|
|
|
fn convert_copy<R, W>(reader: &mut R, writer: &mut W, offset: u64, size: u64) -> BlockResult<()>
|
|
where
|
|
R: Read + Seek,
|
|
W: Write + Seek,
|
|
{
|
|
const CHUNK_SIZE: usize = 65536;
|
|
let mut buf = [0; CHUNK_SIZE];
|
|
let mut read_count = 0;
|
|
reader
|
|
.seek(SeekFrom::Start(offset))
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?;
|
|
writer
|
|
.seek(SeekFrom::Start(offset))
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?;
|
|
loop {
|
|
let this_count = min(CHUNK_SIZE as u64, size - read_count) as usize;
|
|
let nread = reader
|
|
.read(&mut buf[..this_count])
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ReadingData(e)))?;
|
|
writer
|
|
.write(&buf[..nread])
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::WritingData(e)))?;
|
|
read_count += nread as u64;
|
|
if nread == 0 || read_count == size {
|
|
break;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn convert_reader_writer<R, W>(reader: &mut R, writer: &mut W, size: u64) -> BlockResult<()>
|
|
where
|
|
R: Read + Seek + SeekHole,
|
|
W: Write + Seek,
|
|
{
|
|
let mut offset = 0;
|
|
while offset < size {
|
|
// Find the next range of data.
|
|
let next_data = match reader
|
|
.seek_data(offset)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?
|
|
{
|
|
Some(o) => o,
|
|
None => {
|
|
// No more data in the file.
|
|
break;
|
|
}
|
|
};
|
|
let next_hole = match reader
|
|
.seek_hole(next_data)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?
|
|
{
|
|
Some(o) => o,
|
|
None => {
|
|
// This should not happen - there should always be at least one hole
|
|
// after any data.
|
|
return Err(BlockError::new(
|
|
BlockErrorKind::Io,
|
|
Error::SeekingFile(io::Error::from_raw_os_error(EINVAL)),
|
|
));
|
|
}
|
|
};
|
|
let count = next_hole - next_data;
|
|
convert_copy(reader, writer, next_data, count)?;
|
|
offset = next_hole;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn convert_reader<R>(reader: &mut R, dst_file: RawFile, dst_type: ImageType) -> BlockResult<()>
|
|
where
|
|
R: Read + Seek + SeekHole,
|
|
{
|
|
let src_size = reader
|
|
.seek(SeekFrom::End(0))
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?;
|
|
reader
|
|
.rewind()
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?;
|
|
|
|
// Ensure the destination file is empty before writing to it.
|
|
dst_file
|
|
.set_len(0)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SettingFileSize(e)))?;
|
|
|
|
match dst_type {
|
|
ImageType::Qcow2 => {
|
|
let mut dst_writer = QcowFile::new(dst_file, 3, src_size, true)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
|
|
convert_reader_writer(reader, &mut dst_writer, src_size)
|
|
}
|
|
ImageType::Raw => {
|
|
let mut dst_writer = dst_file;
|
|
// Set the length of the destination file to convert it into a sparse file
|
|
// of the desired size.
|
|
dst_writer
|
|
.set_len(src_size)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SettingFileSize(e)))?;
|
|
convert_reader_writer(reader, &mut dst_writer, src_size)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Copy the contents of a disk image in `src_file` into `dst_file`.
|
|
/// The type of `src_file` is automatically detected, and the output file type is
|
|
/// determined by `dst_type`.
|
|
pub fn convert(
|
|
mut src_file: RawFile,
|
|
dst_file: RawFile,
|
|
dst_type: ImageType,
|
|
src_max_nesting_depth: u32,
|
|
) -> BlockResult<()> {
|
|
let src_type = detect_image_type(&mut src_file)?;
|
|
match src_type {
|
|
ImageType::Qcow2 => {
|
|
let mut src_reader =
|
|
QcowFile::from_with_nesting_depth(src_file, src_max_nesting_depth, true)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
|
|
convert_reader(&mut src_reader, dst_file, dst_type)
|
|
}
|
|
ImageType::Raw => {
|
|
// src_file is a raw file.
|
|
let mut src_reader = src_file;
|
|
convert_reader(&mut src_reader, dst_file, dst_type)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Detect the type of an image file by checking for a valid qcow2 header.
|
|
pub fn detect_image_type(file: &mut RawFile) -> BlockResult<ImageType> {
|
|
let orig_seek = file
|
|
.stream_position()
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?;
|
|
file.rewind()
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?;
|
|
let magic = u32::read_be(file)
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ReadingHeader(e)))?;
|
|
let image_type = if magic == QCOW_MAGIC {
|
|
ImageType::Qcow2
|
|
} else {
|
|
ImageType::Raw
|
|
};
|
|
file.seek(SeekFrom::Start(orig_seek))
|
|
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?;
|
|
Ok(image_type)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod unit_tests {
|
|
use std::error::Error as StdError;
|
|
use std::fs::File;
|
|
use std::path::Path;
|
|
|
|
use vmm_sys_util::tempdir::TempDir;
|
|
use vmm_sys_util::tempfile::TempFile;
|
|
use vmm_sys_util::write_zeroes::WriteZeroes;
|
|
|
|
use super::header::DEFAULT_CLUSTER_BITS;
|
|
use super::util::{COMPRESSED_FLAG, ZERO_FLAG};
|
|
use super::*;
|
|
use crate::formats::qcow::common::unit_tests::compress_allocated_clusters;
|
|
|
|
fn valid_header_v3() -> Vec<u8> {
|
|
vec![
|
|
0x51u8, 0x46, 0x49, 0xfb, // magic
|
|
0x00, 0x00, 0x00, 0x03, // version
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // backing file offset
|
|
0x00, 0x00, 0x00, 0x00, // backing file size
|
|
0x00, 0x00, 0x00, 0x10, // cluster_bits
|
|
0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, // size
|
|
0x00, 0x00, 0x00, 0x00, // crypt method
|
|
0x00, 0x00, 0x01, 0x00, // L1 size
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, // L1 table offset
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, // refcount table offset
|
|
0x00, 0x00, 0x00, 0x03, // refcount table clusters
|
|
0x00, 0x00, 0x00, 0x00, // nb snapshots
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, // snapshots offset
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // incompatible_features
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // compatible_features
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // autoclear_features
|
|
0x00, 0x00, 0x00, 0x04, // refcount_order
|
|
0x00, 0x00, 0x00, 0x68, // header_length
|
|
]
|
|
}
|
|
|
|
fn valid_header_v2() -> Vec<u8> {
|
|
vec![
|
|
0x51u8, 0x46, 0x49, 0xfb, // magic
|
|
0x00, 0x00, 0x00, 0x02, // version
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // backing file offset
|
|
0x00, 0x00, 0x00, 0x00, // backing file size
|
|
0x00, 0x00, 0x00, 0x10, // cluster_bits
|
|
0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, // size
|
|
0x00, 0x00, 0x00, 0x00, // crypt method
|
|
0x00, 0x00, 0x01, 0x00, // L1 size
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, // L1 table offset
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, // refcount table offset
|
|
0x00, 0x00, 0x00, 0x03, // refcount table clusters
|
|
0x00, 0x00, 0x00, 0x00, // nb snapshots
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, // snapshots offset
|
|
]
|
|
}
|
|
|
|
// Test case found by clusterfuzz to allocate excessive memory.
|
|
fn test_huge_header() -> Vec<u8> {
|
|
vec![
|
|
0x51, 0x46, 0x49, 0xfb, // magic
|
|
0x00, 0x00, 0x00, 0x03, // version
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // backing file offset
|
|
0x00, 0x00, 0x00, 0x00, // backing file size
|
|
0x00, 0x00, 0x00, 0x09, // cluster_bits
|
|
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, // size
|
|
0x00, 0x00, 0x00, 0x00, // crypt method
|
|
0x00, 0x00, 0x01, 0x00, // L1 size
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, // L1 table offset
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, // refcount table offset
|
|
0x00, 0x00, 0x00, 0x03, // refcount table clusters
|
|
0x00, 0x00, 0x00, 0x00, // nb snapshots
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, // snapshots offset
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // incompatible_features
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // compatible_features
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // autoclear_features
|
|
0x00, 0x00, 0x00, 0x04, // refcount_order
|
|
0x00, 0x00, 0x00, 0x68, // header_length
|
|
]
|
|
}
|
|
|
|
fn basic_file(header: &[u8]) -> RawFile {
|
|
let mut disk_file: RawFile = RawFile::new(TempFile::new().unwrap().into_file(), false);
|
|
disk_file.write_all(header).unwrap();
|
|
disk_file.set_len(0x1_0000_0000).unwrap();
|
|
disk_file.rewind().unwrap();
|
|
disk_file
|
|
}
|
|
|
|
fn with_basic_file<F>(header: &[u8], mut testfn: F)
|
|
where
|
|
F: FnMut(RawFile),
|
|
{
|
|
testfn(basic_file(header)); // File closed when the function exits.
|
|
}
|
|
|
|
fn with_default_file<F>(file_size: u64, direct: bool, mut testfn: F)
|
|
where
|
|
F: FnMut(QcowFile),
|
|
{
|
|
let tmp: RawFile = RawFile::new(TempFile::new().unwrap().into_file(), direct);
|
|
let qcow_file = QcowFile::new(tmp, 3, file_size, true).unwrap();
|
|
|
|
testfn(qcow_file); // File closed when the function exits.
|
|
}
|
|
|
|
fn header_with_cluster_bits(cluster_bits: u32, size: u64) -> QcowHeader {
|
|
let mut header = QcowHeader::create_for_size_and_path(3, size, None).unwrap();
|
|
let cluster_size = 1u32 << cluster_bits;
|
|
let entries_per_cluster = cluster_size / std::mem::size_of::<u64>() as u32;
|
|
let num_clusters = div_round_up_u64(size, u64::from(cluster_size)) as u32;
|
|
let num_l2_clusters = div_round_up_u32(num_clusters, entries_per_cluster);
|
|
let l1_clusters = div_round_up_u32(num_l2_clusters, entries_per_cluster);
|
|
let header_clusters =
|
|
div_round_up_u32(std::mem::size_of::<QcowHeader>() as u32, cluster_size);
|
|
let max_refcount_clusters = max_refcount_clusters(
|
|
DEFAULT_REFCOUNT_ORDER,
|
|
cluster_size,
|
|
num_clusters + l1_clusters + num_l2_clusters + header_clusters,
|
|
) as u32;
|
|
|
|
header.cluster_bits = cluster_bits;
|
|
header.l1_size = num_l2_clusters;
|
|
header.l1_table_offset = u64::from(cluster_size);
|
|
header.refcount_table_offset = u64::from(cluster_size * (l1_clusters + 1));
|
|
header.refcount_table_clusters = div_round_up_u32(
|
|
max_refcount_clusters * std::mem::size_of::<u64>() as u32,
|
|
cluster_size,
|
|
);
|
|
header
|
|
}
|
|
|
|
fn qcow_file_with_cluster_bits(cluster_bits: u32, size: u64) -> QcowFile {
|
|
let disk_file = RawFile::new(TempFile::new().unwrap().into_file(), false);
|
|
let header = header_with_cluster_bits(cluster_bits, size);
|
|
QcowFile::new_from_header(disk_file, &header, true).unwrap()
|
|
}
|
|
|
|
fn qcow_overlay_with_backing_pattern(offset: u64, len: usize, value: u8) -> QcowFile {
|
|
qcow_overlay_with_backing_pattern_and_cluster_bits(offset, len, value, 16)
|
|
}
|
|
|
|
fn qcow_overlay_with_backing_pattern_and_cluster_bits(
|
|
offset: u64,
|
|
len: usize,
|
|
value: u8,
|
|
cluster_bits: u32,
|
|
) -> QcowFile {
|
|
let cluster_size = 1u64 << cluster_bits;
|
|
let size = (offset + len as u64).max(cluster_size * 4);
|
|
let mut backing = qcow_file_with_cluster_bits(cluster_bits, size);
|
|
let backing_data = vec![value; len];
|
|
backing.seek(SeekFrom::Start(offset)).unwrap();
|
|
backing.write_all(&backing_data).unwrap();
|
|
backing.flush().unwrap();
|
|
|
|
let mut overlay = qcow_file_with_cluster_bits(cluster_bits, size);
|
|
overlay.set_backing_file(Some(Box::new(backing)));
|
|
overlay
|
|
}
|
|
|
|
#[test]
|
|
fn write_read_start_backing_v2() {
|
|
let disk_file = basic_file(&valid_header_v2());
|
|
let mut backing = QcowFile::from(disk_file).unwrap();
|
|
backing
|
|
.write_all(b"test first bytes")
|
|
.expect("Failed to write test string.");
|
|
let mut buf = [0u8; 4];
|
|
let wrapping_disk_file = basic_file(&valid_header_v2());
|
|
let mut wrapping = QcowFile::from(wrapping_disk_file).unwrap();
|
|
wrapping.set_backing_file(Some(Box::new(backing)));
|
|
wrapping.seek(SeekFrom::Start(0)).expect("Failed to seek.");
|
|
wrapping.read_exact(&mut buf).expect("Failed to read.");
|
|
assert_eq!(&buf, b"test");
|
|
}
|
|
|
|
#[test]
|
|
fn write_read_start_backing_v3() {
|
|
let disk_file = basic_file(&valid_header_v3());
|
|
let mut backing = QcowFile::from(disk_file).unwrap();
|
|
backing
|
|
.write_all(b"test first bytes")
|
|
.expect("Failed to write test string.");
|
|
let mut buf = [0u8; 4];
|
|
let wrapping_disk_file = basic_file(&valid_header_v3());
|
|
let mut wrapping = QcowFile::from(wrapping_disk_file).unwrap();
|
|
wrapping.set_backing_file(Some(Box::new(backing)));
|
|
wrapping.seek(SeekFrom::Start(0)).expect("Failed to seek.");
|
|
wrapping.read_exact(&mut buf).expect("Failed to read.");
|
|
assert_eq!(&buf, b"test");
|
|
}
|
|
|
|
#[test]
|
|
fn default_header_v2() {
|
|
let header = QcowHeader::create_for_size_and_path(2, 0x10_0000, None);
|
|
let mut disk_file: RawFile = RawFile::new(TempFile::new().unwrap().into_file(), false);
|
|
header
|
|
.expect("Failed to create header.")
|
|
.write_to(&mut disk_file)
|
|
.expect("Failed to write header to temporary file.");
|
|
disk_file.rewind().unwrap();
|
|
QcowFile::from(disk_file).expect("Failed to create Qcow from default Header");
|
|
}
|
|
|
|
#[test]
|
|
fn default_header_v3() {
|
|
let header = QcowHeader::create_for_size_and_path(3, 0x10_0000, None);
|
|
let mut disk_file: RawFile = RawFile::new(TempFile::new().unwrap().into_file(), false);
|
|
header
|
|
.expect("Failed to create header.")
|
|
.write_to(&mut disk_file)
|
|
.expect("Failed to write header to temporary file.");
|
|
disk_file.rewind().unwrap();
|
|
QcowFile::from(disk_file).expect("Failed to create Qcow from default Header");
|
|
}
|
|
|
|
#[test]
|
|
fn header_read() {
|
|
with_basic_file(&valid_header_v2(), |mut disk_file: RawFile| {
|
|
let header = QcowHeader::new(&mut disk_file).expect("Failed to create Header.");
|
|
assert_eq!(header.version, 2);
|
|
assert_eq!(header.refcount_order, DEFAULT_REFCOUNT_ORDER);
|
|
assert_eq!(header.header_size, V2_BARE_HEADER_SIZE);
|
|
});
|
|
with_basic_file(&valid_header_v3(), |mut disk_file: RawFile| {
|
|
let header = QcowHeader::new(&mut disk_file).expect("Failed to create Header.");
|
|
assert_eq!(header.version, 3);
|
|
assert_eq!(header.refcount_order, DEFAULT_REFCOUNT_ORDER);
|
|
assert_eq!(header.header_size, V3_BARE_HEADER_SIZE);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn header_v2_with_backing() {
|
|
let header = QcowHeader::create_for_size_and_path(2, 0x10_0000, Some("/my/path/to/a/file"))
|
|
.expect("Failed to create header.");
|
|
let mut disk_file: RawFile = RawFile::new(TempFile::new().unwrap().into_file(), false);
|
|
header
|
|
.write_to(&mut disk_file)
|
|
.expect("Failed to write header to shm.");
|
|
disk_file.rewind().unwrap();
|
|
let read_header = QcowHeader::new(&mut disk_file).expect("Failed to create header.");
|
|
assert_eq!(
|
|
header.backing_file.as_ref().map(|bf| bf.path.clone()),
|
|
Some(String::from("/my/path/to/a/file"))
|
|
);
|
|
assert_eq!(
|
|
read_header.backing_file.as_ref().map(|bf| &bf.path),
|
|
header.backing_file.as_ref().map(|bf| &bf.path)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn header_v3_with_backing() {
|
|
let header = QcowHeader::create_for_size_and_path(3, 0x10_0000, Some("/my/path/to/a/file"))
|
|
.expect("Failed to create header.");
|
|
let mut disk_file: RawFile = RawFile::new(TempFile::new().unwrap().into_file(), false);
|
|
header
|
|
.write_to(&mut disk_file)
|
|
.expect("Failed to write header to shm.");
|
|
disk_file.rewind().unwrap();
|
|
let read_header = QcowHeader::new(&mut disk_file).expect("Failed to create header.");
|
|
assert_eq!(
|
|
header.backing_file.as_ref().map(|bf| bf.path.clone()),
|
|
Some(String::from("/my/path/to/a/file"))
|
|
);
|
|
assert_eq!(
|
|
read_header.backing_file.as_ref().map(|bf| &bf.path),
|
|
header.backing_file.as_ref().map(|bf| &bf.path)
|
|
);
|
|
}
|
|
|
|
/// Write a header to a fresh file with backing_file_offset and
|
|
/// backing_file_size patched. Panics on setup failures, returns
|
|
/// the parse result of the patched header.
|
|
fn read_header_with_patched_backing(offset: u64, size: u32) -> Result<QcowHeader> {
|
|
let mut header = QcowHeader::create_for_size_and_path(3, 0x10_0000, None)
|
|
.expect("Failed to create header.");
|
|
header.backing_file_offset = offset;
|
|
header.backing_file_size = size;
|
|
let mut disk_file: RawFile = RawFile::new(
|
|
TempFile::new()
|
|
.expect("Failed to create temp file.")
|
|
.into_file(),
|
|
false,
|
|
);
|
|
header
|
|
.write_to(&mut disk_file)
|
|
.expect("Failed to write header.");
|
|
disk_file.rewind().expect("Failed to rewind disk file.");
|
|
QcowHeader::new(&mut disk_file)
|
|
}
|
|
|
|
#[test]
|
|
fn backing_file_offset_at_cluster_boundary() {
|
|
let cluster_size = 1u64 << DEFAULT_CLUSTER_BITS;
|
|
let err = read_header_with_patched_backing(cluster_size, 1).unwrap_err();
|
|
assert!(matches!(
|
|
err,
|
|
Error::BackingFileOutsideFirstCluster(_, _, _)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn backing_file_offset_past_cluster() {
|
|
let cluster_size = 1u64 << DEFAULT_CLUSTER_BITS;
|
|
let err = read_header_with_patched_backing(cluster_size + 4096, 16).unwrap_err();
|
|
assert!(matches!(
|
|
err,
|
|
Error::BackingFileOutsideFirstCluster(_, _, _)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn backing_file_end_past_cluster() {
|
|
let cluster_size = 1u64 << DEFAULT_CLUSTER_BITS;
|
|
let err = read_header_with_patched_backing(cluster_size - 4, 16).unwrap_err();
|
|
assert!(matches!(
|
|
err,
|
|
Error::BackingFileOutsideFirstCluster(_, _, _)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn backing_file_fits_at_cluster_end() {
|
|
let cluster_size = 1u64 << DEFAULT_CLUSTER_BITS;
|
|
let header =
|
|
read_header_with_patched_backing(cluster_size - 16, 16).expect("Header should parse.");
|
|
assert_eq!(header.backing_file_offset, cluster_size - 16);
|
|
assert_eq!(header.backing_file_size, 16);
|
|
}
|
|
|
|
/// Helper to create a test file with header extensions
|
|
fn create_header_with_extension(ext_type: u32, ext_data: &[u8]) -> (RawFile, QcowHeader) {
|
|
let header = QcowHeader::create_for_size_and_path(3, 0x10_0000, None)
|
|
.expect("Failed to create header.");
|
|
|
|
let mut disk_file: RawFile = RawFile::new(TempFile::new().unwrap().into_file(), false);
|
|
header.write_to(&mut disk_file).unwrap();
|
|
|
|
// Write extension
|
|
disk_file
|
|
.seek(SeekFrom::Start(header.header_size as u64))
|
|
.unwrap();
|
|
u32::write_be(&mut disk_file, ext_type).unwrap();
|
|
u32::write_be(&mut disk_file, ext_data.len() as u32).unwrap();
|
|
disk_file.write_all(ext_data).unwrap();
|
|
|
|
// Add padding to 8-byte boundary
|
|
let padding = (8 - (ext_data.len() % 8)) % 8;
|
|
if padding > 0 {
|
|
disk_file.write_all(&vec![0u8; padding]).unwrap();
|
|
}
|
|
|
|
u32::write_be(&mut disk_file, HEADER_EXT_END).unwrap();
|
|
|
|
disk_file.rewind().unwrap();
|
|
|
|
(disk_file, header)
|
|
}
|
|
|
|
#[test]
|
|
fn read_header_extensions_unknown_extension() {
|
|
let (mut disk_file, mut header) = create_header_with_extension(
|
|
0x12345678, // unknown type
|
|
"test".as_bytes(),
|
|
);
|
|
|
|
// Extension parsing needs a backing file to set format on
|
|
header.backing_file = Some(BackingFileConfig {
|
|
path: "/test/backing".to_string(),
|
|
format: None,
|
|
});
|
|
|
|
QcowHeader::read_header_extensions(&mut disk_file, &mut header, None).unwrap();
|
|
assert_eq!(header.backing_file.as_ref().and_then(|bf| bf.format), None);
|
|
}
|
|
|
|
#[test]
|
|
fn read_header_extensions_raw_format() {
|
|
let (mut disk_file, mut header) =
|
|
create_header_with_extension(HEADER_EXT_BACKING_FORMAT, "raw".as_bytes());
|
|
|
|
header.backing_file = Some(BackingFileConfig {
|
|
path: "/test/backing".to_string(),
|
|
format: None,
|
|
});
|
|
|
|
QcowHeader::read_header_extensions(&mut disk_file, &mut header, None).unwrap();
|
|
assert_eq!(
|
|
header.backing_file.as_ref().and_then(|bf| bf.format),
|
|
Some(ImageType::Raw)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn read_header_extensions_qcow2_format() {
|
|
let (mut disk_file, mut header) =
|
|
create_header_with_extension(HEADER_EXT_BACKING_FORMAT, "qcow2".as_bytes());
|
|
|
|
header.backing_file = Some(BackingFileConfig {
|
|
path: "/test/backing".to_string(),
|
|
format: None,
|
|
});
|
|
|
|
QcowHeader::read_header_extensions(&mut disk_file, &mut header, None).unwrap();
|
|
assert_eq!(
|
|
header.backing_file.as_ref().and_then(|bf| bf.format),
|
|
Some(ImageType::Qcow2)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn read_header_extensions_invalid_format() {
|
|
let (mut disk_file, mut header) =
|
|
create_header_with_extension(HEADER_EXT_BACKING_FORMAT, "vmdk".as_bytes());
|
|
|
|
header.backing_file = Some(BackingFileConfig {
|
|
path: "/test/backing".to_string(),
|
|
format: None,
|
|
});
|
|
|
|
let result = QcowHeader::read_header_extensions(&mut disk_file, &mut header, None);
|
|
assert!(matches!(
|
|
result.unwrap_err(),
|
|
Error::UnsupportedBackingFileFormat(_)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn read_header_extensions_invalid_utf8() {
|
|
let (mut disk_file, mut header) = create_header_with_extension(
|
|
HEADER_EXT_BACKING_FORMAT,
|
|
&[0xFF, 0xFE, 0xFD], // invalid UTF-8
|
|
);
|
|
|
|
let result = QcowHeader::read_header_extensions(&mut disk_file, &mut header, None);
|
|
// Should fail with InvalidBackingFileName error
|
|
assert!(matches!(
|
|
result.unwrap_err(),
|
|
Error::InvalidBackingFileName(_)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn no_backing_file() {
|
|
// `backing_file` is `None`
|
|
let header = QcowHeader::create_for_size_and_path(3, 0x10_0000, None)
|
|
.expect("Failed to create header.");
|
|
let mut disk_file: RawFile = RawFile::new(TempFile::new().unwrap().into_file(), false);
|
|
header
|
|
.write_to(&mut disk_file)
|
|
.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, true).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn disable_backing_file() {
|
|
// `backing_file` is `Some`
|
|
let header =
|
|
QcowHeader::create_for_size_and_path(3, 0x10_0000, Some("/path/to/backing/file"))
|
|
.expect("Failed to create header.");
|
|
let mut disk_file: RawFile = RawFile::new(TempFile::new().unwrap().into_file(), false);
|
|
header
|
|
.write_to(&mut disk_file)
|
|
.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, true);
|
|
let err = res.unwrap_err();
|
|
assert!(matches!(err.kind(), BlockErrorKind::Overflow));
|
|
let source = StdError::source(&err).unwrap();
|
|
let qcow_err = source.downcast_ref::<Error>().unwrap();
|
|
assert!(matches!(qcow_err, Error::MaxNestingDepthExceeded));
|
|
}
|
|
|
|
/// Create a qcow2 file with itself as its backing file.
|
|
///
|
|
/// Without configuration `max_nesting_depth`, this will cause infinite recursion when loading
|
|
/// the file until stack overflow.
|
|
fn new_self_referential_qcow(path: &Path) -> Result<()> {
|
|
let header = QcowHeader::create_for_size_and_path(3, 0x10_0000, path.to_str())?;
|
|
let mut disk_file = RawFile::new(
|
|
File::create(path).expect("Failed to create image file."),
|
|
false,
|
|
);
|
|
header.write_to(&mut disk_file)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn max_nesting_backing() {
|
|
let test_dir = TempDir::new_with_prefix("/tmp/ch").unwrap();
|
|
let img_path = test_dir.as_path().join("test.img");
|
|
|
|
new_self_referential_qcow(img_path.as_path()).unwrap();
|
|
|
|
let err = QcowFile::from_with_nesting_depth(
|
|
RawFile::new(
|
|
File::open(img_path.as_path()).expect("Failed to open qcow image file"),
|
|
false,
|
|
),
|
|
MAX_NESTING_DEPTH,
|
|
true,
|
|
)
|
|
.expect_err("Opening qcow file with itself as backing file should fail.");
|
|
|
|
// This type of error is complex. For comparing easily, we can check if it contains the
|
|
// type name after formatting.
|
|
assert!(format!("{err:?}").contains(&format!("{:?}", Error::MaxNestingDepthExceeded)));
|
|
// This should recursively call the function ten times before throwing an error, and the
|
|
// error `BackingFileOpen` should also be repeated ten times.
|
|
assert_eq!(
|
|
format!("{err:?}")
|
|
.matches("BackingFileOpen")
|
|
.collect::<Vec<_>>()
|
|
.len() as u32,
|
|
MAX_NESTING_DEPTH,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_magic() {
|
|
let invalid_header = vec![0x51u8, 0x46, 0x4a, 0xfb];
|
|
with_basic_file(&invalid_header, |mut disk_file: RawFile| {
|
|
QcowHeader::new(&mut disk_file).expect_err("Invalid header worked.");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_refcount_order() {
|
|
let mut header = valid_header_v3();
|
|
header[99] = 7;
|
|
with_basic_file(&header, |disk_file: RawFile| {
|
|
QcowFile::from(disk_file).expect_err("Invalid refcount order worked.");
|
|
});
|
|
}
|
|
|
|
/// Test all valid refcount orders (0-6) can be opened.
|
|
#[test]
|
|
fn refcount_all_orders() {
|
|
for order in 0..=6u8 {
|
|
let mut header = valid_header_v3();
|
|
header[99] = order;
|
|
with_basic_file(&header, |disk_file: RawFile| {
|
|
QcowFile::from(disk_file).expect("refcount order should work");
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Test write/read roundtrip for all refcount orders.
|
|
#[test]
|
|
fn refcount_all_orders_write_read() {
|
|
for order in 0..=6u8 {
|
|
let mut header = valid_header_v3();
|
|
header[99] = order;
|
|
with_basic_file(&header, |disk_file: RawFile| {
|
|
let mut q = QcowFile::from(disk_file).unwrap();
|
|
let test_data = b"test data for refcount";
|
|
|
|
// Write and read back
|
|
q.write_all(test_data).unwrap();
|
|
q.rewind().unwrap();
|
|
let mut buf = vec![0u8; test_data.len()];
|
|
q.read_exact(&mut buf).unwrap();
|
|
assert_eq!(&buf, test_data);
|
|
|
|
// Write to another cluster
|
|
q.seek(SeekFrom::Start(0x10000)).unwrap();
|
|
q.write_all(test_data).unwrap();
|
|
q.seek(SeekFrom::Start(0x10000)).unwrap();
|
|
q.read_exact(&mut buf).unwrap();
|
|
assert_eq!(&buf, test_data);
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Test overwrite and multi-cluster allocation for all refcount orders.
|
|
#[test]
|
|
fn refcount_all_orders_overwrite() {
|
|
for order in 0..=6u8 {
|
|
let mut header = valid_header_v3();
|
|
header[99] = order;
|
|
with_basic_file(&header, |disk_file: RawFile| {
|
|
let mut q = QcowFile::from(disk_file).unwrap();
|
|
|
|
// Write then overwrite
|
|
q.write_all(b"initial data here!!!").unwrap();
|
|
q.rewind().unwrap();
|
|
let new_data = b"overwritten data!!!!";
|
|
q.write_all(new_data).unwrap();
|
|
q.rewind().unwrap();
|
|
let mut buf = vec![0u8; new_data.len()];
|
|
q.read_exact(&mut buf).unwrap();
|
|
assert_eq!(&buf, new_data);
|
|
|
|
// Allocate multiple clusters
|
|
let cluster_size = 0x10000u64;
|
|
for i in 1..4u64 {
|
|
q.seek(SeekFrom::Start(i * cluster_size)).unwrap();
|
|
q.write_all(b"cluster data").unwrap();
|
|
}
|
|
for i in 1..4u64 {
|
|
let mut cluster_buf = vec![0u8; 12];
|
|
q.seek(SeekFrom::Start(i * cluster_size)).unwrap();
|
|
q.read_exact(&mut cluster_buf).unwrap();
|
|
assert_eq!(&cluster_buf, b"cluster data");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Test L2 cache eviction for all refcount orders.
|
|
#[test]
|
|
fn refcount_all_orders_l2_eviction() {
|
|
for order in 0..=6u8 {
|
|
let mut header = valid_header_v3();
|
|
header[99] = order;
|
|
with_basic_file(&header, |disk_file: RawFile| {
|
|
let mut q = QcowFile::from(disk_file).unwrap();
|
|
|
|
// L2 cache has 100 entries. Write to >100 regions to force eviction.
|
|
let cluster_size = 0x10000u64;
|
|
let l2_coverage = cluster_size * (cluster_size / 8);
|
|
|
|
for i in 0..110u64 {
|
|
q.seek(SeekFrom::Start(i * l2_coverage)).unwrap();
|
|
q.write_all(b"eviction test").unwrap();
|
|
}
|
|
|
|
// Verify evicted regions can be re-read
|
|
for i in [0u64, 1, 50, 100, 109] {
|
|
let mut buf = vec![0u8; 13];
|
|
q.seek(SeekFrom::Start(i * l2_coverage)).unwrap();
|
|
q.read_exact(&mut buf).unwrap();
|
|
assert_eq!(&buf, b"eviction test");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Test sub-byte refcount read/write roundtrip with max values.
|
|
#[test]
|
|
fn refcount_subbyte_max_values() {
|
|
for (bits, max_val) in [(1u64, 1u64), (2, 3), (4, 15)] {
|
|
let file = vmm_sys_util::tempfile::TempFile::new().unwrap().into_file();
|
|
let cluster_size = 0x10000u64;
|
|
file.set_len(cluster_size * 2).unwrap();
|
|
let raw = RawFile::new(file, false);
|
|
let mut qcow_raw = QcowRawFile::from(raw, cluster_size, bits).unwrap();
|
|
|
|
let entries = (cluster_size * 8 / bits) as usize;
|
|
let mut table: Vec<u64> = (0..entries as u64).map(|i| i % (max_val + 1)).collect();
|
|
table[0] = max_val;
|
|
table[entries - 1] = max_val;
|
|
|
|
qcow_raw.write_refcount_block(cluster_size, &table).unwrap();
|
|
let read_table = qcow_raw.read_refcount_block(cluster_size).unwrap();
|
|
|
|
assert_eq!(read_table.len(), entries);
|
|
for (i, (&written, &read)) in table.iter().zip(read_table.iter()).enumerate() {
|
|
assert_eq!(read, written & max_val, "{bits}-bit entry {i} mismatch");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Test byte-aligned refcounts with max values.
|
|
#[test]
|
|
fn refcount_byte_aligned_large_values() {
|
|
for (bits, test_val) in [
|
|
(8u64, 0xFFu64),
|
|
(16, 0xFFFFu64),
|
|
(32, 0xFFFF_FFFFu64),
|
|
(64, u64::MAX),
|
|
] {
|
|
let file = vmm_sys_util::tempfile::TempFile::new().unwrap().into_file();
|
|
let cluster_size = 0x10000u64;
|
|
file.set_len(cluster_size * 2).unwrap();
|
|
let raw = RawFile::new(file, false);
|
|
let mut qcow_raw = QcowRawFile::from(raw, cluster_size, bits).unwrap();
|
|
|
|
let entries = (cluster_size * 8 / bits) as usize;
|
|
let mut table: Vec<u64> = vec![0; entries];
|
|
table[0] = test_val;
|
|
table[1] = 1;
|
|
table[entries - 1] = test_val;
|
|
|
|
qcow_raw.write_refcount_block(cluster_size, &table).unwrap();
|
|
let read_table = qcow_raw.read_refcount_block(cluster_size).unwrap();
|
|
|
|
assert_eq!(read_table[0], test_val);
|
|
assert_eq!(read_table[1], 1);
|
|
assert_eq!(read_table[entries - 1], test_val);
|
|
}
|
|
}
|
|
|
|
/// Test RefcountOverflow error when exceeding max refcount value.
|
|
#[test]
|
|
fn refcount_overflow_returns_error() {
|
|
use super::refcount::Error as RefcountError;
|
|
|
|
for (refcount_bits, max_val) in [(1u64, 1u64), (2, 3), (4, 15)] {
|
|
let file = vmm_sys_util::tempfile::TempFile::new().unwrap().into_file();
|
|
let cluster_size = 0x10000u64;
|
|
let refcount_block_entries = cluster_size * 8 / refcount_bits;
|
|
file.set_len(cluster_size * 3).unwrap();
|
|
|
|
let raw = RawFile::new(file, false);
|
|
let mut qcow_raw = QcowRawFile::from(raw, cluster_size, refcount_bits).unwrap();
|
|
|
|
// Set up refcount table pointing to refcount block
|
|
let refcount_table_offset = cluster_size;
|
|
qcow_raw
|
|
.file_mut()
|
|
.seek(SeekFrom::Start(refcount_table_offset))
|
|
.unwrap();
|
|
qcow_raw
|
|
.file_mut()
|
|
.write_all(&(cluster_size * 2).to_be_bytes())
|
|
.unwrap();
|
|
|
|
let zeros = vec![0u64; refcount_block_entries as usize];
|
|
qcow_raw
|
|
.write_refcount_block(cluster_size * 2, &zeros)
|
|
.unwrap();
|
|
|
|
let mut refcount = RefCount::new(
|
|
&mut qcow_raw,
|
|
refcount_table_offset,
|
|
1,
|
|
refcount_block_entries,
|
|
cluster_size,
|
|
refcount_bits,
|
|
)
|
|
.unwrap();
|
|
|
|
// Overflow should fail
|
|
let result = refcount.set_cluster_refcount(&mut qcow_raw, 0, max_val + 1, None);
|
|
assert!(
|
|
matches!(result, Err(RefcountError::RefcountOverflow { .. })),
|
|
"{refcount_bits}-bit: expected overflow error"
|
|
);
|
|
|
|
// Max value should not overflow
|
|
let result = refcount.set_cluster_refcount(&mut qcow_raw, 0, max_val, None);
|
|
assert!(
|
|
!matches!(result, Err(RefcountError::RefcountOverflow { .. })),
|
|
"{refcount_bits}-bit: max value should not overflow"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_cluster_bits() {
|
|
let mut header = valid_header_v3();
|
|
header[23] = 3;
|
|
with_basic_file(&header, |disk_file: RawFile| {
|
|
QcowFile::from(disk_file).expect_err("Failed to create file.");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_header_huge_file() {
|
|
let header = test_huge_header();
|
|
with_basic_file(&header, |disk_file: RawFile| {
|
|
QcowFile::from(disk_file).expect_err("Failed to create file.");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_header_crazy_file_size_rejected() {
|
|
let mut header = valid_header_v3();
|
|
header[24..32].copy_from_slice(&[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1e]);
|
|
with_basic_file(&header, |disk_file: RawFile| {
|
|
QcowFile::from(disk_file).expect_err("Failed to create file.");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_huge_l1_table() {
|
|
let mut header = valid_header_v3();
|
|
header[36] = 0x12;
|
|
with_basic_file(&header, |disk_file: RawFile| {
|
|
QcowFile::from(disk_file).expect_err("Failed to create file.");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_header_1_tb_file_min_cluster() {
|
|
let mut header = test_huge_header();
|
|
header[24] = 0;
|
|
header[26] = 1;
|
|
header[31] = 0;
|
|
// 1 TB with the min cluster size makes the arrays too big, it should fail.
|
|
with_basic_file(&header, |disk_file: RawFile| {
|
|
QcowFile::from(disk_file).expect_err("Failed to create file.");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_l2_entry_zero_flag() {
|
|
let empty_entry: u64 = 0;
|
|
let standard_entry: u64 = 0x1000;
|
|
let zero_flag_entry: u64 = 0x1000 | ZERO_FLAG;
|
|
let compressed_entry: u64 = COMPRESSED_FLAG;
|
|
let compressed_entry_with_low_bit: u64 = COMPRESSED_FLAG | ZERO_FLAG;
|
|
|
|
assert!(l2_entry_is_empty(empty_entry));
|
|
assert!(!l2_entry_is_empty(standard_entry));
|
|
|
|
assert!(!l2_entry_is_compressed(standard_entry));
|
|
assert!(l2_entry_is_compressed(compressed_entry));
|
|
assert!(l2_entry_is_compressed(compressed_entry_with_low_bit));
|
|
|
|
assert!(!l2_entry_is_zero(standard_entry));
|
|
assert!(l2_entry_is_zero(zero_flag_entry));
|
|
assert!(l2_entry_is_zero(compressed_entry_with_low_bit));
|
|
|
|
// Note: l2_entry_is_zero() only checks bit 0, so compressed entries
|
|
// must be checked before interpreting bit 0 as a zero flag.
|
|
}
|
|
|
|
#[test]
|
|
fn test_header_1_tb_file() {
|
|
let mut header = test_huge_header();
|
|
// reset to 1 TB size.
|
|
header[24] = 0;
|
|
header[26] = 1;
|
|
header[31] = 0;
|
|
// set cluster_bits
|
|
header[23] = 16;
|
|
with_basic_file(&header, |disk_file: RawFile| {
|
|
let mut qcow = QcowFile::from(disk_file).expect("Failed to create file.");
|
|
qcow.seek(SeekFrom::Start(0x100_0000_0000 - 8))
|
|
.expect("Failed to seek.");
|
|
let value = 0x0000_0040_3f00_ffffu64;
|
|
qcow.write_all(&value.to_le_bytes())
|
|
.expect("failed to write data");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_header_huge_num_refcounts() {
|
|
let mut header = valid_header_v3();
|
|
header[56..60].copy_from_slice(&[0x02, 0x00, 0xe8, 0xff]);
|
|
with_basic_file(&header, |disk_file: RawFile| {
|
|
QcowFile::from(disk_file).expect_err("Created disk with crazy refcount clusters");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_header_huge_refcount_offset() {
|
|
let mut header = valid_header_v3();
|
|
header[48..56].copy_from_slice(&[0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x02, 0x00]);
|
|
with_basic_file(&header, |disk_file: RawFile| {
|
|
QcowFile::from(disk_file).expect_err("Created disk with crazy refcount offset");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn write_read_start() {
|
|
with_basic_file(&valid_header_v3(), |disk_file: RawFile| {
|
|
let mut q = QcowFile::from(disk_file).unwrap();
|
|
q.write_all(b"test first bytes")
|
|
.expect("Failed to write test string.");
|
|
let mut buf = [0u8; 4];
|
|
q.rewind().expect("Failed to seek.");
|
|
q.read_exact(&mut buf).expect("Failed to read.");
|
|
assert_eq!(&buf, b"test");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn write_read_start_backing_overlap() {
|
|
let disk_file = basic_file(&valid_header_v3());
|
|
let mut backing = QcowFile::from(disk_file).unwrap();
|
|
backing
|
|
.write_all(b"test first bytes")
|
|
.expect("Failed to write test string.");
|
|
let wrapping_disk_file = basic_file(&valid_header_v3());
|
|
let mut wrapping = QcowFile::from(wrapping_disk_file).unwrap();
|
|
wrapping.set_backing_file(Some(Box::new(backing)));
|
|
wrapping.seek(SeekFrom::Start(0)).expect("Failed to seek.");
|
|
wrapping
|
|
.write_all(b"TEST")
|
|
.expect("Failed to write second test string.");
|
|
let mut buf = [0u8; 10];
|
|
wrapping.seek(SeekFrom::Start(0)).expect("Failed to seek.");
|
|
wrapping.read_exact(&mut buf).expect("Failed to read.");
|
|
assert_eq!(&buf, b"TEST first");
|
|
}
|
|
|
|
#[test]
|
|
fn offset_write_read() {
|
|
with_basic_file(&valid_header_v3(), |disk_file: RawFile| {
|
|
let mut q = QcowFile::from(disk_file).unwrap();
|
|
let b = [0x55u8; 0x1000];
|
|
q.seek(SeekFrom::Start(0xfff2000)).expect("Failed to seek.");
|
|
q.write_all(&b).expect("Failed to write test string.");
|
|
let mut buf = [0u8; 4];
|
|
q.seek(SeekFrom::Start(0xfff2000)).expect("Failed to seek.");
|
|
q.read_exact(&mut buf).expect("Failed to read.");
|
|
assert_eq!(buf[0], 0x55);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn resize_grow_within_l1() {
|
|
with_default_file(0x10_0000, false, |mut q| {
|
|
let original_size = q.virtual_size();
|
|
assert_eq!(original_size, 0x10_0000);
|
|
|
|
q.resize(original_size)
|
|
.expect("Resize to same size should succeed");
|
|
assert_eq!(q.virtual_size(), original_size);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn resize_grow_with_l1_growth() {
|
|
let initial_size = 1024 * 1024; // 1 MB
|
|
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, true).unwrap();
|
|
|
|
let original_l1_size = q.header().l1_size;
|
|
assert_eq!(q.virtual_size(), initial_size);
|
|
|
|
let test_data = b"Hello, QCOW resize test!";
|
|
q.rewind().unwrap();
|
|
q.write_all(test_data).unwrap();
|
|
|
|
q.resize(new_size).expect("Resize should succeed");
|
|
assert_eq!(q.virtual_size(), new_size);
|
|
|
|
assert!(q.header().l1_size > original_l1_size);
|
|
|
|
// Verify original data is still intact
|
|
let mut buf = vec![0u8; test_data.len()];
|
|
q.rewind().unwrap();
|
|
q.read_exact(&mut buf).unwrap();
|
|
assert_eq!(&buf, test_data);
|
|
|
|
let new_offset = new_size - 0x10000; // 64KB before end
|
|
q.seek(SeekFrom::Start(new_offset)).unwrap();
|
|
let new_data = b"Data at new end!";
|
|
q.write_all(new_data).unwrap();
|
|
|
|
let mut buf2 = vec![0u8; new_data.len()];
|
|
q.seek(SeekFrom::Start(new_offset)).unwrap();
|
|
q.read_exact(&mut buf2).unwrap();
|
|
assert_eq!(&buf2, new_data);
|
|
}
|
|
|
|
#[test]
|
|
fn resize_shrink_fails() {
|
|
with_default_file(0x10_0000, false, |mut q| {
|
|
let original_size = q.virtual_size();
|
|
let smaller_size = original_size / 2;
|
|
|
|
let result = q.resize(smaller_size);
|
|
assert!(result.is_err());
|
|
let err = result.unwrap_err();
|
|
assert!(matches!(err.kind(), BlockErrorKind::UnsupportedFeature));
|
|
assert!(matches!(
|
|
err.downcast_ref::<Error>(),
|
|
Some(Error::ShrinkNotSupported)
|
|
));
|
|
|
|
assert_eq!(q.virtual_size(), original_size);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn resize_with_backing_file_fails() {
|
|
let backing_temp = TempFile::new().unwrap();
|
|
let backing_path = backing_temp.as_path().to_str().unwrap().to_string();
|
|
let backing_size = 1024 * 1024; // 1 MB
|
|
|
|
{
|
|
let backing_raw = RawFile::new(backing_temp.as_file().try_clone().unwrap(), false);
|
|
let _backing_qcow = QcowFile::new(backing_raw, 3, backing_size, true).unwrap();
|
|
}
|
|
|
|
let overlay_file = TempFile::new().unwrap();
|
|
let overlay_raw = RawFile::new(overlay_file.into_file(), false);
|
|
let backing_config = BackingFileConfig {
|
|
path: backing_path,
|
|
format: Some(ImageType::Qcow2),
|
|
};
|
|
let mut overlay =
|
|
QcowFile::new_from_backing(overlay_raw, 3, backing_size, &backing_config, true)
|
|
.unwrap();
|
|
|
|
assert_eq!(overlay.virtual_size(), backing_size);
|
|
|
|
let result = overlay.resize(backing_size * 2);
|
|
assert!(result.is_err());
|
|
let err = result.unwrap_err();
|
|
assert!(matches!(err.kind(), BlockErrorKind::UnsupportedFeature));
|
|
assert!(matches!(
|
|
err.downcast_ref::<Error>(),
|
|
Some(Error::ResizeWithBackingFile)
|
|
));
|
|
|
|
assert_eq!(overlay.virtual_size(), backing_size);
|
|
}
|
|
|
|
#[test]
|
|
fn read_beyond_backing_file_returns_zeros() {
|
|
let backing_temp = TempFile::new().unwrap();
|
|
let backing_path = backing_temp.as_path().to_str().unwrap().to_string();
|
|
let backing_size = 1024 * 1024;
|
|
|
|
{
|
|
let backing_raw = RawFile::new(backing_temp.as_file().try_clone().unwrap(), false);
|
|
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();
|
|
let boundary_data = [0xAAu8; 512];
|
|
backing_qcow
|
|
.seek(SeekFrom::Start(backing_size - 512))
|
|
.unwrap();
|
|
backing_qcow.write_all(&boundary_data).unwrap();
|
|
backing_qcow.flush().unwrap();
|
|
}
|
|
|
|
let overlay_file = TempFile::new().unwrap();
|
|
let overlay_raw = RawFile::new(overlay_file.into_file(), false);
|
|
let backing_config = BackingFileConfig {
|
|
path: backing_path,
|
|
format: Some(ImageType::Qcow2),
|
|
};
|
|
let overlay_size = backing_size * 2; // 2x the backing size
|
|
let mut overlay =
|
|
QcowFile::new_from_backing(overlay_raw, 3, overlay_size, &backing_config, true)
|
|
.unwrap();
|
|
|
|
assert_eq!(overlay.virtual_size(), overlay_size);
|
|
|
|
let mut buf = vec![0u8; 12];
|
|
overlay.rewind().unwrap();
|
|
overlay.read_exact(&mut buf).unwrap();
|
|
assert_eq!(&buf, b"BACKING_DATA");
|
|
|
|
let offset_beyond = backing_size + 4096;
|
|
let mut beyond_buf = vec![0xFFu8; 4096];
|
|
overlay.seek(SeekFrom::Start(offset_beyond)).unwrap();
|
|
overlay.read_exact(&mut beyond_buf).unwrap();
|
|
assert!(
|
|
beyond_buf.iter().all(|&b| b == 0),
|
|
"Read beyond backing file should return zeros"
|
|
);
|
|
|
|
let offset_at_boundary = backing_size - 512;
|
|
let mut boundary_buf = vec![0xFFu8; 1024]; // 512 in backing, 512 beyond
|
|
overlay.seek(SeekFrom::Start(offset_at_boundary)).unwrap();
|
|
overlay.read_exact(&mut boundary_buf).unwrap();
|
|
assert!(
|
|
boundary_buf[..512].iter().all(|&b| b == 0xAA),
|
|
"Portion within backing file should contain backing data"
|
|
);
|
|
assert!(
|
|
boundary_buf[512..].iter().all(|&b| b == 0),
|
|
"Portion beyond backing file should be zeros"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn write_zeroes_read() {
|
|
with_basic_file(&valid_header_v3(), |disk_file: RawFile| {
|
|
let mut q = QcowFile::from(disk_file).unwrap();
|
|
// Write some test data.
|
|
let b = [0x55u8; 0x1000];
|
|
q.seek(SeekFrom::Start(0xfff2000)).expect("Failed to seek.");
|
|
q.write_all(&b).expect("Failed to write test string.");
|
|
// Overwrite the test data with zeroes.
|
|
q.seek(SeekFrom::Start(0xfff2000)).expect("Failed to seek.");
|
|
let nwritten = q.write_zeroes(0x200).expect("Failed to write zeroes.");
|
|
assert_eq!(nwritten, 0x200);
|
|
// Verify that the correct part of the data was zeroed out.
|
|
let mut buf = [0u8; 0x1000];
|
|
q.seek(SeekFrom::Start(0xfff2000)).expect("Failed to seek.");
|
|
q.read_exact(&mut buf).expect("Failed to read.");
|
|
assert_eq!(buf[0], 0);
|
|
assert_eq!(buf[0x1FF], 0);
|
|
assert_eq!(buf[0x200], 0x55);
|
|
assert_eq!(buf[0xFFF], 0x55);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn write_zeroes_full_cluster() {
|
|
// Choose a size that is larger than a cluster.
|
|
// valid_header uses cluster_bits = 12, which corresponds to a cluster size of 4096.
|
|
const CHUNK_SIZE: usize = 4096 * 2 + 512;
|
|
with_basic_file(&valid_header_v3(), |disk_file: RawFile| {
|
|
let mut q = QcowFile::from(disk_file).unwrap();
|
|
// Write some test data.
|
|
let b = [0x55u8; CHUNK_SIZE];
|
|
q.rewind().expect("Failed to seek.");
|
|
q.write_all(&b).expect("Failed to write test string.");
|
|
// Overwrite the full cluster with zeroes.
|
|
q.rewind().expect("Failed to seek.");
|
|
let nwritten = q.write_zeroes(CHUNK_SIZE).expect("Failed to write zeroes.");
|
|
assert_eq!(nwritten, CHUNK_SIZE);
|
|
// Verify that the data was zeroed out.
|
|
let mut buf = [0u8; CHUNK_SIZE];
|
|
q.rewind().expect("Failed to seek.");
|
|
q.read_exact(&mut buf).expect("Failed to read.");
|
|
assert_eq!(buf[0], 0);
|
|
assert_eq!(buf[CHUNK_SIZE - 1], 0);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn discard_sets_zero_flag() {
|
|
with_basic_file(&valid_header_v3(), |disk_file: RawFile| {
|
|
let mut q = QcowFile::from(disk_file).unwrap();
|
|
|
|
// Write some test data to allocate a cluster
|
|
let test_data = [0x42u8; 4096];
|
|
q.seek(SeekFrom::Start(0x10000)).expect("Failed to seek.");
|
|
q.write_all(&test_data).expect("Failed to write test data.");
|
|
|
|
// Verify data was written
|
|
let mut buf = [0u8; 4096];
|
|
q.seek(SeekFrom::Start(0x10000)).expect("Failed to seek.");
|
|
q.read_exact(&mut buf).expect("Failed to read.");
|
|
assert_eq!(buf[0], 0x42);
|
|
assert_eq!(buf[4095], 0x42);
|
|
|
|
// DISCARD the full cluster (via write_zeroes which calls punch_hole)
|
|
q.seek(SeekFrom::Start(0x10000)).expect("Failed to seek.");
|
|
let nwritten = q.write_zeroes(4096).expect("Failed to discard cluster.");
|
|
assert_eq!(nwritten, 4096);
|
|
|
|
// Verify reads now return zeros (due to zero flag)
|
|
q.seek(SeekFrom::Start(0x10000)).expect("Failed to seek.");
|
|
q.read_exact(&mut buf).expect("Failed to read.");
|
|
assert_eq!(buf[0], 0);
|
|
assert_eq!(buf[4095], 0);
|
|
|
|
// Write new data to the trimmed cluster
|
|
let new_data = [0x99u8; 4096];
|
|
q.seek(SeekFrom::Start(0x10000)).expect("Failed to seek.");
|
|
q.write_all(&new_data)
|
|
.expect("Failed to write to trimmed cluster.");
|
|
|
|
// Verify new data can be read (cluster was reallocated)
|
|
q.seek(SeekFrom::Start(0x10000)).expect("Failed to seek.");
|
|
q.read_exact(&mut buf).expect("Failed to read.");
|
|
assert_eq!(buf[0], 0x99);
|
|
assert_eq!(buf[4095], 0x99);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_header() {
|
|
with_basic_file(&valid_header_v2(), |disk_file: RawFile| {
|
|
let q = QcowFile::from(disk_file).unwrap();
|
|
assert_eq!(q.virtual_size(), 0x20_0000_0000);
|
|
});
|
|
with_basic_file(&valid_header_v3(), |disk_file: RawFile| {
|
|
let q = QcowFile::from(disk_file).unwrap();
|
|
assert_eq!(q.virtual_size(), 0x20_0000_0000);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn read_small_buffer() {
|
|
with_basic_file(&valid_header_v3(), |disk_file: RawFile| {
|
|
let mut q = QcowFile::from(disk_file).unwrap();
|
|
let mut b = [5u8; 16];
|
|
q.seek(SeekFrom::Start(1000)).expect("Failed to seek.");
|
|
q.read_exact(&mut b).expect("Failed to read.");
|
|
assert_eq!(0, b[0]);
|
|
assert_eq!(0, b[15]);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn replay_ext4() {
|
|
with_basic_file(&valid_header_v3(), |disk_file: RawFile| {
|
|
let mut q = QcowFile::from(disk_file).unwrap();
|
|
const BUF_SIZE: usize = 0x1000;
|
|
let mut b = [0u8; BUF_SIZE];
|
|
|
|
struct Transfer {
|
|
pub write: bool,
|
|
pub addr: u64,
|
|
}
|
|
|
|
// Write transactions from mkfs.ext4.
|
|
let xfers: Vec<Transfer> = vec![
|
|
Transfer {
|
|
write: false,
|
|
addr: 0xfff0000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0xfffe000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x0,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x1000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0xffff000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0xffdf000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0xfff8000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0xffe0000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0xffce000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0xffb6000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0xffab000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0xffa4000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0xff8e000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0xff86000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0xff84000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0xff89000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0xfe7e000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x100000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x3000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x7000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0xf000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x2000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x4000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x5000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x6000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x8000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x9000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0xa000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0xb000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0xc000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0xd000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0xe000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x10000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x11000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x12000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x13000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x14000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x15000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x16000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x17000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x18000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x19000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x1a000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x1b000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x1c000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x1d000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x1e000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x1f000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x21000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x22000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x24000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x40000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x0,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x3000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x7000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x0,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x1000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x2000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x3000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x0,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x449000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x48000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x48000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x448000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x44a000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x48000,
|
|
},
|
|
Transfer {
|
|
write: false,
|
|
addr: 0x48000,
|
|
},
|
|
Transfer {
|
|
write: true,
|
|
addr: 0x0,
|
|
},
|
|
Transfer {
|
|
write: true,
|
|
addr: 0x448000,
|
|
},
|
|
Transfer {
|
|
write: true,
|
|
addr: 0x449000,
|
|
},
|
|
Transfer {
|
|
write: true,
|
|
addr: 0x44a000,
|
|
},
|
|
Transfer {
|
|
write: true,
|
|
addr: 0xfff0000,
|
|
},
|
|
Transfer {
|
|
write: true,
|
|
addr: 0xfff1000,
|
|
},
|
|
Transfer {
|
|
write: true,
|
|
addr: 0xfff2000,
|
|
},
|
|
Transfer {
|
|
write: true,
|
|
addr: 0xfff3000,
|
|
},
|
|
Transfer {
|
|
write: true,
|
|
addr: 0xfff4000,
|
|
},
|
|
Transfer {
|
|
write: true,
|
|
addr: 0xfff5000,
|
|
},
|
|
Transfer {
|
|
write: true,
|
|
addr: 0xfff6000,
|
|
},
|
|
Transfer {
|
|
write: true,
|
|
addr: 0xfff7000,
|
|
},
|
|
Transfer {
|
|
write: true,
|
|
addr: 0xfff8000,
|
|
},
|
|
Transfer {
|
|
write: true,
|
|
addr: 0xfff9000,
|
|
},
|
|
Transfer {
|
|
write: true,
|
|
addr: 0xfffa000,
|
|
},
|
|
Transfer {
|
|
write: true,
|
|
addr: 0xfffb000,
|
|
},
|
|
Transfer {
|
|
write: true,
|
|
addr: 0xfffc000,
|
|
},
|
|
Transfer {
|
|
write: true,
|
|
addr: 0xfffd000,
|
|
},
|
|
Transfer {
|
|
write: true,
|
|
addr: 0xfffe000,
|
|
},
|
|
Transfer {
|
|
write: true,
|
|
addr: 0xffff000,
|
|
},
|
|
];
|
|
|
|
for xfer in &xfers {
|
|
q.seek(SeekFrom::Start(xfer.addr)).expect("Failed to seek.");
|
|
if xfer.write {
|
|
q.write_all(&b).expect("Failed to write.");
|
|
} else {
|
|
let read_count: usize = q.read(&mut b).expect("Failed to read.");
|
|
assert_eq!(read_count, BUF_SIZE);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn combo_write_read() {
|
|
combo_write_read_common(false);
|
|
}
|
|
|
|
#[test]
|
|
fn combo_write_read_direct() {
|
|
combo_write_read_common(true);
|
|
}
|
|
|
|
fn combo_write_read_common(direct: bool) {
|
|
with_default_file(1024 * 1024 * 1024 * 256, direct, |mut qcow_file| {
|
|
const NUM_BLOCKS: usize = 555;
|
|
const BLOCK_SIZE: usize = 0x1_0000;
|
|
const OFFSET: usize = 0x1_0000_0020;
|
|
let data = [0x55u8; BLOCK_SIZE];
|
|
let mut readback = [0u8; BLOCK_SIZE];
|
|
for i in 0..NUM_BLOCKS {
|
|
let seek_offset = OFFSET + i * BLOCK_SIZE;
|
|
qcow_file
|
|
.seek(SeekFrom::Start(seek_offset as u64))
|
|
.expect("Failed to seek.");
|
|
let nwritten = qcow_file.write(&data).expect("Failed to write test data.");
|
|
assert_eq!(nwritten, BLOCK_SIZE);
|
|
// Read back the data to check it was written correctly.
|
|
qcow_file
|
|
.seek(SeekFrom::Start(seek_offset as u64))
|
|
.expect("Failed to seek.");
|
|
let nread = qcow_file.read(&mut readback).expect("Failed to read.");
|
|
assert_eq!(nread, BLOCK_SIZE);
|
|
for (orig, read) in data.iter().zip(readback.iter()) {
|
|
assert_eq!(orig, read);
|
|
}
|
|
}
|
|
// Check that address 0 is still zeros.
|
|
qcow_file.rewind().expect("Failed to seek.");
|
|
let nread = qcow_file.read(&mut readback).expect("Failed to read.");
|
|
assert_eq!(nread, BLOCK_SIZE);
|
|
for read in readback.iter() {
|
|
assert_eq!(*read, 0);
|
|
}
|
|
// Check the data again after the writes have happened.
|
|
for i in 0..NUM_BLOCKS {
|
|
let seek_offset = OFFSET + i * BLOCK_SIZE;
|
|
qcow_file
|
|
.seek(SeekFrom::Start(seek_offset as u64))
|
|
.expect("Failed to seek.");
|
|
let nread = qcow_file.read(&mut readback).expect("Failed to read.");
|
|
assert_eq!(nread, BLOCK_SIZE);
|
|
for (orig, read) in data.iter().zip(readback.iter()) {
|
|
assert_eq!(orig, read);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
fn seek_cur(file: &mut QcowFile) -> u64 {
|
|
file.stream_position().unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn seek_data() {
|
|
seek_data_common(false);
|
|
}
|
|
|
|
#[test]
|
|
fn seek_data_direct() {
|
|
seek_data_common(true);
|
|
}
|
|
|
|
fn seek_data_common(direct: bool) {
|
|
with_default_file(0x30000, direct, |mut file| {
|
|
// seek_data at or after the end of the file should return None
|
|
assert_eq!(file.seek_data(0x10000).unwrap(), None);
|
|
assert_eq!(seek_cur(&mut file), 0);
|
|
assert_eq!(file.seek_data(0x10001).unwrap(), None);
|
|
assert_eq!(seek_cur(&mut file), 0);
|
|
|
|
// Write some data to [0x10000, 0x20000)
|
|
let b = [0x55u8; 0x10000];
|
|
file.seek(SeekFrom::Start(0x10000)).unwrap();
|
|
file.write_all(&b).unwrap();
|
|
assert_eq!(file.seek_data(0).unwrap(), Some(0x10000));
|
|
assert_eq!(seek_cur(&mut file), 0x10000);
|
|
|
|
// seek_data within data should return the same offset
|
|
assert_eq!(file.seek_data(0x10000).unwrap(), Some(0x10000));
|
|
assert_eq!(seek_cur(&mut file), 0x10000);
|
|
assert_eq!(file.seek_data(0x10001).unwrap(), Some(0x10001));
|
|
assert_eq!(seek_cur(&mut file), 0x10001);
|
|
assert_eq!(file.seek_data(0x1FFFF).unwrap(), Some(0x1FFFF));
|
|
assert_eq!(seek_cur(&mut file), 0x1FFFF);
|
|
|
|
assert_eq!(file.seek_data(0).unwrap(), Some(0x10000));
|
|
assert_eq!(seek_cur(&mut file), 0x10000);
|
|
assert_eq!(file.seek_data(0x1FFFF).unwrap(), Some(0x1FFFF));
|
|
assert_eq!(seek_cur(&mut file), 0x1FFFF);
|
|
assert_eq!(file.seek_data(0x20000).unwrap(), None);
|
|
assert_eq!(seek_cur(&mut file), 0x1FFFF);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn seek_hole() {
|
|
seek_hole_common(false);
|
|
}
|
|
|
|
#[test]
|
|
fn seek_hole_direct() {
|
|
seek_hole_common(true);
|
|
}
|
|
|
|
fn seek_hole_common(direct: bool) {
|
|
with_default_file(0x30000, direct, |mut file| {
|
|
// File consisting entirely of a hole
|
|
assert_eq!(file.seek_hole(0).unwrap(), Some(0));
|
|
assert_eq!(seek_cur(&mut file), 0);
|
|
assert_eq!(file.seek_hole(0xFFFF).unwrap(), Some(0xFFFF));
|
|
assert_eq!(seek_cur(&mut file), 0xFFFF);
|
|
|
|
// seek_hole at or after the end of the file should return None
|
|
file.rewind().unwrap();
|
|
assert_eq!(file.seek_hole(0x30000).unwrap(), None);
|
|
assert_eq!(seek_cur(&mut file), 0);
|
|
assert_eq!(file.seek_hole(0x30001).unwrap(), None);
|
|
assert_eq!(seek_cur(&mut file), 0);
|
|
|
|
// Write some data to [0x10000, 0x20000)
|
|
let b = [0x55u8; 0x10000];
|
|
file.seek(SeekFrom::Start(0x10000)).unwrap();
|
|
file.write_all(&b).unwrap();
|
|
|
|
// seek_hole within a hole should return the same offset
|
|
assert_eq!(file.seek_hole(0).unwrap(), Some(0));
|
|
assert_eq!(seek_cur(&mut file), 0);
|
|
assert_eq!(file.seek_hole(0xFFFF).unwrap(), Some(0xFFFF));
|
|
assert_eq!(seek_cur(&mut file), 0xFFFF);
|
|
|
|
// seek_hole within data should return the next hole
|
|
file.rewind().unwrap();
|
|
assert_eq!(file.seek_hole(0x10000).unwrap(), Some(0x20000));
|
|
assert_eq!(seek_cur(&mut file), 0x20000);
|
|
file.rewind().unwrap();
|
|
assert_eq!(file.seek_hole(0x10001).unwrap(), Some(0x20000));
|
|
assert_eq!(seek_cur(&mut file), 0x20000);
|
|
file.rewind().unwrap();
|
|
assert_eq!(file.seek_hole(0x1FFFF).unwrap(), Some(0x20000));
|
|
assert_eq!(seek_cur(&mut file), 0x20000);
|
|
file.rewind().unwrap();
|
|
assert_eq!(file.seek_hole(0xFFFF).unwrap(), Some(0xFFFF));
|
|
assert_eq!(seek_cur(&mut file), 0xFFFF);
|
|
file.rewind().unwrap();
|
|
assert_eq!(file.seek_hole(0x10000).unwrap(), Some(0x20000));
|
|
assert_eq!(seek_cur(&mut file), 0x20000);
|
|
file.rewind().unwrap();
|
|
assert_eq!(file.seek_hole(0x1FFFF).unwrap(), Some(0x20000));
|
|
assert_eq!(seek_cur(&mut file), 0x20000);
|
|
file.rewind().unwrap();
|
|
assert_eq!(file.seek_hole(0x20000).unwrap(), Some(0x20000));
|
|
assert_eq!(seek_cur(&mut file), 0x20000);
|
|
file.rewind().unwrap();
|
|
assert_eq!(file.seek_hole(0x20001).unwrap(), Some(0x20001));
|
|
assert_eq!(seek_cur(&mut file), 0x20001);
|
|
|
|
// seek_hole at EOF should return None
|
|
file.rewind().unwrap();
|
|
assert_eq!(file.seek_hole(0x30000).unwrap(), None);
|
|
assert_eq!(seek_cur(&mut file), 0);
|
|
|
|
// Write some data to [0x20000, 0x30000)
|
|
file.seek(SeekFrom::Start(0x20000)).unwrap();
|
|
file.write_all(&b).unwrap();
|
|
|
|
// seek_hole within [0x20000, 0x30000) should now find the hole at EOF
|
|
assert_eq!(file.seek_hole(0x20000).unwrap(), Some(0x30000));
|
|
assert_eq!(seek_cur(&mut file), 0x30000);
|
|
file.rewind().unwrap();
|
|
assert_eq!(file.seek_hole(0x20001).unwrap(), Some(0x30000));
|
|
assert_eq!(seek_cur(&mut file), 0x30000);
|
|
file.rewind().unwrap();
|
|
assert_eq!(file.seek_hole(0x30000).unwrap(), None);
|
|
assert_eq!(seek_cur(&mut file), 0);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn rebuild_refcounts() {
|
|
with_basic_file(&valid_header_v3(), |mut disk_file: RawFile| {
|
|
let header = QcowHeader::new(&mut disk_file).expect("Failed to create Header.");
|
|
let cluster_size = 65536;
|
|
let refcount_bits = 1u64 << header.refcount_order;
|
|
let mut raw_file = QcowRawFile::from(disk_file, cluster_size, refcount_bits)
|
|
.expect("Failed to create QcowRawFile.");
|
|
QcowFile::rebuild_refcounts(&mut raw_file, header)
|
|
.expect("Failed to rebuild recounts.");
|
|
});
|
|
}
|
|
|
|
// Helper to create a v3 header with specific incompatible feature bits set
|
|
fn header_v3_with_incompat_features(features: u64) -> Vec<u8> {
|
|
let mut header = valid_header_v3();
|
|
// incompatible_features is at offset 72, big-endian u64
|
|
header[72..80].copy_from_slice(&features.to_be_bytes());
|
|
header
|
|
}
|
|
|
|
// Helper to create a v3 header with specific autoclear feature bits set
|
|
fn header_v3_with_autoclear_features(features: u64) -> Vec<u8> {
|
|
let mut header = valid_header_v3();
|
|
let offset = AUTOCLEAR_FEATURES_OFFSET as usize;
|
|
header[offset..offset + 8].copy_from_slice(&features.to_be_bytes());
|
|
header
|
|
}
|
|
|
|
#[test]
|
|
fn accept_incompat_dirty_bit() {
|
|
let header = header_v3_with_incompat_features(1 << 0);
|
|
with_basic_file(&header, |disk_file: RawFile| {
|
|
let result = QcowFile::from(disk_file);
|
|
assert!(
|
|
result.is_ok(),
|
|
"Expected dirty bit to be accepted, got: {result:?}"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn reject_corrupt_bit_for_writable_open() {
|
|
// Bit 1: corrupt - image metadata is corrupted
|
|
let header = header_v3_with_incompat_features(1 << 1);
|
|
with_basic_file(&header, |disk_file: RawFile| {
|
|
let result = QcowFile::from(disk_file);
|
|
assert!(result.is_err());
|
|
let err = result.unwrap_err();
|
|
assert!(
|
|
matches!(err.kind(), BlockErrorKind::CorruptImage),
|
|
"Expected CorruptImage error, got: {err:?}"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn reject_unsupported_incompat_external_data_bit() {
|
|
// Bit 2: external data file
|
|
let header = header_v3_with_incompat_features(1 << 2);
|
|
with_basic_file(&header, |disk_file: RawFile| {
|
|
let result = QcowFile::from(disk_file);
|
|
assert!(result.is_err());
|
|
let err = result.unwrap_err();
|
|
assert!(matches!(err.kind(), BlockErrorKind::UnsupportedFeature));
|
|
let source = StdError::source(&err).unwrap();
|
|
let qcow_err = source.downcast_ref::<Error>().unwrap();
|
|
assert!(
|
|
matches!(qcow_err, Error::UnsupportedFeature(v) if v.to_string().contains("external")),
|
|
"Expected UnsupportedFeature error mentioning external, got: {err:?}"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn reject_unsupported_incompat_extended_l2_bit() {
|
|
// Bit 4: extended L2 entries
|
|
let header = header_v3_with_incompat_features(1 << 4);
|
|
with_basic_file(&header, |disk_file: RawFile| {
|
|
let result = QcowFile::from(disk_file);
|
|
assert!(result.is_err());
|
|
let err = result.unwrap_err();
|
|
assert!(matches!(err.kind(), BlockErrorKind::UnsupportedFeature));
|
|
let source = StdError::source(&err).unwrap();
|
|
let qcow_err = source.downcast_ref::<Error>().unwrap();
|
|
assert!(
|
|
matches!(qcow_err, Error::UnsupportedFeature(v) if v.to_string().contains("extended")),
|
|
"Expected UnsupportedFeature error mentioning extended, got: {err:?}"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn reject_multiple_unsupported_incompat_bits() {
|
|
// Multiple unsupported bits: external data (2) + extended L2 (4)
|
|
let header = header_v3_with_incompat_features((1 << 2) | (1 << 4));
|
|
with_basic_file(&header, |disk_file: RawFile| {
|
|
let result = QcowFile::from(disk_file);
|
|
assert!(result.is_err());
|
|
assert!(matches!(
|
|
result.unwrap_err().kind(),
|
|
BlockErrorKind::UnsupportedFeature
|
|
));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn reject_unknown_incompat_bit() {
|
|
// Unknown bit 5 (not defined in spec)
|
|
let header = header_v3_with_incompat_features(1 << 5);
|
|
with_basic_file(&header, |disk_file: RawFile| {
|
|
let result = QcowFile::from(disk_file);
|
|
assert!(result.is_err());
|
|
let err = result.unwrap_err();
|
|
assert!(matches!(err.kind(), BlockErrorKind::UnsupportedFeature));
|
|
let source = StdError::source(&err).unwrap();
|
|
let qcow_err = source.downcast_ref::<Error>().unwrap();
|
|
assert!(
|
|
matches!(qcow_err, Error::UnsupportedFeature(v) if v.to_string().contains("unknown")),
|
|
"Expected UnsupportedFeature error mentioning unknown, got: {err:?}"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn dirty_bit_set_on_open_cleared_on_close_v3() {
|
|
// Test that the dirty bit is set when a v3 image is opened and cleared when it's closed
|
|
let header = valid_header_v3();
|
|
with_basic_file(&header, |mut disk_file: RawFile| {
|
|
// Verify dirty bit is not set initially
|
|
disk_file
|
|
.seek(SeekFrom::Start(V2_BARE_HEADER_SIZE as u64))
|
|
.unwrap();
|
|
let features_before = u64::read_be(&mut disk_file).unwrap();
|
|
assert_eq!(
|
|
features_before & IncompatFeatures::DIRTY.bits(),
|
|
0,
|
|
"Dirty bit should not be set initially"
|
|
);
|
|
|
|
// Open the file - this should set the dirty bit
|
|
disk_file.rewind().unwrap();
|
|
{
|
|
let qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap();
|
|
|
|
// Verify dirty bit is set while file is open
|
|
disk_file
|
|
.seek(SeekFrom::Start(V2_BARE_HEADER_SIZE as u64))
|
|
.unwrap();
|
|
let features_during = u64::read_be(&mut disk_file).unwrap();
|
|
assert_ne!(
|
|
features_during & IncompatFeatures::DIRTY.bits(),
|
|
0,
|
|
"Dirty bit should be set while file is open"
|
|
);
|
|
|
|
drop(qcow); // Close the file
|
|
}
|
|
|
|
// Verify dirty bit is cleared after close
|
|
disk_file
|
|
.seek(SeekFrom::Start(V2_BARE_HEADER_SIZE as u64))
|
|
.unwrap();
|
|
let features_after = u64::read_be(&mut disk_file).unwrap();
|
|
assert_eq!(
|
|
features_after & IncompatFeatures::DIRTY.bits(),
|
|
0,
|
|
"Dirty bit should be cleared after close"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn dirty_bit_not_used_for_v2() {
|
|
// Test that v2 images don't use the dirty bit (no incompatible_features field)
|
|
let header = valid_header_v2();
|
|
with_basic_file(&header, |mut disk_file: RawFile| {
|
|
// Open and close v2 file - should work without touching offset 72
|
|
disk_file.rewind().unwrap();
|
|
let qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap();
|
|
assert_eq!(qcow.header.version, 2, "Should be a v2 file");
|
|
drop(qcow);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn dirty_bit_not_set_for_readonly_v3() {
|
|
// Test that read-only v3 files don't set the dirty bit (e.g., backing files)
|
|
let header = valid_header_v3();
|
|
|
|
// Create a temp file with a valid v3 qcow header
|
|
let temp_file = TempFile::new().unwrap();
|
|
let temp_path = temp_file.as_path().to_owned();
|
|
{
|
|
let mut file = temp_file.as_file().try_clone().unwrap();
|
|
file.write_all(&header).unwrap();
|
|
file.set_len(0x1_0000_0000).unwrap();
|
|
}
|
|
|
|
// Open the file read-only
|
|
let readonly_file = OpenOptions::new()
|
|
.read(true)
|
|
.write(false)
|
|
.open(&temp_path)
|
|
.unwrap();
|
|
let raw_file = RawFile::new(readonly_file, false);
|
|
|
|
// Verify the file is detected as read-only
|
|
assert!(
|
|
!raw_file.is_writable(),
|
|
"File should be detected as read-only"
|
|
);
|
|
|
|
// Open as QcowFile - should not set dirty bit for read-only files
|
|
let qcow = QcowFile::from(raw_file).unwrap();
|
|
assert!(
|
|
!qcow.raw_file.file().is_writable(),
|
|
"File should be read-only"
|
|
);
|
|
|
|
// Verify dirty bit was not written to disk
|
|
let verify_file = OpenOptions::new().read(true).open(&temp_path).unwrap();
|
|
let mut verify_raw = RawFile::new(verify_file, false);
|
|
verify_raw
|
|
.seek(SeekFrom::Start(V2_BARE_HEADER_SIZE as u64))
|
|
.unwrap();
|
|
let features = u64::read_be(&mut verify_raw).unwrap();
|
|
assert_eq!(
|
|
features & IncompatFeatures::DIRTY.bits(),
|
|
0,
|
|
"Dirty bit should not be written for read-only files"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn autoclear_features_cleared_on_open() {
|
|
let header = header_v3_with_autoclear_features(0xFFFF_FFFF_FFFF_FFFF);
|
|
with_basic_file(&header, |mut disk_file: RawFile| {
|
|
disk_file
|
|
.seek(SeekFrom::Start(AUTOCLEAR_FEATURES_OFFSET))
|
|
.unwrap();
|
|
let features_before = u64::read_be(&mut disk_file).unwrap();
|
|
assert_eq!(
|
|
features_before, 0xFFFF_FFFF_FFFF_FFFF,
|
|
"Autoclear features should be set initially"
|
|
);
|
|
|
|
disk_file.rewind().unwrap();
|
|
{
|
|
let _qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap();
|
|
}
|
|
|
|
disk_file
|
|
.seek(SeekFrom::Start(AUTOCLEAR_FEATURES_OFFSET))
|
|
.unwrap();
|
|
let features_after = u64::read_be(&mut disk_file).unwrap();
|
|
assert_eq!(
|
|
features_after, 0,
|
|
"Autoclear features should be cleared after open for write"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn autoclear_features_not_cleared_for_readonly() {
|
|
let header = header_v3_with_autoclear_features(0xFFFF_FFFF_FFFF_FFFF);
|
|
|
|
let temp_file = TempFile::new().unwrap();
|
|
let temp_path = temp_file.as_path().to_owned();
|
|
{
|
|
let mut file = temp_file.as_file().try_clone().unwrap();
|
|
file.write_all(&header).unwrap();
|
|
file.set_len(0x1_0000_0000).unwrap();
|
|
}
|
|
|
|
let readonly_file = OpenOptions::new()
|
|
.read(true)
|
|
.write(false)
|
|
.open(&temp_path)
|
|
.unwrap();
|
|
let raw_file = RawFile::new(readonly_file, false);
|
|
let _qcow = QcowFile::from(raw_file).unwrap();
|
|
drop(_qcow);
|
|
|
|
let verify_file = OpenOptions::new().read(true).open(&temp_path).unwrap();
|
|
let mut verify_raw = RawFile::new(verify_file, false);
|
|
verify_raw
|
|
.seek(SeekFrom::Start(AUTOCLEAR_FEATURES_OFFSET))
|
|
.unwrap();
|
|
let features = u64::read_be(&mut verify_raw).unwrap();
|
|
assert_eq!(
|
|
features, 0xFFFF_FFFF_FFFF_FFFF,
|
|
"Autoclear features should NOT be cleared for read-only files"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn autoclear_features_v2_ignored() {
|
|
let header = valid_header_v2();
|
|
with_basic_file(&header, |mut disk_file: RawFile| {
|
|
disk_file.rewind().unwrap();
|
|
let qcow = QcowFile::from(disk_file).unwrap();
|
|
assert_eq!(qcow.header.version, 2);
|
|
assert_eq!(qcow.header.autoclear_features, 0);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn corrupt_image_rejected_for_write() {
|
|
// Test that a corrupt image cannot be opened for writing
|
|
let header = header_v3_with_incompat_features(IncompatFeatures::CORRUPT.bits());
|
|
with_basic_file(&header, |disk_file: RawFile| {
|
|
assert!(disk_file.is_writable(), "File should be writable");
|
|
|
|
let result = QcowFile::from(disk_file);
|
|
assert!(result.is_err());
|
|
let err = result.unwrap_err();
|
|
assert!(
|
|
matches!(err.kind(), BlockErrorKind::CorruptImage),
|
|
"Expected CorruptImage error, got: {err:?}"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn corrupt_image_allowed_readonly() {
|
|
// Test that a corrupt image can be opened read-only
|
|
let header = header_v3_with_incompat_features(IncompatFeatures::CORRUPT.bits());
|
|
|
|
// Create a temp file with the corrupt header
|
|
let temp_file = TempFile::new().unwrap();
|
|
let temp_path = temp_file.as_path().to_owned();
|
|
{
|
|
let mut file = temp_file.as_file().try_clone().unwrap();
|
|
file.write_all(&header).unwrap();
|
|
file.set_len(0x1_0000_0000).unwrap();
|
|
}
|
|
|
|
let readonly_file = OpenOptions::new()
|
|
.read(true)
|
|
.write(false)
|
|
.open(&temp_path)
|
|
.unwrap();
|
|
let raw_file = RawFile::new(readonly_file, false);
|
|
assert!(!raw_file.is_writable(), "File should be read-only");
|
|
|
|
let result = QcowFile::from(raw_file);
|
|
assert!(
|
|
result.is_ok(),
|
|
"Corrupt image should be openable read-only, got: {:?}",
|
|
result.err()
|
|
);
|
|
|
|
let qcow = result.unwrap();
|
|
assert!(qcow.header.is_corrupt(), "Corrupt bit should be set");
|
|
}
|
|
|
|
#[test]
|
|
fn set_corrupt_bit() {
|
|
// Test that set_corrupt_bit correctly sets the corrupt bit
|
|
let header = valid_header_v3();
|
|
with_basic_file(&header, |mut disk_file: RawFile| {
|
|
let mut qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap();
|
|
|
|
assert!(!qcow.header.is_corrupt(), "Should not be corrupt initially");
|
|
|
|
qcow.header
|
|
.set_corrupt_bit(qcow.raw_file.file_mut())
|
|
.unwrap();
|
|
|
|
// Verify in memory
|
|
assert!(qcow.header.is_corrupt(), "Should be corrupt after set");
|
|
|
|
// Verify on disk
|
|
disk_file
|
|
.seek(SeekFrom::Start(V2_BARE_HEADER_SIZE as u64))
|
|
.unwrap();
|
|
let features = u64::read_be(&mut disk_file).unwrap();
|
|
assert!(
|
|
IncompatFeatures::from_bits_retain(features).contains(IncompatFeatures::CORRUPT),
|
|
"Corrupt bit should be set on disk"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn corrupt_bit_persists_with_dirty() {
|
|
// Test that both corrupt and dirty bits can coexist
|
|
let header = header_v3_with_incompat_features(
|
|
IncompatFeatures::CORRUPT.bits() | IncompatFeatures::DIRTY.bits(),
|
|
);
|
|
|
|
let temp_file = TempFile::new().unwrap();
|
|
let temp_path = temp_file.as_path().to_owned();
|
|
{
|
|
let mut file = temp_file.as_file().try_clone().unwrap();
|
|
file.write_all(&header).unwrap();
|
|
file.set_len(0x1_0000_0000).unwrap();
|
|
}
|
|
|
|
// Writable would be rejected due to corrupt bit
|
|
let readonly_file = OpenOptions::new()
|
|
.read(true)
|
|
.write(false)
|
|
.open(&temp_path)
|
|
.unwrap();
|
|
let raw_file = RawFile::new(readonly_file, false);
|
|
|
|
let qcow = QcowFile::from(raw_file).unwrap();
|
|
|
|
let features = IncompatFeatures::from_bits_truncate(qcow.header.incompatible_features);
|
|
assert!(
|
|
features.contains(IncompatFeatures::CORRUPT),
|
|
"Corrupt bit should be set"
|
|
);
|
|
assert!(
|
|
features.contains(IncompatFeatures::DIRTY),
|
|
"Dirty bit should also be set"
|
|
);
|
|
}
|
|
|
|
/// Helper to check if corrupt bit is set on disk by re-reading the header
|
|
fn is_corrupt_on_disk(disk_file: &mut RawFile) -> bool {
|
|
disk_file.rewind().unwrap();
|
|
QcowHeader::new(disk_file).unwrap().is_corrupt()
|
|
}
|
|
|
|
/// Helper to clear the corrupt bit on disk while preserving other bits
|
|
fn clear_corrupt_bit_on_disk(disk_file: &mut RawFile) {
|
|
disk_file
|
|
.seek(SeekFrom::Start(V2_BARE_HEADER_SIZE as u64))
|
|
.unwrap();
|
|
let features = u64::read_be(disk_file).unwrap();
|
|
let mut flags = IncompatFeatures::from_bits_retain(features);
|
|
flags.remove(IncompatFeatures::CORRUPT);
|
|
disk_file
|
|
.seek(SeekFrom::Start(V2_BARE_HEADER_SIZE as u64))
|
|
.unwrap();
|
|
u64::write_be(disk_file, flags.bits()).unwrap();
|
|
assert!(
|
|
!is_corrupt_on_disk(disk_file),
|
|
"Corrupt bit should be cleared"
|
|
);
|
|
}
|
|
|
|
/// Helper to corrupt L1 entry by making L2 table address unaligned.
|
|
///
|
|
/// Returns true if corruption was applied, i.e. the L1 entry was allocated.
|
|
fn corrupt_l1_entry(disk_file: &mut RawFile) -> bool {
|
|
let l1_table_offset = 0x0004_0000u64;
|
|
disk_file.seek(SeekFrom::Start(l1_table_offset)).unwrap();
|
|
let l1_entry = u64::read_be(disk_file).unwrap();
|
|
if l1_entry != 0 {
|
|
let unaligned = l1_entry | 0x200; // Make unaligned
|
|
disk_file.seek(SeekFrom::Start(l1_table_offset)).unwrap();
|
|
u64::write_be(disk_file, unaligned).unwrap();
|
|
disk_file.sync_all().unwrap();
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Helper to corrupt L2 entry by making cluster address unaligned.
|
|
///
|
|
/// Returns true if corruption was applied, i.e. an allocated non-compressed
|
|
/// L2 entry was found.
|
|
fn corrupt_l2_entry(disk_file: &mut RawFile) -> bool {
|
|
let l1_table_offset = 0x0004_0000u64;
|
|
disk_file.seek(SeekFrom::Start(l1_table_offset)).unwrap();
|
|
let l1_entry = u64::read_be(disk_file).unwrap();
|
|
if l1_entry == 0 {
|
|
return false;
|
|
}
|
|
let l2_table_addr = l1_entry & L1_TABLE_OFFSET_MASK;
|
|
disk_file.seek(SeekFrom::Start(l2_table_addr)).unwrap();
|
|
let l2_entry = u64::read_be(disk_file).unwrap();
|
|
if l2_entry != 0 && !l2_entry_is_compressed(l2_entry) {
|
|
let unaligned = l2_entry | 0x200;
|
|
disk_file.seek(SeekFrom::Start(l2_table_addr)).unwrap();
|
|
u64::write_be(disk_file, unaligned).unwrap();
|
|
disk_file.sync_all().unwrap();
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Asserts that read on corrupted disk sets the corrupt bit
|
|
fn assert_corruption_on_read(disk_file: &mut RawFile) {
|
|
clear_corrupt_bit_on_disk(disk_file);
|
|
|
|
disk_file.rewind().unwrap();
|
|
let mut qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap();
|
|
let mut buf = [0u8; 16];
|
|
let result = qcow.read(&mut buf);
|
|
|
|
assert_eq!(
|
|
result.map_err(|e| e.raw_os_error()),
|
|
Err(Some(libc::EIO)),
|
|
"read should fail with EIO on corrupted image"
|
|
);
|
|
assert!(
|
|
is_corrupt_on_disk(disk_file),
|
|
"Corrupt bit should be set after read"
|
|
);
|
|
}
|
|
|
|
/// Asserts that write on corrupted disk sets the corrupt bit
|
|
fn assert_corruption_on_write(disk_file: &mut RawFile) {
|
|
clear_corrupt_bit_on_disk(disk_file);
|
|
|
|
disk_file.rewind().unwrap();
|
|
let mut qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap();
|
|
let result = qcow.write_all(b"overwrite");
|
|
|
|
assert_eq!(
|
|
result.map_err(|e| e.raw_os_error()),
|
|
Err(Some(libc::EIO)),
|
|
"write should fail with EIO on corrupted image"
|
|
);
|
|
assert!(
|
|
is_corrupt_on_disk(disk_file),
|
|
"Corrupt bit should be set after write"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn corrupt_bit_on_unaligned_l2_address() {
|
|
let header = valid_header_v3();
|
|
with_basic_file(&header, |mut disk_file: RawFile| {
|
|
{
|
|
let mut qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap();
|
|
qcow.write_all(b"test data").unwrap();
|
|
}
|
|
|
|
assert!(
|
|
corrupt_l1_entry(&mut disk_file),
|
|
"Failed to corrupt L1 entry - was data written?"
|
|
);
|
|
assert_corruption_on_read(&mut disk_file);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn corrupt_bit_on_unaligned_cluster_address_read() {
|
|
let header = valid_header_v3();
|
|
with_basic_file(&header, |mut disk_file: RawFile| {
|
|
{
|
|
let mut qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap();
|
|
qcow.write_all(b"test data to allocate cluster").unwrap();
|
|
}
|
|
|
|
assert!(
|
|
corrupt_l2_entry(&mut disk_file),
|
|
"Failed to corrupt L2 entry - was cluster allocated?"
|
|
);
|
|
assert_corruption_on_read(&mut disk_file);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn corrupt_bit_on_unaligned_cluster_address_write() {
|
|
let header = valid_header_v3();
|
|
with_basic_file(&header, |mut disk_file: RawFile| {
|
|
{
|
|
let mut qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap();
|
|
qcow.write_all(b"test data to allocate cluster").unwrap();
|
|
}
|
|
|
|
assert!(
|
|
corrupt_l2_entry(&mut disk_file),
|
|
"Failed to corrupt L2 entry - was cluster allocated?"
|
|
);
|
|
assert_corruption_on_write(&mut disk_file);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn corrupt_bit_not_set_on_normal_operations() {
|
|
let header = valid_header_v3();
|
|
with_basic_file(&header, |mut disk_file: RawFile| {
|
|
{
|
|
let mut qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap();
|
|
|
|
qcow.write_all(b"test data 1234567890").unwrap();
|
|
qcow.seek(SeekFrom::Start(0)).unwrap();
|
|
|
|
let mut buf = [0u8; 20];
|
|
qcow.read_exact(&mut buf).unwrap();
|
|
assert_eq!(&buf, b"test data 1234567890");
|
|
|
|
qcow.seek(SeekFrom::Start(0x10000)).unwrap();
|
|
qcow.write_all(b"more data").unwrap();
|
|
|
|
qcow.flush().unwrap();
|
|
}
|
|
|
|
assert!(
|
|
!is_corrupt_on_disk(&mut disk_file),
|
|
"Corrupt bit should NOT be set after normal operations"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn corrupt_bit_v2_image_not_affected() {
|
|
let header = valid_header_v2();
|
|
with_basic_file(&header, |mut disk_file: RawFile| {
|
|
{
|
|
let mut qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap();
|
|
qcow.write_all(b"test data").unwrap();
|
|
qcow.seek(SeekFrom::Start(0)).unwrap();
|
|
let mut buf = [0u8; 9];
|
|
qcow.read_exact(&mut buf).unwrap();
|
|
assert_eq!(&buf, b"test data");
|
|
}
|
|
|
|
disk_file.rewind().unwrap();
|
|
let qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap();
|
|
assert_eq!(qcow.header.version, 2);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn write_zeroes_unallocated_overlay_with_backing_must_read_zero() {
|
|
const CLUSTER_SIZE: usize = 0x10000;
|
|
const OFFSET: u64 = 0x10000;
|
|
|
|
let mut overlay = qcow_overlay_with_backing_pattern(OFFSET, CLUSTER_SIZE, 0xAB);
|
|
let nwritten = overlay
|
|
.write_zeroes_at(OFFSET, CLUSTER_SIZE)
|
|
.expect("failed to zero unallocated overlay cluster");
|
|
assert_eq!(nwritten, CLUSTER_SIZE);
|
|
|
|
let mut buf = [0xFFu8; CLUSTER_SIZE];
|
|
overlay.seek(SeekFrom::Start(OFFSET)).unwrap();
|
|
overlay.read_exact(&mut buf).unwrap();
|
|
assert!(
|
|
buf.iter().all(|&b| b == 0),
|
|
"zeroed unallocated overlay cluster exposed backing data"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn partial_write_after_write_zeroes_must_not_reintroduce_backing_data() {
|
|
const CLUSTER_SIZE: usize = 0x10000;
|
|
const OFFSET: u64 = 0x10000;
|
|
const PATCH_OFFSET: usize = 0x4000;
|
|
const PATCH_LEN: usize = 0x1000;
|
|
|
|
let mut overlay = qcow_overlay_with_backing_pattern(OFFSET, CLUSTER_SIZE, 0xAB);
|
|
overlay.write_zeroes_at(OFFSET, CLUSTER_SIZE).unwrap();
|
|
|
|
let patch = [0x99u8; PATCH_LEN];
|
|
overlay
|
|
.seek(SeekFrom::Start(OFFSET + PATCH_OFFSET as u64))
|
|
.unwrap();
|
|
overlay.write_all(&patch).unwrap();
|
|
|
|
let mut buf = [0xFFu8; CLUSTER_SIZE];
|
|
overlay.seek(SeekFrom::Start(OFFSET)).unwrap();
|
|
overlay.read_exact(&mut buf).unwrap();
|
|
|
|
assert!(buf[..PATCH_OFFSET].iter().all(|&b| b == 0));
|
|
assert!(
|
|
buf[PATCH_OFFSET..PATCH_OFFSET + PATCH_LEN]
|
|
.iter()
|
|
.all(|&b| b == 0x99)
|
|
);
|
|
assert!(buf[PATCH_OFFSET + PATCH_LEN..].iter().all(|&b| b == 0));
|
|
}
|
|
|
|
#[test]
|
|
fn partial_write_after_write_zeroes_large_cluster_must_not_reintroduce_backing_data() {
|
|
const CLUSTER_BITS: u32 = 20;
|
|
const CLUSTER_SIZE: usize = 1 << CLUSTER_BITS;
|
|
const OFFSET: u64 = CLUSTER_SIZE as u64;
|
|
const PATCH_OFFSET: usize = 0x4000;
|
|
const PATCH_LEN: usize = 0x1000;
|
|
|
|
let mut overlay = qcow_overlay_with_backing_pattern_and_cluster_bits(
|
|
OFFSET,
|
|
CLUSTER_SIZE,
|
|
0xAB,
|
|
CLUSTER_BITS,
|
|
);
|
|
overlay.write_zeroes_at(OFFSET, CLUSTER_SIZE).unwrap();
|
|
|
|
let patch = [0x99u8; PATCH_LEN];
|
|
overlay
|
|
.seek(SeekFrom::Start(OFFSET + PATCH_OFFSET as u64))
|
|
.unwrap();
|
|
overlay.write_all(&patch).unwrap();
|
|
|
|
let mut buf = vec![0xFFu8; CLUSTER_SIZE];
|
|
overlay.seek(SeekFrom::Start(OFFSET)).unwrap();
|
|
overlay.read_exact(&mut buf).unwrap();
|
|
|
|
assert!(buf[..PATCH_OFFSET].iter().all(|&b| b == 0));
|
|
assert!(
|
|
buf[PATCH_OFFSET..PATCH_OFFSET + PATCH_LEN]
|
|
.iter()
|
|
.all(|&b| b == 0x99)
|
|
);
|
|
assert!(buf[PATCH_OFFSET + PATCH_LEN..].iter().all(|&b| b == 0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_compressed_read() {
|
|
let cluster_size = 65536usize;
|
|
let data: Vec<u8> = (0..=255).cycle().take(cluster_size).collect();
|
|
let temp = TempFile::new().unwrap();
|
|
{
|
|
let raw_file = RawFile::new(temp.as_file().try_clone().unwrap(), false);
|
|
let mut qcow = QcowFile::new(raw_file, 3, 100 * 1024 * 1024, false).unwrap();
|
|
qcow.seek(SeekFrom::Start(0)).unwrap();
|
|
qcow.write_all(&data).unwrap();
|
|
qcow.flush().unwrap();
|
|
}
|
|
|
|
compress_allocated_clusters(&mut temp.as_file().try_clone().unwrap());
|
|
|
|
let raw_file = RawFile::new(temp.as_file().try_clone().unwrap(), false);
|
|
let mut qcow = QcowFile::from(raw_file).unwrap();
|
|
qcow.seek(SeekFrom::Start(0)).unwrap();
|
|
let mut buf = vec![0u8; cluster_size];
|
|
qcow.read_exact(&mut buf).unwrap();
|
|
assert_eq!(buf, data);
|
|
}
|
|
}
|