From 8acefad89350dc948e8b4ba348bbb963a31a5eb1 Mon Sep 17 00:00:00 2001 From: Anatol Belski Date: Wed, 15 Jul 2026 21:12:54 +0200 Subject: [PATCH] block: qcow: Test freed clusters stay on the free list After churning a single L2 region with an fsync per write, scan the file for refcount==0 clusters and assert every one is on the runtime free list. The reuse bug left relocated refcount block clusters free on disk but off the list, so this fails without the fix, reporting the exact count of stranded clusters. Assisted-by: Claude:Opus-4.8 Signed-off-by: Anatol Belski --- block/src/formats/qcow/engine_sync.rs | 31 +++++++++++++++++++++++++++ block/src/formats/qcow/metadata.rs | 6 ++++++ 2 files changed, 37 insertions(+) diff --git a/block/src/formats/qcow/engine_sync.rs b/block/src/formats/qcow/engine_sync.rs index af75ace58..c59178ef1 100644 --- a/block/src/formats/qcow/engine_sync.rs +++ b/block/src/formats/qcow/engine_sync.rs @@ -516,6 +516,37 @@ mod unit_tests { } } + // Every refcount==0 cluster in the file must be on the runtime free list. + // The bug left relocated refcount-block clusters free on disk yet absent + // from the list, so the allocator never reused them. + #[test] + fn freed_clusters_are_tracked_in_free_list() { + const CL: u64 = 65536; + let virtual_size = 512 * 1024 * 1024; + let (temp, disk) = create_disk_with_data(virtual_size, &[], 0, false, false); + let n: u64 = 400; + + for i in 0..n { + let pattern = vec![(i as u8).wrapping_add(1); CL as usize]; + async_write(&disk, i * CL, &pattern); + async_fsync(&disk); + } + + let file_clusters = temp.as_file().metadata().unwrap().len() / CL; + let mut free_on_disk = 0u64; + for c in 0..file_clusters { + if disk.metadata().cluster_refcount(c * CL).unwrap() == 0 { + free_on_disk += 1; + } + } + let tracked = disk.metadata().free_list_len() as u64; + assert_eq!( + free_on_disk, tracked, + "{free_on_disk} free clusters on disk but {tracked} tracked; \ + relocated clusters are stranded off the free list", + ); + } + #[test] fn test_qcow_sync_rejects_out_of_bounds_allocated_l2_entry_on_read() { let data = vec![0x5a; 4096]; diff --git a/block/src/formats/qcow/metadata.rs b/block/src/formats/qcow/metadata.rs index 48f67af4b..18711c92c 100644 --- a/block/src/formats/qcow/metadata.rs +++ b/block/src/formats/qcow/metadata.rs @@ -357,6 +357,12 @@ impl QcowMetadata { self.inner.read().unwrap().header.clone() } + #[cfg(test)] + pub fn free_list_len(&self) -> usize { + let inner = self.inner.read().unwrap(); + inner.avail_clusters.len() + inner.unref_clusters.len() + } + #[cfg(test)] pub fn cluster_refcount(&self, address: u64) -> io::Result { let mut inner = self.inner.write().unwrap();