From efad6578d1ee3d094359c56f14f1f5c0d8ec81a0 Mon Sep 17 00:00:00 2001 From: Anatol Belski Date: Sun, 30 Nov 2025 20:14:35 +0100 Subject: [PATCH] block: qcow: Fix refcount leak when converting compressed clusters When converting a compressed cluster to standard during write operations, the old compressed cluster's refcount was never decremented, causing leak warnings by `qemu-img check ..` `Leaked cluster X refcount=N reference=M` Additionally, compressed data can span multiple physical clusters, not just one. The compressed cluster address and size are encoded in the L2 entry, and the data may cross cluster boundaries. The proper handling is implemented as follows: - Extract compressed cluster address and size before overwriting L2 entry - Identify all clusters occupied by the compressed data - Decrement refcount for each cluster in the range Signed-off-by: Anatol Belski --- block/src/qcow/mod.rs | 25 +++++++++++++++++++++++++ block/src/qcow/qcow_raw_file.rs | 5 +++++ 2 files changed, 30 insertions(+) diff --git a/block/src/qcow/mod.rs b/block/src/qcow/mod.rs index 400e30a82..6effc468f 100644 --- a/block/src/qcow/mod.rs +++ b/block/src/qcow/mod.rs @@ -1204,6 +1204,10 @@ impl QcowFile { 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. + + let (compressed_cluster_addr, compressed_cluster_size) = + l2_entry_compressed_cluster_layout(l2_entry, self.header.cluster_bits); + // Allocate new cluster, decompress into new cluster, then use // offset of new cluster. let decompressed_cluster = self.decompress_l2_cluster(l2_entry)?; @@ -1216,6 +1220,27 @@ impl QcowFile { if nwritten != decompressed_cluster.len() { return Err(std::io::Error::from_raw_os_error(EIO)); } + + // Decrement refcount for each cluster spanned by the old compressed data + 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 + ); + 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| std::io::Error::other(Error::GettingRefcount(e)))?; + if refcount > 0 { + let mut newly_unref = self.set_cluster_refcount(addr, refcount - 1)?; + self.unref_clusters.append(&mut newly_unref); + } + addr += self.raw_file.cluster_size(); + } + cluster_addr } else if l2_entry_is_empty(l2_entry) { let initial_data = if let Some(backing) = self.backing_file.as_mut() { diff --git a/block/src/qcow/qcow_raw_file.rs b/block/src/qcow/qcow_raw_file.rs index d3a9ad7ab..1d9dbc0b5 100644 --- a/block/src/qcow/qcow_raw_file.rs +++ b/block/src/qcow/qcow_raw_file.rs @@ -136,6 +136,11 @@ impl QcowRawFile { address & self.cluster_mask } + /// Returns the base address of the cluster containing `address`. + pub fn cluster_address(&self, address: u64) -> u64 { + address & !self.cluster_mask + } + /// Zeros out a cluster in the file. pub fn zero_cluster(&mut self, address: u64) -> io::Result<()> { let cluster_size = self.cluster_size as usize;