#![expect(missing_docs)]
#![cfg(target_os = "linux")]
#![expect(unsafe_code)]
mod ioctl;
mod nvme;
use anyhow::Context;
use async_trait::async_trait;
use blocking::unblock;
use disk_backend::DiskError;
use disk_backend::DiskIo;
use disk_backend::UnmapBehavior;
use disk_backend::pr::PersistentReservation;
use disk_backend::pr::ReservationCapabilities;
use disk_backend::pr::ReservationReport;
use disk_backend::pr::ReservationType;
use disk_backend::resolve::ResolveDiskParameters;
use disk_backend::resolve::ResolvedDisk;
use fs_err::PathExt;
use guestmem::MemoryRead;
use guestmem::MemoryWrite;
use inspect::Inspect;
use io_uring::opcode;
use io_uring::types;
use io_uring::types::RwFlags;
use mesh::MeshPayload;
use nvme::check_nvme_status;
use nvme_spec::nvm;
use pal::unix::affinity;
use pal_uring::Initiate;
use pal_uring::IoInitiator;
use scsi_buffers::BounceBufferTracker;
use scsi_buffers::RequestBuffers;
use std::fmt::Debug;
use std::fs;
use std::os::unix::io::AsRawFd;
use std::os::unix::prelude::FileTypeExt;
use std::os::unix::prelude::MetadataExt;
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use thiserror::Error;
use uevent::CallbackHandle;
use uevent::UeventListener;
use vm_resource::AsyncResolveResource;
use vm_resource::ResourceId;
use vm_resource::ResourceResolver;
use vm_resource::kind::DiskHandleKind;
pub struct BlockDeviceResolver {
uring: Arc<dyn Initiate>,
uevent_listener: Option<Arc<UeventListener>>,
bounce_buffer_tracker: Arc<BounceBufferTracker>,
always_bounce: bool,
}
impl BlockDeviceResolver {
pub fn new(
uring: Arc<dyn Initiate>,
uevent_listener: Option<Arc<UeventListener>>,
bounce_buffer_tracker: Arc<BounceBufferTracker>,
always_bounce: bool,
) -> Self {
Self {
uring,
uevent_listener,
bounce_buffer_tracker,
always_bounce,
}
}
}
#[derive(MeshPayload)]
pub struct OpenBlockDeviceConfig {
pub file: fs::File,
}
impl ResourceId<DiskHandleKind> for OpenBlockDeviceConfig {
const ID: &'static str = "block";
}
#[derive(Debug, Error)]
pub enum ResolveDiskError {
#[error("failed to create new device")]
NewDevice(#[source] NewDeviceError),
#[error("invalid disk")]
InvalidDisk(#[source] disk_backend::InvalidDisk),
}
#[async_trait]
impl AsyncResolveResource<DiskHandleKind, OpenBlockDeviceConfig> for BlockDeviceResolver {
type Output = ResolvedDisk;
type Error = ResolveDiskError;
async fn resolve(
&self,
_resolver: &ResourceResolver,
rsrc: OpenBlockDeviceConfig,
input: ResolveDiskParameters<'_>,
) -> Result<Self::Output, Self::Error> {
let disk = BlockDevice::new(
rsrc.file,
input.read_only,
self.uring.clone(),
self.uevent_listener.as_deref(),
self.bounce_buffer_tracker.clone(),
self.always_bounce,
)
.await
.map_err(ResolveDiskError::NewDevice)?;
ResolvedDisk::new(disk).map_err(ResolveDiskError::InvalidDisk)
}
}
pub fn open_file_for_block(path: &Path, read_only: bool) -> std::io::Result<fs::File> {
use std::os::unix::prelude::*;
tracing::debug!(?path, read_only, "open_file_for_block");
fs::OpenOptions::new()
.read(true)
.write(!read_only)
.custom_flags(libc::O_DIRECT)
.open(path)
}
#[derive(Inspect)]
#[inspect(extra = "BlockDevice::inspect_extra")]
pub struct BlockDevice {
file: Arc<fs::File>,
sector_size: u32,
physical_sector_size: u32,
sector_shift: u32,
sector_count: AtomicU64,
optimal_unmap_sectors: u32,
read_only: bool,
#[inspect(skip)]
uring: Arc<dyn Initiate>,
#[inspect(flatten)]
device_type: DeviceType,
supports_pr: bool,
supports_fua: bool,
#[inspect(skip)]
_uevent_filter: Option<CallbackHandle>,
resize_epoch: Arc<ResizeEpoch>,
resized_acked: AtomicU64,
#[inspect(skip)]
bounce_buffer_tracker: Arc<BounceBufferTracker>,
always_bounce: bool,
}
#[derive(Inspect, Debug, Default)]
#[inspect(transparent)]
struct ResizeEpoch {
epoch: AtomicU64,
#[inspect(skip)]
event: event_listener::Event,
}
#[derive(Debug, Copy, Clone, Inspect)]
#[inspect(tag = "device_type")]
enum DeviceType {
File {
sector_count: u64,
},
UnknownBlock,
NVMe {
ns_id: u32,
rescap: nvm::ReservationCapabilities,
},
}
impl BlockDevice {
fn inspect_extra(&self, resp: &mut inspect::Response<'_>) {
match self.device_type {
DeviceType::NVMe { .. } => {
resp.field_mut_with("interrupt_aggregation", |new_value| {
self.inspect_interrupt_coalescing(new_value)
});
}
DeviceType::UnknownBlock => {}
DeviceType::File { .. } => {}
}
}
fn inspect_interrupt_coalescing(&self, new_value: Option<&str>) -> anyhow::Result<String> {
let coalescing = if let Some(new_value) = new_value {
let coalescing = (|| {
let (threshold, time) = new_value.split_once(' ')?;
Some(
nvme::InterruptCoalescing::new()
.with_aggregation_threshold(threshold.parse().ok()?)
.with_aggregation_time(time.parse().ok()?),
)
})()
.context("expected `<aggregation_threshold> <aggregation_time>`")?;
nvme::nvme_set_features_interrupt_coalescing(&self.file, coalescing)?;
coalescing
} else if let Ok(coalescing) = nvme::nvme_get_features_interrupt_coalescing(&self.file) {
coalescing
} else {
return Ok("not supported".into());
};
Ok(format!(
"{} {}",
coalescing.aggregation_threshold(),
coalescing.aggregation_time()
))
}
}
#[derive(Debug, Error)]
pub enum NewDeviceError {
#[error("block device ioctl error")]
IoctlError(#[from] DiskError),
#[error("failed to read device metadata")]
DeviceMetadata(#[source] anyhow::Error),
#[error("invalid file type, not a file or block device")]
InvalidFileType,
#[error("invalid disk size {0:#x}")]
InvalidDiskSize(u64),
}
impl BlockDevice {
pub async fn new(
file: fs::File,
read_only: bool,
uring: Arc<dyn Initiate>,
uevent_listener: Option<&UeventListener>,
bounce_buffer_tracker: Arc<BounceBufferTracker>,
always_bounce: bool,
) -> Result<BlockDevice, NewDeviceError> {
let initiator = uring.initiator();
assert!(initiator.probe(opcode::Read::CODE));
assert!(initiator.probe(opcode::Write::CODE));
assert!(initiator.probe(opcode::Readv::CODE));
assert!(initiator.probe(opcode::Writev::CODE));
assert!(initiator.probe(opcode::Fsync::CODE));
let metadata = file.metadata().map_err(DiskError::Io)?;
let mut uevent_filter = None;
let resize_epoch = Arc::new(ResizeEpoch::default());
let devmeta = if metadata.file_type().is_block_device() {
let rdev = metadata.rdev();
let (major, minor) = (libc::major(rdev), libc::minor(rdev));
if let Some(uevent_listener) = uevent_listener {
let resize_epoch = resize_epoch.clone();
uevent_filter = Some(
uevent_listener
.add_block_resize_callback(major, minor, {
move || {
tracing::info!(major, minor, "disk resized");
resize_epoch.epoch.fetch_add(1, Ordering::SeqCst);
resize_epoch.event.notify(usize::MAX);
}
})
.await,
);
}
DeviceMetadata::from_block_device(&file, major, minor)
.map_err(NewDeviceError::DeviceMetadata)?
} else if metadata.file_type().is_file() {
DeviceMetadata::from_file(&metadata).map_err(NewDeviceError::DeviceMetadata)?
} else {
return Err(NewDeviceError::InvalidFileType);
};
let sector_size = devmeta.logical_block_size;
let sector_shift = sector_size.trailing_zeros();
let physical_sector_size = devmeta.physical_block_size.max(sector_size);
let sector_count = devmeta.disk_size >> sector_shift;
let unmap_granularity = devmeta.discard_granularity >> sector_shift;
let file = Arc::new(file);
let device = BlockDevice {
file,
sector_size,
physical_sector_size,
sector_shift: sector_size.trailing_zeros(),
sector_count: sector_count.into(),
optimal_unmap_sectors: unmap_granularity,
read_only,
uring,
device_type: devmeta.device_type,
supports_pr: devmeta.supports_pr,
supports_fua: devmeta.fua,
_uevent_filter: uevent_filter,
resize_epoch,
resized_acked: 0.into(),
bounce_buffer_tracker,
always_bounce,
};
Ok(device)
}
fn initiator(&self) -> &IoInitiator {
self.uring.initiator()
}
fn handle_resize(&self) {
if let Err(err) = self.handle_resize_inner() {
tracing::error!(
error = &err as &dyn std::error::Error,
"failed to update disk size"
);
}
}
fn handle_resize_inner(&self) -> std::io::Result<()> {
let mut acked = self.resized_acked.load(Ordering::SeqCst);
loop {
let epoch = self.resize_epoch.epoch.load(Ordering::SeqCst);
if acked == epoch {
break Ok(());
}
let size_in_bytes = ioctl::query_block_device_size_in_bytes(&self.file)?;
let new_sector_count = size_in_bytes / self.sector_size as u64;
let original_sector_count = self.sector_count.load(Ordering::SeqCst);
tracing::debug!(original_sector_count, new_sector_count, "resize");
if original_sector_count != new_sector_count {
tracing::info!(
original_sector_count,
new_sector_count,
"Disk size updating..."
);
self.sector_count.store(new_sector_count, Ordering::SeqCst);
}
acked = self
.resized_acked
.compare_exchange(acked, epoch, Ordering::SeqCst, Ordering::SeqCst)
.unwrap_or_else(|x| x);
}
}
fn map_io_error(&self, err: std::io::Error) -> DiskError {
if !matches!(self.device_type, DeviceType::File { .. }) {
match err.raw_os_error() {
Some(libc::EBADE) => return DiskError::ReservationConflict,
Some(libc::ENOSPC) => return DiskError::IllegalBlock,
_ => {}
}
}
DiskError::Io(err)
}
}
struct DeviceMetadata {
device_type: DeviceType,
disk_size: u64,
logical_block_size: u32,
physical_block_size: u32,
discard_granularity: u32,
supports_pr: bool,
fua: bool,
}
impl DeviceMetadata {
fn from_block_device(file: &fs::File, major: u32, minor: u32) -> anyhow::Result<Self> {
let devpath = PathBuf::from(format!("/sys/dev/block/{major}:{minor}"));
devpath
.fs_err_metadata()
.context("could not find sysfs path for block device")?;
let mut supports_pr = false;
let device_type = match fs_err::read_to_string(devpath.join("nsid")) {
Ok(ns_id) => {
let ns_id = ns_id
.trim()
.parse()
.context("failed to parse NVMe namespace ID")?;
let rescap = nvme::nvme_identify_namespace_data(file, ns_id)?.rescap;
let oncs = nvme::nvme_identify_controller_data(file)?.oncs;
tracing::debug!(rescap = ?rescap, oncs = ?oncs, "get identify data");
supports_pr = oncs.reservations() && u8::from(rescap) != 0;
Some(DeviceType::NVMe { ns_id, rescap })
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
Err(err) => Err(err).context("failed to read NVMe namespace ID")?,
};
let device_type = device_type.unwrap_or(DeviceType::UnknownBlock);
fn read_val<T: FromStr>(devpath: &Path, path: &str, msg: &str) -> anyhow::Result<T>
where
T::Err: 'static + std::error::Error + Send + Sync,
{
fs_err::read_to_string(devpath.join(path))
.with_context(|| format!("failed to read {msg}"))?
.trim()
.parse()
.with_context(|| format!("failed to parse {msg}"))
}
let logical_block_size = read_val(&devpath, "queue/logical_block_size", "sector size")?;
let physical_block_size = read_val(
&devpath,
"queue/physical_block_size",
"physical sector size",
)?;
let disk_size = read_val::<u64>(&devpath, "size", "disk size")? * 512;
let discard_granularity =
read_val(&devpath, "queue/discard_granularity", "discard granularity")?;
let fua = read_val::<u8>(&devpath, "queue/fua", "fua")? != 0;
Self {
device_type,
disk_size,
logical_block_size,
physical_block_size,
discard_granularity,
supports_pr,
fua,
}
.validate()
}
fn from_file(metadata: &fs::Metadata) -> anyhow::Result<Self> {
let logical_block_size = 512;
Self {
device_type: DeviceType::File {
sector_count: metadata.len() / logical_block_size as u64,
},
disk_size: metadata.size(),
logical_block_size,
physical_block_size: metadata.blksize() as u32,
discard_granularity: 0,
supports_pr: false,
fua: false,
}
.validate()
}
fn validate(self) -> anyhow::Result<Self> {
let Self {
device_type: _,
disk_size,
logical_block_size,
physical_block_size,
discard_granularity,
supports_pr: _,
fua: _,
} = self;
if logical_block_size < 512 || !logical_block_size.is_power_of_two() {
anyhow::bail!("invalid sector size {logical_block_size}");
}
if !physical_block_size.is_power_of_two() {
anyhow::bail!("invalid physical sector size {physical_block_size}");
}
if disk_size % logical_block_size as u64 != 0 {
anyhow::bail!("invalid disk size {disk_size:#x}");
}
if discard_granularity % logical_block_size != 0 {
anyhow::bail!("invalid discard granularity {discard_granularity}");
}
Ok(self)
}
}
impl DiskIo for BlockDevice {
fn disk_type(&self) -> &str {
"block_device"
}
fn sector_count(&self) -> u64 {
if self.resize_epoch.epoch.load(Ordering::Relaxed)
!= self.resized_acked.load(Ordering::Relaxed)
{
self.handle_resize();
}
self.sector_count.load(Ordering::Relaxed)
}
fn sector_size(&self) -> u32 {
self.sector_size
}
fn disk_id(&self) -> Option<[u8; 16]> {
None
}
fn physical_sector_size(&self) -> u32 {
self.physical_sector_size
}
fn is_fua_respected(&self) -> bool {
self.supports_fua
}
fn is_read_only(&self) -> bool {
self.read_only
}
fn pr(&self) -> Option<&dyn PersistentReservation> {
if self.supports_pr { Some(self) } else { None }
}
async fn eject(&self) -> Result<(), DiskError> {
let file = self.file.clone();
unblock(move || {
ioctl::lockdoor(&file, false)?;
ioctl::eject(&file)
})
.await
.map_err(|err| self.map_io_error(err))?;
Ok(())
}
async fn read_vectored(
&self,
buffers: &RequestBuffers<'_>,
sector: u64,
) -> Result<(), DiskError> {
let io_size = buffers.len();
tracing::trace!(sector, io_size, "read_vectored");
let mut bounce_buffer = None;
let locked;
let should_bounce = self.always_bounce || !buffers.is_aligned(self.sector_size() as usize);
let io_vecs = if !should_bounce {
locked = buffers.lock(true)?;
locked.io_vecs()
} else {
tracing::trace!("double buffering IO");
bounce_buffer
.insert(
self.bounce_buffer_tracker
.acquire_bounce_buffers(buffers.len(), affinity::get_cpu_number() as usize)
.await,
)
.buffer
.io_vecs()
};
let (r, _) = unsafe {
self.initiator().issue_io((), |_| {
opcode::Readv::new(
types::Fd(self.file.as_raw_fd()),
io_vecs.as_ptr().cast(),
io_vecs.len() as u32,
)
.offset((sector * self.sector_size() as u64) as _)
.build()
})
}
.await;
let bytes_read = r.map_err(|err| self.map_io_error(err))?;
tracing::trace!(bytes_read, "read_vectored");
if bytes_read != io_size as i32 {
return Err(DiskError::IllegalBlock);
}
if let Some(mut bounce_buffer) = bounce_buffer {
buffers
.writer()
.write(bounce_buffer.buffer.as_mut_bytes())?;
}
Ok(())
}
async fn write_vectored(
&self,
buffers: &RequestBuffers<'_>,
sector: u64,
fua: bool,
) -> Result<(), DiskError> {
let io_size = buffers.len();
tracing::trace!(sector, io_size, "write_vectored");
if let DeviceType::File { sector_count } = self.device_type {
if sector + (io_size as u64 >> self.sector_shift) > sector_count {
return Err(DiskError::IllegalBlock);
}
}
let mut bounce_buffer;
let locked;
let should_bounce = self.always_bounce || !buffers.is_aligned(self.sector_size() as usize);
let io_vecs = if !should_bounce {
locked = buffers.lock(false)?;
locked.io_vecs()
} else {
tracing::trace!("double buffering IO");
bounce_buffer = self
.bounce_buffer_tracker
.acquire_bounce_buffers(buffers.len(), affinity::get_cpu_number() as usize)
.await;
buffers.reader().read(bounce_buffer.buffer.as_mut_bytes())?;
bounce_buffer.buffer.io_vecs()
};
const RWF_DSYNC: RwFlags = 0x00000002;
let (r, _) = unsafe {
self.initiator().issue_io((), |_| {
opcode::Writev::new(
types::Fd(self.file.as_raw_fd()),
io_vecs.as_ptr().cast::<libc::iovec>(),
io_vecs.len() as _,
)
.offset((sector * self.sector_size() as u64) as _)
.rw_flags(if fua { RWF_DSYNC } else { 0 })
.build()
})
}
.await;
let bytes_written = r.map_err(|err| self.map_io_error(err))?;
tracing::trace!(bytes_written, "write_vectored");
if bytes_written != io_size as i32 {
return Err(DiskError::IllegalBlock);
}
Ok(())
}
async fn sync_cache(&self) -> Result<(), DiskError> {
unsafe {
self.initiator()
.issue_io((), |_| {
opcode::Fsync::new(types::Fd(self.file.as_raw_fd())).build()
})
.await
.0
.map_err(|err| self.map_io_error(err))?;
}
Ok(())
}
async fn wait_resize(&self, sector_count: u64) -> u64 {
loop {
let listen = self.resize_epoch.event.listen();
let current = self.sector_count();
if current != sector_count {
break current;
}
listen.await;
}
}
async fn unmap(
&self,
sector_offset: u64,
sector_count: u64,
_block_level_only: bool,
) -> Result<(), DiskError> {
let file = self.file.clone();
let file_offset = sector_offset << self.sector_shift;
let length = sector_count << self.sector_shift;
tracing::debug!(file = ?file, file_offset, length, "unmap_async");
match unblock(move || ioctl::discard(&file, file_offset, length)).await {
Ok(()) => {}
Err(_) if sector_offset + sector_count > self.sector_count() => {
return Err(DiskError::IllegalBlock);
}
Err(err) => return Err(self.map_io_error(err)),
}
Ok(())
}
fn unmap_behavior(&self) -> UnmapBehavior {
if self.optimal_unmap_sectors == 0 {
UnmapBehavior::Ignored
} else {
UnmapBehavior::Unspecified
}
}
fn optimal_unmap_sectors(&self) -> u32 {
self.optimal_unmap_sectors
}
}
#[async_trait::async_trait]
impl PersistentReservation for BlockDevice {
fn capabilities(&self) -> ReservationCapabilities {
match &self.device_type {
&DeviceType::NVMe { rescap, .. } => {
nvme_common::from_nvme_reservation_capabilities(rescap)
}
DeviceType::File { .. } | DeviceType::UnknownBlock => unreachable!(),
}
}
async fn report(&self) -> Result<ReservationReport, DiskError> {
assert!(matches!(self.device_type, DeviceType::NVMe { .. }));
self.nvme_persistent_reservation_report()
.await
.map_err(|err| self.map_io_error(err))
}
async fn register(
&self,
current_key: Option<u64>,
new_key: u64,
ptpl: Option<bool>,
) -> Result<(), DiskError> {
assert!(matches!(self.device_type, DeviceType::NVMe { .. }));
if ptpl == Some(false) {
tracing::warn!("ignoring guest request to disable persist through power loss");
}
let file = self.file.clone();
unblock(move || {
ioctl::pr_register(
&file,
current_key.unwrap_or(0),
new_key,
if current_key.is_none() {
ioctl::PR_FL_IGNORE_KEY
} else {
0
},
)
})
.await
.and_then(check_nvme_status)
.map_err(|err| self.map_io_error(err))?;
Ok(())
}
async fn reserve(&self, key: u64, reservation_type: ReservationType) -> Result<(), DiskError> {
assert!(matches!(self.device_type, DeviceType::NVMe { .. }));
let file = self.file.clone();
unblock(move || ioctl::pr_reserve(&file, reservation_type, key))
.await
.and_then(check_nvme_status)
.map_err(|err| self.map_io_error(err))?;
Ok(())
}
async fn release(&self, key: u64, reservation_type: ReservationType) -> Result<(), DiskError> {
assert!(matches!(self.device_type, DeviceType::NVMe { .. }));
let file = self.file.clone();
unblock(move || ioctl::pr_release(&file, reservation_type, key))
.await
.and_then(check_nvme_status)
.map_err(|err| self.map_io_error(err))?;
Ok(())
}
async fn clear(&self, key: u64) -> Result<(), DiskError> {
assert!(matches!(self.device_type, DeviceType::NVMe { .. }));
let file = self.file.clone();
unblock(move || ioctl::pr_clear(&file, key))
.await
.and_then(check_nvme_status)
.map_err(|err| self.map_io_error(err))?;
Ok(())
}
async fn preempt(
&self,
current_key: u64,
preempt_key: u64,
reservation_type: ReservationType,
abort: bool,
) -> Result<(), DiskError> {
assert!(matches!(self.device_type, DeviceType::NVMe { .. }));
let file = self.file.clone();
unblock(move || {
ioctl::pr_preempt(&file, reservation_type, current_key, preempt_key, abort)
})
.await
.and_then(check_nvme_status)
.map_err(|err| self.map_io_error(err))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures::executor::block_on;
use guestmem::GuestMemory;
use hvdef::HV_PAGE_SIZE;
use hvdef::HV_PAGE_SIZE_USIZE;
use once_cell::sync::OnceCell;
use pal_async::async_test;
use pal_uring::IoUringPool;
use scsi_buffers::OwnedRequestBuffers;
fn is_buggy_kernel() -> bool {
let output = String::from_utf8(
std::process::Command::new("uname")
.arg("-r")
.output()
.unwrap()
.stdout,
)
.unwrap();
output.contains("5.13")
}
fn new_block_device() -> Result<BlockDevice, NewDeviceError> {
static POOL: OnceCell<Arc<IoInitiator>> = OnceCell::new();
let initiator = POOL
.get_or_try_init(|| {
let pool = IoUringPool::new("test", 16)?;
let initiator = pool.client().initiator().clone();
std::thread::spawn(|| pool.run());
Ok(Arc::new(initiator))
})
.map_err(|err| NewDeviceError::IoctlError(DiskError::Io(err)))?;
let bounce_buffer_tracker = Arc::new(BounceBufferTracker::new(
2048,
affinity::num_procs() as usize,
));
let test_file = tempfile::tempfile().unwrap();
test_file.set_len(1024 * 64).unwrap();
block_on(BlockDevice::new(
test_file.try_clone().unwrap(),
false,
initiator.clone(),
None,
bounce_buffer_tracker,
false,
))
}
macro_rules! get_block_device_or_skip {
() => {
match new_block_device() {
Ok(pool) => {
if is_buggy_kernel() {
println!("Test case skipped (buggy kernel version)");
return;
}
pool
}
Err(NewDeviceError::IoctlError(DiskError::Io(err)))
if err.raw_os_error() == Some(libc::ENOSYS) =>
{
println!("Test case skipped (no IO-Uring support)");
return;
}
Err(err) => panic!("{}", err),
}
};
}
async fn run_async_disk_io(fua: bool) {
let disk = get_block_device_or_skip!();
let test_guest_mem = GuestMemory::allocate(0x8000);
test_guest_mem
.write_at(0, &(0..0x8000).map(|x| x as u8).collect::<Vec<_>>())
.unwrap();
let write_buffers = OwnedRequestBuffers::new(&[3, 2, 1, 0]);
disk.write_vectored(&write_buffers.buffer(&test_guest_mem), 0, fua)
.await
.unwrap();
if !fua {
disk.sync_cache().await.unwrap();
}
let read_buffers = OwnedRequestBuffers::new(&[7, 6, 5, 4]);
disk.read_vectored(&read_buffers.buffer(&test_guest_mem), 0)
.await
.unwrap();
let mut source = vec![0u8; 4 * HV_PAGE_SIZE_USIZE];
test_guest_mem.read_at(0, &mut source).unwrap();
let mut target = vec![0u8; 4 * HV_PAGE_SIZE_USIZE];
test_guest_mem
.read_at(4 * HV_PAGE_SIZE, &mut target)
.unwrap();
assert_eq!(source, target);
}
#[async_test]
async fn test_async_disk_io() {
run_async_disk_io(false).await;
}
#[async_test]
async fn test_async_disk_io_fua() {
run_async_disk_io(true).await;
}
async fn run_async_disk_io_unaligned(fua: bool) {
let disk = get_block_device_or_skip!();
let test_guest_mem = GuestMemory::allocate(0x8000);
test_guest_mem
.write_at(0, &(0..0x8000).map(|x| x as u8).collect::<Vec<_>>())
.unwrap();
let write_buffers =
OwnedRequestBuffers::new_unaligned(&[0, 1, 2, 3], 512, 3 * HV_PAGE_SIZE_USIZE);
disk.write_vectored(&write_buffers.buffer(&test_guest_mem), 0, fua)
.await
.unwrap();
if !fua {
disk.sync_cache().await.unwrap();
}
let read_buffers =
OwnedRequestBuffers::new_unaligned(&[4, 5, 6, 7], 512, 3 * HV_PAGE_SIZE_USIZE);
disk.read_vectored(&read_buffers.buffer(&test_guest_mem), 0)
.await
.unwrap();
let mut source = vec![0u8; 3 * HV_PAGE_SIZE_USIZE];
test_guest_mem.read_at(512, &mut source).unwrap();
let mut target = vec![0u8; 3 * HV_PAGE_SIZE_USIZE];
test_guest_mem
.read_at(4 * HV_PAGE_SIZE + 512, &mut target)
.unwrap();
assert_eq!(source, target);
}
#[async_test]
async fn test_async_disk_io_unaligned() {
run_async_disk_io_unaligned(false).await;
}
#[async_test]
async fn test_async_disk_io_unaligned_fua() {
run_async_disk_io_unaligned(true).await;
}
#[async_test]
async fn test_illegal_lba() {
let disk = get_block_device_or_skip!();
let gm = GuestMemory::allocate(512);
match disk
.write_vectored(
&OwnedRequestBuffers::linear(0, 512, true).buffer(&gm),
i64::MAX as u64 / 512,
false,
)
.await
{
Err(DiskError::IllegalBlock) => {}
r => panic!("unexpected result: {:?}", r),
}
}
}