block: Avoid raw iovecs in VHDX sync I/O

The VHDX synchronous async I/O backend still converted owned
AsyncIoOperation targets back into raw iovec slices before calling the
VHDX read and write helpers. That kept pointer dereferences in the owned
path and allowed a backend mistake to violate the safety boundary.

Handle owned VHDX reads and writes through AsyncIoOperation copy helpers
instead. The VHDX file operations still run synchronously, but data is
copied through operation-owned buffers or guest-memory targets without
reconstructing Rust slices from raw iovec pointers.

Assisted-by: Codex:GPT-5
Signed-off-by: Dylan Reid <dgreid@fb.com>
This commit is contained in:
Dylan Reid
2026-05-23 10:02:26 -07:00
committed by Rob Bradford
parent 92d9a53ed3
commit cc2e528c88
+18 -63
View File
@@ -6,11 +6,10 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use std::fs::File; use std::fs::File;
use std::io::{IoSlice, IoSliceMut, Read, Seek, SeekFrom, Write}; use std::io::{Read, Seek, SeekFrom, Write};
use std::os::fd::AsRawFd; use std::os::fd::AsRawFd;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use smallvec::SmallVec;
use vmm_sys_util::eventfd::EventFd; use vmm_sys_util::eventfd::EventFd;
use crate::async_io::{ use crate::async_io::{
@@ -18,7 +17,6 @@ use crate::async_io::{
DiskFileError, DiskFileError,
}; };
use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp}; use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp};
use crate::request::DEFAULT_DESCRIPTOR_VEC_SIZE;
use crate::vhdx::{Vhdx, VhdxError}; use crate::vhdx::{Vhdx, VhdxError};
use crate::{BlockBackend, Error, disk_file}; use crate::{BlockBackend, Error, disk_file};
@@ -124,64 +122,30 @@ impl VhdxSync {
} }
} }
// SAFETY: each iovec must describe writable memory that remains valid for fn read_operation(&mut self, op: &mut AsyncIoOperation) -> AsyncIoResult<usize> {
// this synchronous call. The caller must also ensure that creating a let offset = op.offset();
// mutable slice from each iovec does not violate Rust aliasing rules. let mut buf = vec![0u8; op.total_len()];
unsafe fn read_iovecs(
&mut self,
offset: libc::off_t,
iovecs: &[libc::iovec],
) -> AsyncIoResult<usize> {
let mut slices: SmallVec<[IoSliceMut; DEFAULT_DESCRIPTOR_VEC_SIZE]> =
SmallVec::with_capacity(iovecs.len());
for iovec in iovecs.iter() {
if iovec.iov_len == 0 {
continue;
}
// SAFETY: Guaranteed by read_iovecs' caller.
let slice = unsafe {
std::slice::from_raw_parts_mut(iovec.iov_base.cast::<u8>(), iovec.iov_len)
};
slices.push(IoSliceMut::new(slice));
}
let mut vhdx = self.vhdx_file.lock().unwrap(); let mut vhdx = self.vhdx_file.lock().unwrap();
vhdx.seek(SeekFrom::Start(offset as u64)) vhdx.seek(SeekFrom::Start(offset as u64))
.map_err(AsyncIoError::ReadVectored)?; .map_err(AsyncIoError::ReadVectored)?;
let mut result = 0usize; let result = vhdx.read(&mut buf).map_err(AsyncIoError::ReadVectored)?;
for slice in slices.iter_mut() { drop(vhdx);
result += vhdx.read(slice).map_err(AsyncIoError::ReadVectored)?;
} op.write_bytes_at(0, &buf[..result])
.map_err(AsyncIoError::ReadVectored)?;
Ok(result) Ok(result)
} }
// SAFETY: each iovec must describe readable memory that remains valid for fn write_operation(&mut self, op: &AsyncIoOperation) -> AsyncIoResult<usize> {
// this synchronous call. The caller must also ensure that creating a shared let offset = op.offset();
// slice from each iovec does not violate Rust aliasing rules. let mut buf = vec![0u8; op.total_len()];
unsafe fn write_iovecs( op.read_bytes_at(0, &mut buf)
&mut self, .map_err(AsyncIoError::WriteVectored)?;
offset: libc::off_t,
iovecs: &[libc::iovec],
) -> AsyncIoResult<usize> {
let mut slices: SmallVec<[IoSlice; DEFAULT_DESCRIPTOR_VEC_SIZE]> =
SmallVec::with_capacity(iovecs.len());
for iovec in iovecs.iter() {
if iovec.iov_len == 0 {
continue;
}
// SAFETY: Guaranteed by write_iovecs' caller.
let slice =
unsafe { std::slice::from_raw_parts(iovec.iov_base.cast::<u8>(), iovec.iov_len) };
slices.push(IoSlice::new(slice));
}
let mut vhdx = self.vhdx_file.lock().unwrap(); let mut vhdx = self.vhdx_file.lock().unwrap();
vhdx.seek(SeekFrom::Start(offset as u64)) vhdx.seek(SeekFrom::Start(offset as u64))
.map_err(AsyncIoError::WriteVectored)?; .map_err(AsyncIoError::WriteVectored)?;
let mut result = 0usize; let result = vhdx.write(&buf).map_err(AsyncIoError::WriteVectored)?;
for slice in slices.iter() {
result += vhdx.write(slice).map_err(AsyncIoError::WriteVectored)?;
}
Ok(result) Ok(result)
} }
} }
@@ -192,21 +156,12 @@ impl AsyncIo for VhdxSync {
} }
fn submit_data_operation(&mut self, op: AsyncIoOperation) -> AsyncIoResult<()> { fn submit_data_operation(&mut self, op: AsyncIoOperation) -> AsyncIoResult<()> {
let offset = op.offset();
let is_read = op.is_read(); let is_read = op.is_read();
let iovecs = op.iovecs(); let mut op = op;
let result = if is_read { let result = if is_read {
// SAFETY: AsyncIoOperation keeps the iovec target alive for this self.read_operation(&mut op)?
// synchronous call. Host-memory operations also satisfy the
// aliasing requirement above; guest-memory-backed iovecs remain a
// temporary unsound VHDX case deferred to a later fix.
unsafe { self.read_iovecs(offset, iovecs)? }
} else { } else {
// SAFETY: AsyncIoOperation keeps the iovec target alive for this self.write_operation(&op)?
// synchronous call. Host-memory operations also satisfy the
// aliasing requirement above; guest-memory-backed iovecs remain a
// temporary unsound VHDX case deferred to a later fix.
unsafe { self.write_iovecs(offset, iovecs)? }
}; };
self.completion_list self.completion_list