block: bounds-check the cumulative descriptor length

Request::execute and Request::execute_async checked each data descriptor
against `disk_nsectors` using the request's fixed start sector. With
sector = disk_nsectors-1 and N descriptors of 512 bytes each, every
descriptor passed (top = disk_nsectors) but the vectored I/O
collectively read/wrote N*512 bytes starting at the last sector — N-1
sectors past EOF.

For the io_uring/aio raw backends this lets the guest extend the host
disk image beyond its provisioned size, exhausting the host filesystem.
For fixed-VHD images (footer at end of file) the same chain overwrites
the footer with guest-controlled bytes, corrupting the disk image.

Replace the per-descriptor check with a chain-wide check_data_bounds().
Pre-validating the entire request before beginning the operation avoids
having to unroll a partial submit.

Signed-off-by: Dylan Reid <dgreid@fb.com>
This commit is contained in:
Dylan Reid
2026-04-24 17:03:26 -07:00
committed by Rob Bradford
parent 3df0579866
commit 376434a695

View File

@@ -194,21 +194,12 @@ impl Request {
mem: &vm_memory::GuestMemoryMmap<B>,
serial: &[u8],
) -> Result<u32, ExecuteError> {
self.check_data_bounds(disk_nsectors)?;
disk.seek(SeekFrom::Start(self.sector << SECTOR_SHIFT))
.map_err(ExecuteError::Seek)?;
let mut len = 0;
for (data_addr, data_len) in &self.data_descriptors {
let mut top: u64 = u64::from(*data_len) / SECTOR_SIZE;
if u64::from(*data_len) % SECTOR_SIZE != 0 {
top += 1;
}
top = top
.checked_add(self.sector)
.ok_or(ExecuteError::BadRequest(Error::InvalidOffset))?;
if top > disk_nsectors {
return Err(ExecuteError::BadRequest(Error::InvalidOffset));
}
match self.request_type {
RequestType::In => {
let mut buf = vec![0u8; *data_len as usize];
@@ -264,6 +255,8 @@ impl Request {
let offset = (sector << SECTOR_SHIFT) as libc::off_t;
let alignment = disk_image.alignment();
self.check_data_bounds(disk_nsectors)?;
let mut iovecs: SmallVec<[libc::iovec; DEFAULT_DESCRIPTOR_VEC_SIZE]> =
SmallVec::with_capacity(self.data_descriptors.len());
for &(data_addr, data_len) in &self.data_descriptors {
@@ -275,17 +268,7 @@ impl Request {
if data_len == 0 {
continue;
}
let mut top: u64 = u64::from(data_len) / SECTOR_SIZE;
if u64::from(data_len) % SECTOR_SIZE != 0 {
top += 1;
}
let data_len = data_len as usize;
top = top
.checked_add(sector)
.ok_or(ExecuteError::BadRequest(Error::InvalidOffset))?;
if top > disk_nsectors {
return Err(ExecuteError::BadRequest(Error::InvalidOffset));
}
let origin_ptr = mem
.get_slice(data_addr, data_len)
@@ -595,4 +578,30 @@ impl Request {
pub fn request_type(&self) -> RequestType {
self.request_type
}
/// For In and Out requests, checks that the descriptors collectively fit in a backing disk of
/// the given size. Returns `Ok(())` if they fit, or `ExecuteError::BadRequest` otherwise.
fn check_data_bounds(&self, disk_nsectors: u64) -> Result<(), ExecuteError> {
if !matches!(self.request_type, RequestType::In | RequestType::Out) {
return Ok(());
}
let mut total_bytes: u64 = 0;
for (_, data_len) in &self.data_descriptors {
total_bytes = total_bytes
.checked_add(u64::from(*data_len))
.ok_or(ExecuteError::BadRequest(Error::InvalidOffset))?;
}
if total_bytes == 0 {
return Ok(());
}
let total_sectors = total_bytes.div_ceil(SECTOR_SIZE);
let end_sector = self
.sector
.checked_add(total_sectors)
.ok_or(ExecuteError::BadRequest(Error::InvalidOffset))?;
if end_sector > disk_nsectors {
return Err(ExecuteError::BadRequest(Error::InvalidOffset));
}
Ok(())
}
}