From 365d6b8017153aa3fb044905ee8e5d02f11dbd4c Mon Sep 17 00:00:00 2001 From: Anatol Belski Date: Wed, 15 Jul 2026 18:23:47 +0200 Subject: [PATCH] block: qcow: Test relocated metadata cluster reuse Add a regression test that churns one L2 region with an fsync per write, relocating the L2 table and its refcount block every iteration, and asserts committed host blocks track live data instead of doubling. Data written across the churn is read back to confirm integrity. Assisted-by: Claude:Opus-4.8 Signed-off-by: Anatol Belski --- block/src/formats/qcow/engine_sync.rs | 42 +++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/block/src/formats/qcow/engine_sync.rs b/block/src/formats/qcow/engine_sync.rs index f02c6c43f..af75ace58 100644 --- a/block/src/formats/qcow/engine_sync.rs +++ b/block/src/formats/qcow/engine_sync.rs @@ -474,6 +474,48 @@ mod unit_tests { assert_eq!(result as usize, data.len()); } + fn async_fsync(disk: &QcowDisk) { + let mut async_io = disk.create_async_io(1).unwrap(); + async_io.fsync(Some(7)).unwrap(); + let (user_data, _result) = next_completion(async_io.as_mut()); + assert_eq!(user_data, 7); + } + + // Freed relocation clusters must be reused so committed blocks track + // live data. + #[test] + fn relocated_metadata_clusters_are_reused() { + use std::os::unix::fs::MetadataExt; + const CL: u64 = 65536; + let virtual_size = 512 * 1024 * 1024; // one L2 table + 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); + } + + temp.as_file().sync_all().unwrap(); + let committed = (temp.as_file().metadata().unwrap().blocks() * 512) / CL; + // Before the fix committed grew to ~2 * n. + assert!( + committed <= n + 64, + "committed {committed} clusters far exceeds {n} live data clusters; \ + relocated metadata clusters are being stranded instead of reused", + ); + + for i in 0..n { + let got = async_read(&disk, i * CL, CL as usize); + assert_eq!( + got, + vec![(i as u8).wrapping_add(1); CL as usize], + "cluster {i} data mismatch", + ); + } + } + #[test] fn test_qcow_sync_rejects_out_of_bounds_allocated_l2_entry_on_read() { let data = vec![0x5a; 4096];