From 623af62743222277b637a77f4fd90397f38ab551 Mon Sep 17 00:00:00 2001 From: Emir Beganovic Date: Tue, 10 Mar 2026 13:02:21 +0100 Subject: [PATCH] block: Implement write_zeroes and punch_hole for AIO backend The AIO block backend advertises VIRTIO_BLK_F_WRITE_ZEROES and VIRTIO_BLK_F_DISCARD to guests because the filesystem probe (supports_sparse_operations) returns true on ext4/XFS. However, RawFileAsyncAio::write_zeroes() and punch_hole() return errors because Linux AIO (io_submit) has no IOCB command for fallocate. When io_uring is unavailable (e.g. io_uring_disabled=2, a common security hardening on enterprise Linux), Cloud Hypervisor falls back to the AIO backend. The guest negotiates the feature, issues WRITE_ZEROES requests, and gets I/O errors. Implement write_zeroes and punch_hole using synchronous libc::fallocate() calls, matching the pattern used by the sync backend (RawFileSync). A VecDeque-based completion list signals results to the caller via the existing eventfd mechanism. Unit tests mirror the existing raw_sync.rs test suite. Integration tests add AIO-specific variants of the discard and fstrim tests using _disable_io_uring=on. Signed-off-by: Emir Beganovic --- block/src/raw_async_aio.rs | 240 +++++++++++++++++++++++++- cloud-hypervisor/tests/integration.rs | 64 ++++++- 2 files changed, 292 insertions(+), 12 deletions(-) diff --git a/block/src/raw_async_aio.rs b/block/src/raw_async_aio.rs index fe7196ebb..20fb26c2c 100644 --- a/block/src/raw_async_aio.rs +++ b/block/src/raw_async_aio.rs @@ -5,6 +5,7 @@ // Copyright © 2023 Crusoe Energy Systems LLC // +use std::collections::VecDeque; use std::fs::File; use std::io::{Seek, SeekFrom}; use std::os::unix::io::{AsRawFd, RawFd}; @@ -73,6 +74,7 @@ pub struct RawFileAsyncAio { ctx: aio::IoContext, eventfd: EventFd, alignment: u64, + completion_list: VecDeque<(u64, i32)>, } impl RawFileAsyncAio { @@ -85,6 +87,7 @@ impl RawFileAsyncAio { ctx, eventfd, alignment: SECTOR_SIZE, + completion_list: VecDeque::new(), }) } } @@ -168,6 +171,11 @@ impl AsyncIo for RawFileAsyncAio { } fn next_completed_request(&mut self) -> Option<(u64, i32)> { + // Drain synchronous completions first (from punch_hole/write_zeroes). + if let Some(completed) = self.completion_list.pop_front() { + return Some(completed); + } + let mut events: [aio::IoEvent; 1] = [aio::IoEvent::default()]; let rc = self.ctx.get_events(0, &mut events, None).unwrap(); if rc == 0 { @@ -177,15 +185,231 @@ impl AsyncIo for RawFileAsyncAio { } } - fn punch_hole(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> { - Err(AsyncIoError::PunchHole(std::io::Error::other( - "punch_hole not supported with AIO backend", - ))) + fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { + // Linux AIO has no IOCB command for fallocate, so perform the operation + // synchronously and signal completion via the completion list, matching + // the pattern used by the sync backend (RawFileSync). + const FALLOC_FL_PUNCH_HOLE: i32 = 0x02; + const FALLOC_FL_KEEP_SIZE: i32 = 0x01; + let mode = FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE; + + // SAFETY: FFI call with valid arguments + let result = unsafe { + libc::fallocate( + self.fd as libc::c_int, + mode, + offset as libc::off_t, + length as libc::off_t, + ) + }; + if result < 0 { + return Err(AsyncIoError::PunchHole(std::io::Error::last_os_error())); + } + + self.completion_list.push_back((user_data, result)); + self.eventfd.write(1).unwrap(); + + Ok(()) } - fn write_zeroes(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> { - Err(AsyncIoError::WriteZeroes(std::io::Error::other( - "write_zeroes not supported with AIO backend", - ))) + fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { + // Linux AIO has no IOCB command for fallocate, so perform the operation + // synchronously and signal completion via the completion list, matching + // the pattern used by the sync backend (RawFileSync). + const FALLOC_FL_ZERO_RANGE: i32 = 0x10; + const FALLOC_FL_KEEP_SIZE: i32 = 0x01; + let mode = FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE; + + // SAFETY: FFI call with valid arguments + let result = unsafe { + libc::fallocate( + self.fd as libc::c_int, + mode, + offset as libc::off_t, + length as libc::off_t, + ) + }; + if result < 0 { + return Err(AsyncIoError::WriteZeroes(std::io::Error::last_os_error())); + } + + self.completion_list.push_back((user_data, result)); + self.eventfd.write(1).unwrap(); + + Ok(()) + } +} + +#[cfg(test)] +mod unit_tests { + use std::io::{Read, Seek, SeekFrom, Write}; + + use vmm_sys_util::tempfile::TempFile; + + use super::*; + + #[test] + fn test_punch_hole() { + let temp_file = TempFile::new().unwrap(); + let mut file = temp_file.into_file(); + + // Write 4MB of data + let data = vec![0xAA; 4 * 1024 * 1024]; + file.write_all(&data).unwrap(); + file.sync_all().unwrap(); + + // Create async IO instance + let mut async_io = RawFileAsyncAio::new(file.as_raw_fd(), 128).unwrap(); + + // Punch hole in the middle (1MB at offset 1MB) + let offset = 1024 * 1024; + let length = 1024 * 1024; + async_io.punch_hole(offset, length, 1).unwrap(); + + // Check completion + let (user_data, result) = async_io.next_completed_request().unwrap(); + assert_eq!(user_data, 1); + assert_eq!(result, 0); + + // Verify the hole reads as zeros + file.seek(SeekFrom::Start(offset)).unwrap(); + let mut read_buf = vec![0; length as usize]; + file.read_exact(&mut read_buf).unwrap(); + assert!( + read_buf.iter().all(|&b| b == 0), + "Punched hole should read as zeros" + ); + + // Verify data before hole is intact + file.seek(SeekFrom::Start(0)).unwrap(); + let mut read_buf = vec![0; 1024]; + file.read_exact(&mut read_buf).unwrap(); + assert!( + read_buf.iter().all(|&b| b == 0xAA), + "Data before hole should be intact" + ); + + // Verify data after hole is intact + file.seek(SeekFrom::Start(offset + length)).unwrap(); + let mut read_buf = vec![0; 1024]; + file.read_exact(&mut read_buf).unwrap(); + assert!( + read_buf.iter().all(|&b| b == 0xAA), + "Data after hole should be intact" + ); + } + + #[test] + fn test_write_zeroes() { + let temp_file = TempFile::new().unwrap(); + let mut file = temp_file.into_file(); + + // Write 4MB of data + let data = vec![0xBB; 4 * 1024 * 1024]; + file.write_all(&data).unwrap(); + file.sync_all().unwrap(); + + // Create async IO instance + let mut async_io = RawFileAsyncAio::new(file.as_raw_fd(), 128).unwrap(); + + // Write zeros in the middle (512KB at offset 2MB) + let offset = 2 * 1024 * 1024; + let length = 512 * 1024; + let write_zeroes_result = async_io.write_zeroes(offset, length, 2); + + // FALLOC_FL_ZERO_RANGE might not be supported on all filesystems (e.g., tmpfs) + // If it fails with ENOTSUP, skip the test + if let Err(AsyncIoError::WriteZeroes(ref e)) = write_zeroes_result + && (e.raw_os_error() == Some(libc::EOPNOTSUPP) + || e.raw_os_error() == Some(libc::ENOTSUP)) + { + eprintln!( + "Skipping test_write_zeroes: filesystem doesn't support FALLOC_FL_ZERO_RANGE" + ); + return; + } + write_zeroes_result.unwrap(); + + // Check completion + let (user_data, result) = async_io.next_completed_request().unwrap(); + assert_eq!(user_data, 2); + assert_eq!(result, 0); + + // Verify the zeroed region reads as zeros + file.seek(SeekFrom::Start(offset)).unwrap(); + let mut read_buf = vec![0; length as usize]; + file.read_exact(&mut read_buf).unwrap(); + assert!( + read_buf.iter().all(|&b| b == 0), + "Zeroed region should read as zeros" + ); + + // Verify data before zeroed region is intact + file.seek(SeekFrom::Start(offset - 1024)).unwrap(); + let mut read_buf = vec![0; 1024]; + file.read_exact(&mut read_buf).unwrap(); + assert!( + read_buf.iter().all(|&b| b == 0xBB), + "Data before zeroed region should be intact" + ); + + // Verify data after zeroed region is intact + file.seek(SeekFrom::Start(offset + length)).unwrap(); + let mut read_buf = vec![0; 1024]; + file.read_exact(&mut read_buf).unwrap(); + assert!( + read_buf.iter().all(|&b| b == 0xBB), + "Data after zeroed region should be intact" + ); + } + + #[test] + fn test_punch_hole_multiple_operations() { + let temp_file = TempFile::new().unwrap(); + let mut file = temp_file.into_file(); + + // Write 8MB of data + let data = vec![0xCC; 8 * 1024 * 1024]; + file.write_all(&data).unwrap(); + file.sync_all().unwrap(); + + // Create async IO instance + let mut async_io = RawFileAsyncAio::new(file.as_raw_fd(), 128).unwrap(); + + // Punch multiple holes + async_io.punch_hole(1024 * 1024, 512 * 1024, 10).unwrap(); + async_io + .punch_hole(3 * 1024 * 1024, 512 * 1024, 11) + .unwrap(); + async_io + .punch_hole(5 * 1024 * 1024, 512 * 1024, 12) + .unwrap(); + + // Check all completions + let (user_data, result) = async_io.next_completed_request().unwrap(); + assert_eq!(user_data, 10); + assert_eq!(result, 0); + + let (user_data, result) = async_io.next_completed_request().unwrap(); + assert_eq!(user_data, 11); + assert_eq!(result, 0); + + let (user_data, result) = async_io.next_completed_request().unwrap(); + assert_eq!(user_data, 12); + assert_eq!(result, 0); + + // Verify all holes read as zeros + file.seek(SeekFrom::Start(1024 * 1024)).unwrap(); + let mut read_buf = vec![0; 512 * 1024]; + file.read_exact(&mut read_buf).unwrap(); + assert!(read_buf.iter().all(|&b| b == 0)); + + file.seek(SeekFrom::Start(3 * 1024 * 1024)).unwrap(); + file.read_exact(&mut read_buf).unwrap(); + assert!(read_buf.iter().all(|&b| b == 0)); + + file.seek(SeekFrom::Start(5 * 1024 * 1024)).unwrap(); + file.read_exact(&mut read_buf).unwrap(); + assert!(read_buf.iter().all(|&b| b == 0)); } } diff --git a/cloud-hypervisor/tests/integration.rs b/cloud-hypervisor/tests/integration.rs index 96963bc4e..1d198828e 100644 --- a/cloud-hypervisor/tests/integration.rs +++ b/cloud-hypervisor/tests/integration.rs @@ -7532,6 +7532,24 @@ mod common_parallel { extra_create_args: &[&str], expect_discard_success: bool, verify_disk: bool, + ) { + _test_virtio_block_discard_with_backend( + format_name, + qemu_img_format, + extra_create_args, + expect_discard_success, + verify_disk, + false, + ); + } + + fn _test_virtio_block_discard_with_backend( + format_name: &str, + qemu_img_format: &str, + extra_create_args: &[&str], + expect_discard_success: bool, + verify_disk: bool, + disable_io_uring: bool, ) { let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); let guest = Guest::new(Box::new(disk_config)); @@ -7574,9 +7592,14 @@ mod common_parallel { ) .as_str(), format!( - "path={},num_queues=4,image_type={}", + "path={},num_queues=4,image_type={}{}", test_disk_path.to_str().unwrap(), - format_name.to_lowercase() + format_name.to_lowercase(), + if disable_io_uring { + ",_disable_io_uring=on" + } else { + "" + } ) .as_str(), ]) @@ -7754,6 +7777,11 @@ mod common_parallel { _test_virtio_block_discard("raw", "raw", &[], true, false); } + #[test] + fn test_virtio_block_discard_raw_aio() { + _test_virtio_block_discard_with_backend("raw", "raw", &[], true, false, true); + } + #[test] fn test_virtio_block_discard_unsupported_vhd() { _test_virtio_block_discard("vhd", "vpc", &["-o", "subformat=fixed"], false, false); @@ -8056,6 +8084,24 @@ mod common_parallel { extra_create_args: &[&str], expect_fstrim_success: bool, verify_disk: bool, + ) { + _test_virtio_block_fstrim_with_backend( + format_name, + qemu_img_format, + extra_create_args, + expect_fstrim_success, + verify_disk, + false, + ); + } + + fn _test_virtio_block_fstrim_with_backend( + format_name: &str, + qemu_img_format: &str, + extra_create_args: &[&str], + expect_fstrim_success: bool, + verify_disk: bool, + disable_io_uring: bool, ) { let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); let guest = Guest::new(Box::new(disk_config)); @@ -8101,9 +8147,14 @@ mod common_parallel { ) .as_str(), format!( - "path={},num_queues=4,image_type={}", + "path={},num_queues=4,image_type={}{}", test_disk_path.to_str().unwrap(), - format_name.to_lowercase() + format_name.to_lowercase(), + if disable_io_uring { + ",_disable_io_uring=on" + } else { + "" + } ) .as_str(), ]) @@ -8242,6 +8293,11 @@ mod common_parallel { _test_virtio_block_fstrim("raw", "raw", &[], true, false); } + #[test] + fn test_virtio_block_fstrim_raw_aio() { + _test_virtio_block_fstrim_with_backend("raw", "raw", &[], true, false, true); + } + #[test] fn test_virtio_block_fstrim_unsupported_vhd() { _test_virtio_block_fstrim("vhd", "vpc", &["-o", "subformat=fixed"], false, false);