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 <anbelski@linux.microsoft.com>
This commit is contained in:
Anatol Belski
2025-11-30 20:14:35 +01:00
committed by Rob Bradford
parent 9e2e85a48f
commit efad6578d1
2 changed files with 30 additions and 0 deletions

View File

@@ -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() {

View File

@@ -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;