use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::{Arc, RwLock};
use crate::device::VirtioMmioState;
use crate::irq::Irq;
pub struct BlkWorkItem {
pub head_idx: u16,
pub request_type: BlkRequestType,
pub sector: u64,
pub buffers: Vec<(u64, u32, bool)>,
pub status_gpa: u64,
pub total_data_len: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlkRequestType {
Read,
Write,
Flush,
GetId,
Unsupported,
Discard,
WriteZeroes,
}
pub struct GuestMemWriter {
ptr: *mut u8,
len: usize,
gpa_base: usize,
}
unsafe impl Send for GuestMemWriter {}
unsafe impl Sync for GuestMemWriter {}
impl GuestMemWriter {
pub unsafe fn new(ptr: *mut u8, len: usize, gpa_base: usize) -> Self {
Self { ptr, len, gpa_base }
}
fn gpa_to_offset(&self, gpa: usize, access_len: usize) -> Option<usize> {
let off = gpa.checked_sub(self.gpa_base)?;
let end = off.checked_add(access_len)?;
if end > self.len {
return None;
}
Some(off)
}
#[allow(clippy::mut_from_ref)] pub unsafe fn slice_mut(&self, gpa: usize, len: usize) -> Option<&mut [u8]> {
let off = self.gpa_to_offset(gpa, len)?;
unsafe { Some(std::slice::from_raw_parts_mut(self.ptr.add(off), len)) }
}
pub fn slice(&self, gpa: usize, len: usize) -> Option<&[u8]> {
let off = self.gpa_to_offset(gpa, len)?;
unsafe { Some(std::slice::from_raw_parts(self.ptr.add(off), len)) }
}
fn write_byte(&self, gpa: usize, val: u8) {
let Some(off) = self.gpa_to_offset(gpa, 1) else {
return;
};
unsafe { *self.ptr.add(off) = val };
}
pub(crate) fn ptr(&self) -> *mut u8 {
self.ptr
}
pub(crate) fn len(&self) -> usize {
self.len
}
pub(crate) fn gpa_base(&self) -> usize {
self.gpa_base
}
}
pub struct BlkWorkerContext {
pub guest_mem: GuestMemWriter,
pub raw_fd: i32,
pub blk_size: u32,
pub capacity_sectors: u64,
pub read_only: bool,
pub device_id: String,
pub mmio_state: Arc<RwLock<VirtioMmioState>>,
pub irq_callback: Arc<dyn Fn(Irq, bool) -> crate::error::Result<()> + Send + Sync>,
pub irq: Irq,
pub running: Arc<AtomicBool>,
pub flush_barrier: Arc<FlushBarrier>,
pub exit_vcpus: Arc<dyn Fn() + Send + Sync>,
pub queue_idx: u16,
}
unsafe impl Send for BlkWorkerContext {}
pub fn blk_io_worker_loop(ctx: BlkWorkerContext, doorbell: std::sync::mpsc::Receiver<()>) {
tracing::info!(
"blk-io worker started (fd={}, q={})",
ctx.raw_fd,
ctx.queue_idx
);
let mut last_avail: u16 = 0;
while doorbell.recv().is_ok() {
if !ctx.running.load(Ordering::Relaxed) {
break;
}
while doorbell.try_recv().is_ok() {}
drain_queue(&ctx, &mut last_avail);
}
tracing::info!("blk-io worker exiting (q={})", ctx.queue_idx);
}
fn drain_queue(ctx: &BlkWorkerContext, last_avail: &mut u16) {
let qi = ctx.queue_idx as usize;
let cfg = {
let Ok(mmio) = ctx.mmio_state.read() else {
return;
};
if qi >= mmio.queue_ready.len() || !mmio.queue_ready[qi] || mmio.queue_num[qi] == 0 {
return;
}
arcbox_virtio::QueueConfig {
desc_addr: mmio.queue_desc[qi],
avail_addr: mmio.queue_driver[qi],
used_addr: mmio.queue_device[qi],
size: mmio.queue_num[qi],
ready: true,
gpa_base: ctx.guest_mem.gpa_base() as u64,
}
};
let mem = std::sync::Arc::new(unsafe {
arcbox_virtio::GuestMemWriter::new(
ctx.guest_mem.ptr(),
ctx.guest_mem.len(),
ctx.guest_mem.gpa_base(),
)
});
let mut queue = arcbox_virtio::SplitQueue::new(mem, ctx.queue_idx, &cfg, true);
queue.set_last_avail_idx(*last_avail);
loop {
while let Some(chain) = queue.pop_avail() {
let (head, len) = match parse_chain(ctx, &chain) {
Some(item) => process_item(ctx, &item),
None => (chain.head_idx, 0),
};
if queue.push_used(head, len) {
trigger_irq(ctx);
}
}
if !queue.enable_notification() {
break;
}
}
*last_avail = queue.last_avail_idx();
}
fn parse_chain(ctx: &BlkWorkerContext, chain: &arcbox_virtio::DescChain) -> Option<BlkWorkItem> {
let header = chain.descriptors.first()?;
let hdr = ctx.guest_mem.slice(header.addr as usize, 16)?;
let req_type = u32::from_le_bytes(hdr[0..4].try_into().ok()?);
let sector = u64::from_le_bytes(hdr[8..16].try_into().ok()?);
let request_type = match req_type {
0 => BlkRequestType::Read,
1 => BlkRequestType::Write,
4 => BlkRequestType::Flush,
8 => BlkRequestType::GetId,
11 => BlkRequestType::Discard,
13 => BlkRequestType::WriteZeroes,
_ => BlkRequestType::Unsupported,
};
let mut buffers = Vec::new();
let mut status_gpa = 0u64;
let mut total_data_len = 0u32;
for desc in chain.descriptors.iter().skip(1) {
let is_write = desc.is_write();
buffers.push((desc.addr, desc.len, is_write));
if is_write && desc.len > 0 {
status_gpa = desc.addr + u64::from(desc.len) - 1;
}
if desc.len > 1 {
total_data_len += desc.len;
}
}
Some(BlkWorkItem {
head_idx: chain.head_idx,
request_type,
sector,
buffers,
status_gpa,
total_data_len,
})
}
fn process_item(ctx: &BlkWorkerContext, item: &BlkWorkItem) -> (u16, u32) {
let is_io = matches!(
item.request_type,
BlkRequestType::Read | BlkRequestType::Write | BlkRequestType::WriteZeroes
);
if is_io {
ctx.flush_barrier.in_flight.fetch_add(1, Ordering::Relaxed);
}
let status = match item.request_type {
BlkRequestType::Read => process_read(ctx, item),
BlkRequestType::Write => process_write(ctx, item),
BlkRequestType::Flush => {
while ctx.flush_barrier.in_flight.load(Ordering::Acquire) > 0 {
std::hint::spin_loop();
}
process_flush(ctx)
}
BlkRequestType::GetId => process_get_id(ctx, item),
BlkRequestType::Unsupported => 2, BlkRequestType::Discard => process_discard(ctx, item),
BlkRequestType::WriteZeroes => process_write_zeroes(ctx, item),
};
if is_io {
ctx.flush_barrier.in_flight.fetch_sub(1, Ordering::Release);
}
ctx.guest_mem.write_byte(item.status_gpa as usize, status);
let total_bytes = if status == 0 {
item.total_data_len + 1 } else {
1 };
(item.head_idx, total_bytes)
}
fn process_read(ctx: &BlkWorkerContext, item: &BlkWorkItem) -> u8 {
let mut iovecs: Vec<libc::iovec> = Vec::new();
for &(gpa, len, is_write) in &item.buffers {
if !is_write || len <= 1 {
continue;
}
let buf = unsafe { ctx.guest_mem.slice_mut(gpa as usize, len as usize) };
let Some(buf) = buf else {
tracing::warn!("blk read: GPA {:#x} len {} out of bounds", gpa, len);
return 1;
};
iovecs.push(libc::iovec {
iov_base: buf.as_mut_ptr().cast(),
iov_len: buf.len(),
});
}
if iovecs.is_empty() {
return 0;
}
let expected_len = iovecs.iter().map(|iov| iov.iov_len).sum::<usize>();
let Ok((byte_offset, _)) = arcbox_virtio::blk::checked_io_byte_range(
item.sector,
expected_len,
ctx.blk_size,
ctx.capacity_sectors,
) else {
tracing::warn!("blk read: range out of capacity at sector {}", item.sector);
return 1;
};
#[allow(clippy::cast_possible_wrap)]
let offset = byte_offset as libc::off_t;
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let n = unsafe { libc::preadv(ctx.raw_fd, iovecs.as_ptr(), iovecs.len() as i32, offset) };
if n < 0 {
tracing::warn!(
"blk preadv failed at sector {}: {}",
item.sector,
std::io::Error::last_os_error()
);
return 1;
}
if n as usize != expected_len {
tracing::warn!(
"blk preadv short read at sector {}: {} < {}",
item.sector,
n,
expected_len
);
return 1;
}
0
}
fn process_write(ctx: &BlkWorkerContext, item: &BlkWorkItem) -> u8 {
if ctx.read_only {
return 1;
}
let mut iovecs: Vec<libc::iovec> = Vec::new();
for &(gpa, len, is_write) in &item.buffers {
if is_write {
continue;
}
let Some(buf) = ctx.guest_mem.slice(gpa as usize, len as usize) else {
tracing::warn!("blk write: GPA {:#x} len {} out of bounds", gpa, len);
return 1;
};
iovecs.push(libc::iovec {
iov_base: buf.as_ptr().cast_mut().cast(),
iov_len: buf.len(),
});
}
if iovecs.is_empty() {
return 0;
}
let expected_len = iovecs.iter().map(|iov| iov.iov_len).sum::<usize>();
let Ok((byte_offset, _)) = arcbox_virtio::blk::checked_io_byte_range(
item.sector,
expected_len,
ctx.blk_size,
ctx.capacity_sectors,
) else {
tracing::warn!("blk write: range out of capacity at sector {}", item.sector);
return 1;
};
#[allow(clippy::cast_possible_wrap)]
let offset = byte_offset as libc::off_t;
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let n = unsafe { libc::pwritev(ctx.raw_fd, iovecs.as_ptr(), iovecs.len() as i32, offset) };
if n < 0 {
tracing::warn!(
"blk pwritev failed at sector {}: {}",
item.sector,
std::io::Error::last_os_error()
);
return 1;
}
if n as usize != expected_len {
tracing::warn!(
"blk pwritev short write at sector {}: {} < {}",
item.sector,
n,
expected_len
);
return 1;
}
0
}
fn process_flush(ctx: &BlkWorkerContext) -> u8 {
let ret = unsafe { libc::fsync(ctx.raw_fd) };
if ret < 0 {
tracing::warn!("blk fsync failed: {}", std::io::Error::last_os_error());
1
} else {
0
}
}
fn process_get_id(ctx: &BlkWorkerContext, item: &BlkWorkItem) -> u8 {
let id_bytes = ctx.device_id.as_bytes();
for &(gpa, len, is_write) in &item.buffers {
if !is_write || len <= 1 {
continue;
}
let buf = unsafe { ctx.guest_mem.slice_mut(gpa as usize, len as usize) };
if let Some(buf) = buf {
let copy_len = id_bytes.len().min(buf.len());
buf[..copy_len].copy_from_slice(&id_bytes[..copy_len]);
}
break;
}
0
}
fn process_discard(ctx: &BlkWorkerContext, item: &BlkWorkItem) -> u8 {
if !ctx.read_only {
return punch_discard_ranges(
&ctx.guest_mem,
ctx.raw_fd,
ctx.blk_size,
ctx.capacity_sectors,
&item.buffers,
);
}
0 }
fn process_write_zeroes(ctx: &BlkWorkerContext, item: &BlkWorkItem) -> u8 {
if ctx.read_only {
return 1; }
zero_ranges(
&ctx.guest_mem,
ctx.raw_fd,
ctx.blk_size,
ctx.capacity_sectors,
&item.buffers,
)
}
fn parse_ranges_from_buffers(
guest_mem: &GuestMemWriter,
buffers: &[(u64, u32, bool)],
log_context: &str,
) -> Result<Vec<arcbox_virtio::blk::DiscardWriteZeroesRange>, ()> {
let mut payload = Vec::new();
for &(gpa, len, is_write) in buffers {
if is_write {
continue;
}
let Some(bytes) = guest_mem.slice(gpa as usize, len as usize) else {
tracing::warn!("{log_context}: range payload GPA {gpa:#x} len {len} out of bounds");
return Err(());
};
payload.extend_from_slice(bytes);
}
arcbox_virtio::blk::parse_range_list(&payload).map_err(|e| {
tracing::warn!(error = %e, "{log_context}: malformed range list");
})
}
fn checked_range_bytes(
range: arcbox_virtio::blk::DiscardWriteZeroesRange,
blk_size: u32,
capacity_sectors: u64,
max_sectors: u32,
log_context: &str,
) -> Result<(u64, u64), ()> {
if range.num_sectors > max_sectors {
tracing::warn!(
sector = range.sector,
num_sectors = range.num_sectors,
max_sectors,
"{log_context}: range exceeds advertised maximum"
);
return Err(());
}
range
.checked_byte_range(blk_size, capacity_sectors)
.map_err(|e| {
tracing::warn!(sector = range.sector, num_sectors = range.num_sectors, error = %e, "{log_context}: invalid range");
})
}
fn validate_discard_flags(
range: arcbox_virtio::blk::DiscardWriteZeroesRange,
log_context: &str,
) -> Result<(), ()> {
if range.flags != 0 {
tracing::warn!(
flags = range.flags,
"{log_context}: discard reserved flags set"
);
return Err(());
}
Ok(())
}
fn validate_write_zeroes_flags(
range: arcbox_virtio::blk::DiscardWriteZeroesRange,
log_context: &str,
) -> Result<(), ()> {
if range.flags & !arcbox_virtio::blk::WRITE_ZEROES_FLAG_UNMAP != 0 {
tracing::warn!(
flags = range.flags,
"{log_context}: write_zeroes reserved flags set"
);
return Err(());
}
Ok(())
}
fn discard_byte_ranges(
guest_mem: &GuestMemWriter,
blk_size: u32,
capacity_sectors: u64,
buffers: &[(u64, u32, bool)],
) -> Result<Vec<(u64, u64)>, ()> {
let ranges = parse_ranges_from_buffers(guest_mem, buffers, "discard")?;
let mut byte_ranges = Vec::with_capacity(ranges.len());
for range in ranges {
validate_discard_flags(range, "discard")?;
byte_ranges.push(checked_range_bytes(
range,
blk_size,
capacity_sectors,
arcbox_virtio::blk::VirtioBlock::MAX_DISCARD_SECTORS,
"discard",
)?);
}
Ok(byte_ranges)
}
fn write_zeroes_byte_ranges(
guest_mem: &GuestMemWriter,
blk_size: u32,
capacity_sectors: u64,
buffers: &[(u64, u32, bool)],
) -> Result<Vec<(u64, u64)>, ()> {
let ranges = parse_ranges_from_buffers(guest_mem, buffers, "write_zeroes")?;
let mut byte_ranges = Vec::with_capacity(ranges.len());
for range in ranges {
validate_write_zeroes_flags(range, "write_zeroes")?;
byte_ranges.push(checked_range_bytes(
range,
blk_size,
capacity_sectors,
arcbox_virtio::blk::VirtioBlock::MAX_WRITE_ZEROES_SECTORS,
"write_zeroes",
)?);
}
Ok(byte_ranges)
}
fn punch_discard_ranges(
guest_mem: &GuestMemWriter,
raw_fd: i32,
blk_size: u32,
capacity_sectors: u64,
buffers: &[(u64, u32, bool)],
) -> u8 {
use arcbox_virtio::blk::{aligned_punch_range, punch_hole};
let Ok(ranges) = discard_byte_ranges(guest_mem, blk_size, capacity_sectors, buffers) else {
return 1;
};
for (start, end) in ranges {
if let Some((offset, hole_len)) = aligned_punch_range(start, end) {
if let Err(e) = punch_hole(raw_fd, offset, hole_len) {
tracing::warn!(
start,
end,
error = %e,
"discard hole-punch failed (HV worker); range left allocated"
);
}
}
}
0
}
fn zero_ranges(
guest_mem: &GuestMemWriter,
raw_fd: i32,
blk_size: u32,
capacity_sectors: u64,
buffers: &[(u64, u32, bool)],
) -> u8 {
let mut status = 0u8;
let Ok(ranges) = write_zeroes_byte_ranges(guest_mem, blk_size, capacity_sectors, buffers)
else {
return 1;
};
for (start, end) in ranges {
if let Err(e) = arcbox_virtio::blk::zero_range(raw_fd, start, end) {
tracing::warn!(start, end, error = %e, "write_zeroes failed (HV worker)");
status = 1;
}
}
status
}
fn trigger_irq(ctx: &BlkWorkerContext) {
if let Ok(mut s) = ctx.mmio_state.write() {
s.trigger_interrupt(1); }
let _ = (ctx.irq_callback)(ctx.irq, true);
(ctx.exit_vcpus)();
}
pub struct BlkQueueWorker {
pub doorbell: std::sync::mpsc::Sender<()>,
}
pub struct BlkWorkerHandle {
pub queues: Vec<BlkQueueWorker>,
}
impl BlkWorkerHandle {
pub fn get_queue(&self, queue_idx: u16) -> Option<&BlkQueueWorker> {
self.queues.get(queue_idx as usize)
}
pub fn ring(&self, queue_idx: u16) {
if let Some(worker) = self.get_queue(queue_idx) {
let _ = worker.doorbell.send(());
}
}
}
pub struct FlushBarrier {
pub in_flight: AtomicU32,
}
impl Default for FlushBarrier {
fn default() -> Self {
Self {
in_flight: AtomicU32::new(0),
}
}
}
impl FlushBarrier {
pub fn new() -> Self {
Self::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_context(mem: &mut [u8], raw_fd: i32, capacity_sectors: u64) -> BlkWorkerContext {
BlkWorkerContext {
guest_mem: unsafe { GuestMemWriter::new(mem.as_mut_ptr(), mem.len(), 0) },
raw_fd,
blk_size: 512,
capacity_sectors,
read_only: false,
device_id: "test-blk".to_string(),
mmio_state: Arc::new(RwLock::new(VirtioMmioState::new(2, 0))),
irq_callback: Arc::new(|_, _| Ok(())),
irq: 32,
running: Arc::new(AtomicBool::new(true)),
flush_barrier: Arc::new(FlushBarrier::new()),
exit_vcpus: Arc::new(|| {}),
queue_idx: 0,
}
}
fn work_item(
request_type: BlkRequestType,
sector: u64,
buffers: Vec<(u64, u32, bool)>,
status_gpa: u64,
) -> BlkWorkItem {
let total_data_len = buffers
.iter()
.filter(|(_, len, _)| *len > 1)
.map(|(_, len, _)| *len)
.sum();
BlkWorkItem {
head_idx: status_gpa as u16,
request_type,
sector,
buffers,
status_gpa,
total_data_len,
}
}
#[test]
fn write_past_capacity_is_rejected() {
use std::io::Write;
use std::os::unix::io::AsRawFd;
let mut temp = tempfile::NamedTempFile::new().unwrap();
temp.write_all(&vec![0u8; 4096]).unwrap();
temp.as_file().sync_all().unwrap();
let mut mem = vec![0xCCu8; 4096];
let ctx = test_context(&mut mem, temp.as_file().as_raw_fd(), 8);
let item = work_item(BlkRequestType::Write, 7, vec![(0, 1024, false)], 1500);
assert_eq!(process_write(&ctx, &item), 1);
assert_eq!(std::fs::metadata(temp.path()).unwrap().len(), 4096);
}
#[test]
fn unsupported_request_completes_unsupp() {
let mut mem = vec![0u8; 4096];
let ctx = test_context(&mut mem, -1, 8);
let item = work_item(BlkRequestType::Unsupported, 0, Vec::new(), 1500);
let (_, total_bytes) = process_item(&ctx, &item);
assert_eq!(total_bytes, 1, "failed request completes with status only");
assert_eq!(mem[1500], 2);
}
#[test]
fn punch_discard_ranges_reclaims_blocks() {
use std::io::Write;
use std::os::unix::fs::MetadataExt;
use std::os::unix::io::AsRawFd;
let mut temp = tempfile::NamedTempFile::new().unwrap();
temp.write_all(&vec![0xEEu8; 1024 * 1024]).unwrap();
temp.as_file().sync_all().unwrap();
let fd = temp.as_file().as_raw_fd();
let mut mem = vec![0u8; 4096];
mem[0..8].copy_from_slice(&0u64.to_le_bytes()); mem[8..12].copy_from_slice(&2048u32.to_le_bytes()); mem[12..16].copy_from_slice(&0u32.to_le_bytes()); let gm = unsafe { GuestMemWriter::new(mem.as_mut_ptr(), mem.len(), 0) };
let buffers = vec![(0u64, 16u32, false), (16u64, 1u32, true)];
assert_eq!(punch_discard_ranges(&gm, fd, 512, 2048, &buffers), 0);
temp.as_file().sync_all().unwrap();
let after = std::fs::metadata(temp.path()).unwrap().blocks() * 512;
assert!(
after < 64 * 1024,
"worker discard should have punched the backing file, still {after} allocated"
);
}
#[test]
#[allow(clippy::cast_possible_wrap)] fn zero_ranges_zeroes_backing_file() {
use std::io::Write;
use std::os::unix::io::AsRawFd;
let mut temp = tempfile::NamedTempFile::new().unwrap();
temp.write_all(&vec![0xCDu8; 16 * 1024]).unwrap();
temp.as_file().sync_all().unwrap();
let fd = temp.as_file().as_raw_fd();
let mut mem = vec![0u8; 4096];
mem[0..8].copy_from_slice(&0u64.to_le_bytes()); mem[8..12].copy_from_slice(&16u32.to_le_bytes()); mem[12..16].copy_from_slice(&0u32.to_le_bytes()); let gm = unsafe { GuestMemWriter::new(mem.as_mut_ptr(), mem.len(), 0) };
let buffers = vec![(0u64, 16u32, false), (16u64, 1u32, true)];
assert_eq!(zero_ranges(&gm, fd, 512, 32, &buffers), 0);
temp.as_file().sync_all().unwrap();
let mut buf = vec![0xFFu8; 8 * 1024];
let n = unsafe { libc::pread(fd, buf.as_mut_ptr().cast(), buf.len(), 0) };
assert_eq!(n, buf.len() as isize);
assert!(
buf.iter().all(|&b| b == 0),
"write_zeroes range must read as zeros"
);
let mut rest = [0xFFu8; 512];
let n = unsafe { libc::pread(fd, rest.as_mut_ptr().cast(), rest.len(), 8 * 1024) };
assert_eq!(n, 512);
assert!(
rest.iter().all(|&b| b == 0xCD),
"data past the range is intact"
);
}
#[test]
fn range_parser_concatenates_split_payload_descriptors() {
let mut mem = vec![0u8; 4096];
mem[0..8].copy_from_slice(&8u64.to_le_bytes());
mem[8..12].copy_from_slice(&8u32.to_le_bytes());
mem[12..16].copy_from_slice(&0u32.to_le_bytes());
let gm = unsafe { GuestMemWriter::new(mem.as_mut_ptr(), mem.len(), 0) };
let buffers = vec![
(0u64, 8u32, false),
(8u64, 8u32, false),
(16u64, 1u32, true),
];
let ranges = discard_byte_ranges(&gm, 512, 32, &buffers).unwrap();
assert_eq!(ranges, vec![(4096, 8192)]);
}
#[test]
fn write_zeroes_rejects_trailing_payload_bytes() {
let mut mem = vec![0u8; 4096];
mem[0..8].copy_from_slice(&0u64.to_le_bytes());
mem[8..12].copy_from_slice(&8u32.to_le_bytes());
mem[12..16].copy_from_slice(&0u32.to_le_bytes());
let gm = unsafe { GuestMemWriter::new(mem.as_mut_ptr(), mem.len(), 0) };
let buffers = vec![(0u64, 17u32, false), (17u64, 1u32, true)];
assert!(write_zeroes_byte_ranges(&gm, 512, 32, &buffers).is_err());
}
#[test]
fn write_zeroes_rejects_ranges_past_capacity() {
let mut mem = vec![0u8; 4096];
mem[0..8].copy_from_slice(&31u64.to_le_bytes());
mem[8..12].copy_from_slice(&2u32.to_le_bytes());
mem[12..16].copy_from_slice(&0u32.to_le_bytes());
let gm = unsafe { GuestMemWriter::new(mem.as_mut_ptr(), mem.len(), 0) };
let buffers = vec![(0u64, 16u32, false), (16u64, 1u32, true)];
assert!(write_zeroes_byte_ranges(&gm, 512, 32, &buffers).is_err());
}
}