@@ -2527,8 +2527,9 @@ EOF
25272527}
25282528
25292529mod 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