Skip to content

Commit 4f69cf6

Browse files
weltlingrbradford
authored andcommitted
tests: qcow: Add backing file verification for qcow2 images
- Automatically detect and verify backing files - Verify backing file integrity with qemu-img check (qcow only) - Compute DJB2 checksums before test to detect modifications Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
1 parent 3fed706 commit 4f69cf6

File tree

3 files changed

+170
-18
lines changed

3 files changed

+170
-18
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cloud-hypervisor/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ vmm-sys-util = { workspace = true }
4141
zbus = { version = "5.13.1", optional = true }
4242

4343
[dev-dependencies]
44+
block = { path = "../block" }
4445
dirs = { workspace = true }
4546
net_util = { path = "../net_util" }
4647
serde_json = { workspace = true }

cloud-hypervisor/tests/integration.rs

Lines changed: 168 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2527,8 +2527,9 @@ EOF
25272527
}
25282528

25292529
mod common_parallel {
2530-
use std::fs::OpenOptions;
2531-
use std::io::SeekFrom;
2530+
use std::cmp;
2531+
use std::fs::{File, OpenOptions};
2532+
use std::io::{self, SeekFrom};
25322533

25332534
use crate::*;
25342535

@@ -3425,6 +3426,12 @@ mod common_parallel {
34253426

34263427
let kernel_path = direct_kernel_boot_path();
34273428

3429+
let initial_backing_checksum = if verify_os_disk {
3430+
compute_backing_checksum(guest.disk_config.disk(DiskType::OperatingSystem).unwrap())
3431+
} else {
3432+
None
3433+
};
3434+
34283435
let mut cloud_child = GuestCommand::new(&guest)
34293436
.args(["--cpus", "boot=4"])
34303437
.args(["--memory", "size=512M,shared=on"])
@@ -3498,7 +3505,10 @@ mod common_parallel {
34983505
handle_child_output(r, &output);
34993506

35003507
if verify_os_disk {
3501-
disk_check_consistency(guest.disk_config.disk(DiskType::OperatingSystem).unwrap());
3508+
disk_check_consistency(
3509+
guest.disk_config.disk(DiskType::OperatingSystem).unwrap(),
3510+
initial_backing_checksum,
3511+
);
35023512
}
35033513
}
35043514

@@ -3517,35 +3527,175 @@ mod common_parallel {
35173527
_test_virtio_block(FOCAL_IMAGE_NAME, true, true, false);
35183528
}
35193529

3520-
/// Uses `qemu-img check` to verify disk image consistency.
3521-
///
3522-
/// Supported formats are `qcow2` (compressed and uncompressed),
3523-
/// `vhdx`, `qed`, `parallels`, `vmdk`, and `vdi`. See man page
3524-
/// for more details.
3525-
///
3526-
/// It takes either a full path to the image or just the name of
3527-
/// the image located in the `workloads` directory.
3528-
fn disk_check_consistency(path_or_image_name: impl AsRef<std::path::Path>) {
3529-
let path = if path_or_image_name.as_ref().exists() {
3530+
fn run_qemu_img(path: &std::path::Path, args: &[&str]) -> std::process::Output {
3531+
std::process::Command::new("qemu-img")
3532+
.arg(args[0])
3533+
.args(&args[1..])
3534+
.arg(path.to_str().unwrap())
3535+
.output()
3536+
.unwrap()
3537+
}
3538+
3539+
fn get_image_info(path: &std::path::Path) -> Option<serde_json::Value> {
3540+
let output = run_qemu_img(path, &["info", "--output=json"]);
3541+
3542+
output.status.success().then(|| ())?;
3543+
serde_json::from_slice(&output.stdout).ok()
3544+
}
3545+
3546+
fn resolve_disk_path(path_or_image_name: impl AsRef<std::path::Path>) -> std::path::PathBuf {
3547+
if path_or_image_name.as_ref().exists() {
35303548
// A full path is provided
35313549
path_or_image_name.as_ref().to_path_buf()
35323550
} else {
35333551
// An image name is provided
35343552
let mut workload_path = dirs::home_dir().unwrap();
35353553
workload_path.push("workloads");
35363554
workload_path.as_path().join(path_or_image_name.as_ref())
3555+
}
3556+
}
3557+
3558+
fn compute_file_checksum(reader: &mut dyn std::io::Read, size: u64) -> u32 {
3559+
// Read first 16MB or entire data if smaller
3560+
let read_size = cmp::min(size, 16 * 1024 * 1024) as usize;
3561+
3562+
let mut buffer = vec![0u8; read_size];
3563+
reader.read_exact(&mut buffer).unwrap();
3564+
3565+
// DJB2 hash
3566+
let mut hash: u32 = 5381;
3567+
for byte in buffer.iter() {
3568+
hash = hash.wrapping_mul(33).wrapping_add(*byte as u32);
3569+
}
3570+
hash
3571+
}
3572+
3573+
#[test]
3574+
fn test_compute_file_checksum_empty() {
3575+
let mut reader = io::Cursor::new(vec![]);
3576+
let checksum = compute_file_checksum(&mut reader, 0);
3577+
assert_eq!(checksum, 5381);
3578+
}
3579+
3580+
#[test]
3581+
fn test_compute_file_checksum_small() {
3582+
let data = b"hello world";
3583+
let mut reader = io::Cursor::new(data);
3584+
let checksum = compute_file_checksum(&mut reader, data.len() as u64);
3585+
assert_eq!(checksum, 894552257);
3586+
}
3587+
3588+
#[test]
3589+
fn test_compute_file_checksum_same_data() {
3590+
let data = b"test data 123";
3591+
let mut reader1 = io::Cursor::new(data);
3592+
let mut reader2 = io::Cursor::new(data);
3593+
let checksum1 = compute_file_checksum(&mut reader1, data.len() as u64);
3594+
let checksum2 = compute_file_checksum(&mut reader2, data.len() as u64);
3595+
assert_eq!(checksum1, checksum2);
3596+
}
3597+
3598+
#[test]
3599+
fn test_compute_file_checksum_different_data() {
3600+
let data1 = b"data1";
3601+
let data2 = b"data2";
3602+
let mut reader1 = io::Cursor::new(data1);
3603+
let mut reader2 = io::Cursor::new(data2);
3604+
let checksum1 = compute_file_checksum(&mut reader1, data1.len() as u64);
3605+
let checksum2 = compute_file_checksum(&mut reader2, data2.len() as u64);
3606+
assert_ne!(checksum1, checksum2);
3607+
}
3608+
3609+
#[test]
3610+
fn test_compute_file_checksum_large_data() {
3611+
let size = 20 * 1024 * 1024;
3612+
let data = vec![0xABu8; size];
3613+
let mut reader = io::Cursor::new(data);
3614+
let checksum = compute_file_checksum(&mut reader, size as u64);
3615+
// Should only read first 16MB
3616+
assert!(checksum != 5381);
3617+
3618+
// Verify only 16MB was read
3619+
let position = reader.position();
3620+
assert_eq!(position, 16 * 1024 * 1024);
3621+
}
3622+
3623+
fn compute_backing_checksum(
3624+
path_or_image_name: impl AsRef<std::path::Path>,
3625+
) -> Option<(std::path::PathBuf, String, u32)> {
3626+
let path = resolve_disk_path(path_or_image_name);
3627+
3628+
let mut file = File::open(&path).ok()?;
3629+
if !matches!(
3630+
block::detect_image_type(&mut file).ok()?,
3631+
block::ImageType::Qcow2
3632+
) {
3633+
return None;
3634+
}
3635+
3636+
let info = get_image_info(&path)?;
3637+
3638+
let backing_file = info["backing-filename"].as_str()?;
3639+
let backing_path = if std::path::Path::new(backing_file).is_absolute() {
3640+
std::path::PathBuf::from(backing_file)
3641+
} else {
3642+
path.parent()
3643+
.unwrap_or_else(|| std::path::Path::new("."))
3644+
.join(backing_file)
35373645
};
35383646

3539-
let output = std::process::Command::new("qemu-img")
3540-
.args(["check", path.to_str().unwrap()])
3541-
.output()
3542-
.expect("should spawn and run command successfully");
3647+
let backing_info = get_image_info(&backing_path)?;
3648+
let backing_format = backing_info["format"].as_str()?.to_string();
3649+
let mut file = File::open(&backing_path).ok()?;
3650+
let file_size = file.metadata().ok()?.len();
3651+
let checksum = compute_file_checksum(&mut file, file_size);
3652+
3653+
Some((backing_path, backing_format, checksum))
3654+
}
3655+
3656+
/// Uses `qemu-img check` to verify disk image consistency.
3657+
///
3658+
/// Supported formats are `qcow2` (compressed and uncompressed),
3659+
/// `vhdx`, `qed`, `parallels`, `vmdk`, and `vdi`. See man page
3660+
/// for more details.
3661+
///
3662+
/// It takes either a full path to the image or just the name of
3663+
/// the image located in the `workloads` directory.
3664+
///
3665+
/// For qcow2 images with backing files, also verifies the backing file
3666+
/// integrity and checks that the backing file hasn't been modified
3667+
/// during the test.
3668+
fn disk_check_consistency(
3669+
path_or_image_name: impl AsRef<std::path::Path>,
3670+
initial_backing_checksum: Option<(std::path::PathBuf, String, u32)>,
3671+
) {
3672+
let path = resolve_disk_path(path_or_image_name);
3673+
let output = run_qemu_img(&path, &["check"]);
35433674

35443675
assert!(
35453676
output.status.success(),
35463677
"qemu-img check failed: {}",
35473678
String::from_utf8_lossy(&output.stderr)
35483679
);
3680+
3681+
if let Some((backing_path, format, initial_checksum)) = initial_backing_checksum {
3682+
if format.parse::<block::qcow::ImageType>().ok() != Some(block::qcow::ImageType::Raw) {
3683+
let output = run_qemu_img(&backing_path, &["check"]);
3684+
3685+
assert!(
3686+
output.status.success(),
3687+
"qemu-img check of backing file failed: {}",
3688+
String::from_utf8_lossy(&output.stderr)
3689+
);
3690+
}
3691+
3692+
let mut file = File::open(&backing_path).unwrap();
3693+
let file_size = file.metadata().unwrap().len();
3694+
assert_eq!(
3695+
initial_checksum,
3696+
compute_file_checksum(&mut file, file_size)
3697+
);
3698+
}
35493699
}
35503700

35513701
#[test]
@@ -3710,7 +3860,7 @@ mod common_parallel {
37103860

37113861
handle_child_output(r, &output);
37123862

3713-
disk_check_consistency(vhdx_path);
3863+
disk_check_consistency(vhdx_path, None);
37143864
}
37153865

37163866
fn vhdx_image_size(disk_name: &str) -> u64 {

0 commit comments

Comments
 (0)