From 2ea2209920b173920bca7467e8efe40dc03d9e03 Mon Sep 17 00:00:00 2001 From: Anatol Belski Date: Wed, 15 Jul 2026 21:44:46 +0200 Subject: [PATCH] block: qcow: Test reopen finds no stranded clusters After churning a single L2 region with an fsync per write, close and reopen the image. parse_qcow rebuilds the free list from the on disk refcounts, so a reopen that recovers many clusters means the runtime allocator stranded them. Without the fix the reopen recovered every relocated cluster. Now it finds none. Assisted-by: Claude:Opus-4.8 Signed-off-by: Anatol Belski --- block/src/formats/qcow/engine_sync.rs | 35 +++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/block/src/formats/qcow/engine_sync.rs b/block/src/formats/qcow/engine_sync.rs index c59178ef1..44b6a327c 100644 --- a/block/src/formats/qcow/engine_sync.rs +++ b/block/src/formats/qcow/engine_sync.rs @@ -547,6 +547,41 @@ mod unit_tests { ); } + // Reopening rebuilds the free list from the on-disk refcounts. With the + // clusters tracked at runtime, a reopen must not discover a pile of them. + #[test] + fn reopen_discovers_no_stranded_clusters() { + 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 tracked_before = disk.metadata().free_list_len(); + + drop(disk); + temp.as_file().sync_all().unwrap(); + let disk = QcowDisk::new( + temp.as_file().try_clone().unwrap(), + false, + false, + false, + false, + ) + .unwrap(); + let tracked_after = disk.metadata().free_list_len(); + + assert!( + tracked_after <= tracked_before + 8, + "reopen recovered {} clusters the allocator had stranded", + tracked_after.saturating_sub(tracked_before), + ); + } + #[test] fn test_qcow_sync_rejects_out_of_bounds_allocated_l2_entry_on_read() { let data = vec![0x5a; 4096];