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 <beganovic.emir@gmail.com>
This commit is contained in:
Emir Beganovic
2026-03-10 13:02:21 +01:00
committed by Rob Bradford
parent 8671e193ff
commit 623af62743
2 changed files with 292 additions and 12 deletions

View File

@@ -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));
}
}

View File

@@ -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);