From edaeaed5f7e77663f43625e06a0511ea187a78cd Mon Sep 17 00:00:00 2001 From: Anatol Belski Date: Mon, 26 Jan 2026 16:39:06 +0100 Subject: [PATCH] tests: qcow: Add corrupt bit integration tests Add integration tests for QCOW2 corrupt bit handling. Verify that images with the corrupt bit set are rejected for writable access but allowed for read-only access with a warning. Helper functions are added to read and modify the corrupt flag in the QCOW2 v3 header. Signed-off-by: Anatol Belski --- cloud-hypervisor/tests/integration.rs | 166 +++++++++++++++++++++++++- 1 file changed, 163 insertions(+), 3 deletions(-) diff --git a/cloud-hypervisor/tests/integration.rs b/cloud-hypervisor/tests/integration.rs index 6a7acfaa6..38b970c1a 100644 --- a/cloud-hypervisor/tests/integration.rs +++ b/cloud-hypervisor/tests/integration.rs @@ -3549,17 +3549,48 @@ mod common_parallel { serde_json::from_slice(&output.stdout).ok() } - fn check_dirty_flag(path: &std::path::Path) -> Result, String> { + fn get_qcow2_v3_info(path: &Path) -> Result, String> { let info = get_image_info(path) .ok_or_else(|| format!("qemu-img info failed for {}", path.display()))?; if info["format"].as_str() != Some("qcow2") { return Ok(None); } - // QCOW2 v3 has compat "1.1", v2 has "0.10" and doesn't support dirty flag + // QCOW2 v3 has compat "1.1", v2 has "0.10" if info["format-specific"]["data"]["compat"].as_str() != Some("1.1") { return Ok(None); } - Ok(info["dirty-flag"].as_bool()) + Ok(Some(info)) + } + + fn check_dirty_flag(path: &Path) -> Result, String> { + Ok(get_qcow2_v3_info(path)?.and_then(|info| info["dirty-flag"].as_bool())) + } + + fn check_corrupt_flag(path: &Path) -> Result, String> { + Ok(get_qcow2_v3_info(path)? + .and_then(|info| info["format-specific"]["data"]["corrupt"].as_bool())) + } + + const QCOW2_INCOMPATIBLE_FEATURES_OFFSET: u64 = 72; + + fn set_corrupt_flag(path: &Path, corrupt: bool) -> io::Result<()> { + let mut file = OpenOptions::new().read(true).write(true).open(path)?; + + file.seek(SeekFrom::Start(QCOW2_INCOMPATIBLE_FEATURES_OFFSET))?; + let mut buf = [0u8; 8]; + file.read_exact(&mut buf)?; + let mut features = u64::from_be_bytes(buf); + + if corrupt { + features |= 0x02; + } else { + features &= !0x02; + } + + file.seek(SeekFrom::Start(QCOW2_INCOMPATIBLE_FEATURES_OFFSET))?; + file.write_all(&features.to_be_bytes())?; + file.sync_all()?; + Ok(()) } fn resolve_disk_path(path_or_image_name: impl AsRef) -> std::path::PathBuf { @@ -3884,6 +3915,135 @@ mod common_parallel { disk_check_consistency(&test_image_path, None); } + #[test] + fn test_virtio_block_qcow2_corrupt_bit_rejected_for_write() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); + + let test_image_path = guest.tmp_dir.as_path().join("test-corrupt.qcow2"); + let original_image = guest.disk_config.disk(DiskType::OperatingSystem).unwrap(); + + copy(original_image, &test_image_path).expect("Failed to copy qcow2 image"); + + assert_eq!( + check_corrupt_flag(&test_image_path).expect("Failed to check corrupt flag"), + Some(false), + "Image should start with corrupt bit cleared" + ); + + set_corrupt_flag(&test_image_path, true).expect("Failed to set corrupt flag"); + + assert_eq!( + check_corrupt_flag(&test_image_path).expect("Failed to check corrupt flag"), + Some(true), + "Corrupt bit should be set" + ); + + let child = GuestCommand::new(&guest) + .args(["--cpus", "boot=1"]) + .args(["--memory", "size=512M"]) + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args([ + "--disk", + &format!("path={}", test_image_path.to_str().unwrap()), + &format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ), + ]) + .default_net() + .capture_output() + .spawn() + .unwrap(); + + let output = child.wait_with_output().unwrap(); + assert!( + !output.status.success(), + "VM should fail to start with corrupt disk image" + ); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("corrupt") || stderr.contains("Corrupt"), + "Error message should mention corruption: {stderr}" + ); + } + + #[test] + fn test_virtio_block_qcow2_corrupt_bit_allowed_readonly() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); + + let test_image_path = guest.tmp_dir.as_path().join("test-corrupt-ro.qcow2"); + let original_image = guest.disk_config.disk(DiskType::OperatingSystem).unwrap(); + + copy(original_image, &test_image_path).expect("Failed to copy qcow2 image"); + + set_corrupt_flag(&test_image_path, true).expect("Failed to set corrupt flag"); + + assert_eq!( + check_corrupt_flag(&test_image_path).expect("Failed to check corrupt flag"), + Some(true), + "Corrupt bit should be set" + ); + + let mut child = GuestCommand::new(&guest) + .args(["--cpus", "boot=1"]) + .args(["--memory", "size=512M"]) + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args([ + "--disk", + &format!("path={},readonly=on", test_image_path.to_str().unwrap()), + &format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ), + ]) + .default_net() + .capture_output() + .spawn() + .unwrap(); + + thread::sleep(Duration::from_secs(5)); + + match child.try_wait() { + Ok(Some(status)) => { + let output = child.wait_with_output().unwrap(); + let stderr = String::from_utf8_lossy(&output.stderr); + panic!( + "VM should not have exited when opening corrupt image as readonly. Exit status: {}, stderr: {}", + status, stderr + ); + } + Ok(None) => { + // VM is still running as expected + } + Err(e) => { + panic!("Error checking process status: {}", e); + } + } + + let _ = unsafe { libc::kill(child.id() as i32, libc::SIGKILL) }; + let output = child.wait_with_output().unwrap(); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("QCOW2 image is marked corrupt, opening read-only"), + "Expected warning about corrupt image being opened read-only. stderr: {}", + stderr + ); + + assert_eq!( + check_corrupt_flag(&test_image_path).expect("Failed to check corrupt flag"), + Some(true), + "Corrupt bit should remain set for read-only access" + ); + } + #[test] fn test_virtio_block_vhd() { let mut workload_path = dirs::home_dir().unwrap();