diff --git a/Cargo.lock b/Cargo.lock index 6545d6013..8b3208e29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -322,6 +322,7 @@ checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" name = "block" version = "0.1.0" dependencies = [ + "bitflags 2.10.0", "byteorder", "crc-any", "flate2", diff --git a/block/Cargo.toml b/block/Cargo.toml index c038b0cd8..70a731a73 100644 --- a/block/Cargo.toml +++ b/block/Cargo.toml @@ -9,6 +9,7 @@ default = [] io_uring = ["dep:io-uring"] [dependencies] +bitflags = { workspace = true } byteorder = { workspace = true } crc-any = "2.5.0" flate2 = "1.1" diff --git a/block/src/qcow/mod.rs b/block/src/qcow/mod.rs index e3aa04349..ae4dd4eed 100644 --- a/block/src/qcow/mod.rs +++ b/block/src/qcow/mod.rs @@ -18,6 +18,7 @@ use std::mem::size_of; use std::os::fd::{AsRawFd, RawFd}; use std::str::{self, FromStr}; +use bitflags::bitflags; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use libc::{EINVAL, EIO, ENOSPC}; use log::error; @@ -116,6 +117,8 @@ pub enum Error { UnsupportedBackingFileFormat(String), #[error("Unsupported compression type")] UnsupportedCompressionType, + #[error("Unsupported qcow2 feature(s)")] + UnsupportedFeature(#[source] MissingFeatureError), #[error("Unsupported refcount order")] UnsupportedRefcountOrder, #[error("Unsupported version: {0}")] @@ -207,6 +210,76 @@ const COMPRESSION_TYPE_ZSTD: u64 = 1; // zstd const HEADER_EXT_END: u32 = 0x00000000; // Backing file format name (raw, qcow2) const HEADER_EXT_BACKING_FORMAT: u32 = 0xe2792aca; +// Feature name table +const HEADER_EXT_FEATURE_NAME_TABLE: u32 = 0x6803f857; + +// Feature name table entry type incompatible +const FEAT_TYPE_INCOMPATIBLE: u8 = 0; + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct IncompatFeatures: u64 { + const DIRTY = 1 << 0; + const CORRUPT = 1 << 1; + const DATA_FILE = 1 << 2; + const COMPRESSION = 1 << 3; + const EXTENDED_L2 = 1 << 4; + } +} + +impl IncompatFeatures { + /// Features supported by this implementation. + const SUPPORTED: IncompatFeatures = IncompatFeatures::COMPRESSION; + + /// Get the fallback name for a known feature bit. + fn flag_name(bit: u8) -> Option<&'static str> { + Some(match Self::from_bits_truncate(1u64 << bit) { + Self::DIRTY => "dirty bit", + Self::CORRUPT => "corrupt bit", + Self::DATA_FILE => "external data file", + Self::EXTENDED_L2 => "extended L2 entries", + _ => return None, + }) + } +} + +/// Error type for unsupported incompatible features. +#[derive(Debug, Clone, Error)] +pub struct MissingFeatureError { + /// Unsupported feature bits. + features: IncompatFeatures, + /// Feature name table from the qcow2 image. + feature_names: Vec<(u8, String)>, +} + +impl MissingFeatureError { + fn new(features: IncompatFeatures, feature_names: Vec<(u8, String)>) -> Self { + Self { + features, + feature_names, + } + } +} + +impl Display for MissingFeatureError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let names: Vec = (0u8..64) + .filter(|&bit| self.features.bits() & (1u64 << bit) != 0) + .map(|bit| { + // First try the image's feature name table + self.feature_names + .iter() + .find(|(b, _)| *b == bit) + .map(|(_, name)| name.clone()) + // Then try hardcoded fallback names + .or_else(|| IncompatFeatures::flag_name(bit).map(|s| s.to_string())) + // Finally, use generic description + .unwrap_or_else(|| format!("unknown feature bit {bit}")) + }) + .collect(); + write!(f, "Missing features: {}", names.join(", ")) + } +} // The format supports a "header extension area", that crosvm does not use. const QCOW_EMPTY_HEADER_EXTENSION_SIZE: u32 = 8; @@ -289,7 +362,12 @@ pub struct QcowHeader { } impl QcowHeader { - fn read_header_extensions(f: &mut RawFile, header: &mut QcowHeader) -> Result<()> { + /// Read header extensions, optionally collecting feature names for error reporting. + fn read_header_extensions( + f: &mut RawFile, + header: &mut QcowHeader, + mut feature_table: Option<&mut Vec<(u8, String)>>, + ) -> Result<()> { // Extensions start directly after the header f.seek(SeekFrom::Start(header.header_size as u64)) .map_err(Error::ReadingHeader)?; @@ -313,6 +391,21 @@ impl QcowHeader { backing_file.format = Some(format_str.parse()?); } } + HEADER_EXT_FEATURE_NAME_TABLE if feature_table.is_some() => { + const FEATURE_NAME_ENTRY_SIZE: usize = 1 + 1 + 46; // type + bit + name + let mut data = vec![0u8; ext_length as usize]; + f.read_exact(&mut data).map_err(Error::ReadingHeader)?; + let table = feature_table.as_mut().unwrap(); + for entry in data.chunks_exact(FEATURE_NAME_ENTRY_SIZE) { + if entry[0] == FEAT_TYPE_INCOMPATIBLE { + let bit_number = entry[1]; + let name_bytes = &entry[2..]; + let name_len = name_bytes.iter().position(|&b| b == 0).unwrap_or(46); + let name = String::from_utf8_lossy(&name_bytes[..name_len]).to_string(); + table.push((bit_number, name)); + } + } + } _ => { // Skip unknown extension f.seek(SeekFrom::Current(ext_length as i64)) @@ -415,8 +508,26 @@ impl QcowHeader { header.backing_file = Some(BackingFileConfig { path, format: None }); } - if version == 3 && header.header_size > V3_BARE_HEADER_SIZE { - Self::read_header_extensions(f, &mut header)?; + if version == 3 { + // Check for unsupported incompatible features first + let features = IncompatFeatures::from_bits_retain(header.incompatible_features); + let unsupported = features - IncompatFeatures::SUPPORTED; + if !unsupported.is_empty() { + // Read extensions only to get feature names for error reporting + let mut feature_table = Vec::new(); + if header.header_size > V3_BARE_HEADER_SIZE { + let _ = Self::read_header_extensions(f, &mut header, Some(&mut feature_table)); + } + return Err(Error::UnsupportedFeature(MissingFeatureError::new( + unsupported, + feature_table, + ))); + } + + // Features OK, now read extensions normally + if header.header_size > V3_BARE_HEADER_SIZE { + Self::read_header_extensions(f, &mut header, None)?; + } } Ok(header) @@ -2389,7 +2500,7 @@ mod unit_tests { format: None, }); - QcowHeader::read_header_extensions(&mut disk_file, &mut header).unwrap(); + QcowHeader::read_header_extensions(&mut disk_file, &mut header, None).unwrap(); assert_eq!(header.backing_file.as_ref().and_then(|bf| bf.format), None); } @@ -2403,7 +2514,7 @@ mod unit_tests { format: None, }); - QcowHeader::read_header_extensions(&mut disk_file, &mut header).unwrap(); + 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) @@ -2420,7 +2531,7 @@ mod unit_tests { format: None, }); - QcowHeader::read_header_extensions(&mut disk_file, &mut header).unwrap(); + 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) @@ -2437,7 +2548,7 @@ mod unit_tests { format: None, }); - let result = QcowHeader::read_header_extensions(&mut disk_file, &mut header); + let result = QcowHeader::read_header_extensions(&mut disk_file, &mut header, None); assert!(matches!( result.unwrap_err(), Error::UnsupportedBackingFileFormat(_) @@ -2451,7 +2562,7 @@ mod unit_tests { &[0xFF, 0xFE, 0xFD], // invalid UTF-8 ); - let result = QcowHeader::read_header_extensions(&mut disk_file, &mut header); + let result = QcowHeader::read_header_extensions(&mut disk_file, &mut header, None); // Should fail with InvalidBackingFileName error assert!(matches!( result.unwrap_err(),