From 9baa904a5c58e4dec182acf253797d8771173641 Mon Sep 17 00:00:00 2001 From: Anatol Belski Date: Mon, 26 Jan 2026 15:08:51 +0100 Subject: [PATCH] block: qcow: Add offset alignment checks for corruption detection Validate that L2 table offsets and refcount block offsets are cluster aligned. Set the corrupt bit when unaligned offsets are detected, as this indicates corrupted L1 or refcount table entries. Validate that data cluster offsets from L2 entries are cluster aligned during both reads and writes to existing clusters. Set the corrupt bit when unaligned data cluster offsets are detected. Prevent allocation of clusters at offset 0, which contains the QCOW2 header and should never be allocated. This catches corruption in the available clusters list. Set the corrupt bit when this condition is detected. Signed-off-by: Anatol Belski --- block/src/qcow/mod.rs | 41 +++++++++++++++++++++++++++++++++++--- block/src/qcow/refcount.rs | 6 ++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/block/src/qcow/mod.rs b/block/src/qcow/mod.rs index d44b7b7eb..2343354ca 100644 --- a/block/src/qcow/mod.rs +++ b/block/src/qcow/mod.rs @@ -1616,7 +1616,12 @@ impl QcowFile { // Cluster with zero flag reads as zeros without accessing disk. return Ok(None); } else { - let start = l2_entry_std_cluster_addr(l2_entry) + self.raw_file.cluster_offset(address); + 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)?; @@ -1678,7 +1683,12 @@ impl QcowFile { let refcount = self .refcounts .get_cluster_refcount(&mut self.raw_file, addr) - .map_err(|e| std::io::Error::other(Error::GettingRefcount(e)))?; + .map_err(|e| { + if matches!(e, refcount::Error::RefblockUnaligned(_)) { + self.set_corrupt_bit_best_effort(); + } + io::Error::other(Error::GettingRefcount(e)) + })?; if refcount > 0 { self.set_cluster_refcount_track_freed(addr, refcount - 1)?; } @@ -1701,7 +1711,12 @@ impl QcowFile { self.update_cluster_addr(l1_index, l2_index, cluster_addr, &mut set_refcounts)?; cluster_addr } else { - l2_entry_std_cluster_addr(l2_entry) + 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 { @@ -1748,6 +1763,10 @@ impl QcowFile { fn get_new_cluster(&mut self, initial_data: Option>) -> std::io::Result { // 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 { @@ -1758,6 +1777,10 @@ impl QcowFile { 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)?; } @@ -1868,6 +1891,9 @@ impl QcowFile { .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}"), @@ -1952,6 +1978,11 @@ impl QcowFile { 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; @@ -2028,6 +2059,10 @@ impl QcowFile { 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)); + } } } diff --git a/block/src/qcow/refcount.rs b/block/src/qcow/refcount.rs index 8fa3d5bfe..5cd61c09b 100644 --- a/block/src/qcow/refcount.rs +++ b/block/src/qcow/refcount.rs @@ -20,6 +20,9 @@ pub enum Error { /// `InvalidIndex` - Address requested isn't within the range of the disk. #[error("Address requested is not within the range of the disk")] InvalidIndex, + /// `RefblockUnaligned` - Refcount block offset is not cluster aligned. + #[error("Refcount block offset {0:#x} is not cluster aligned")] + RefblockUnaligned(u64), /// `NeedCluster` - Handle this error by reading the cluster and calling the function again. #[error("Cluster with addr={0} needs to be read")] NeedCluster(u64), @@ -202,6 +205,9 @@ impl RefCount { if block_addr_disk == 0 { return Ok(0); } + if block_addr_disk & (self.cluster_size - 1) != 0 { + return Err(Error::RefblockUnaligned(block_addr_disk)); + } if !self.refblock_cache.contains_key(table_index) { let table = VecCache::from_vec( raw_file