From 376434a695cec3baae241cbfc7d2cb664c340600 Mon Sep 17 00:00:00 2001 From: Dylan Reid Date: Fri, 24 Apr 2026 17:03:26 -0700 Subject: [PATCH] block: bounds-check the cumulative descriptor length MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- block/src/request.rs | 51 ++++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/block/src/request.rs b/block/src/request.rs index 49ce3abbb..5f1cf33c8 100644 --- a/block/src/request.rs +++ b/block/src/request.rs @@ -194,21 +194,12 @@ impl Request { mem: &vm_memory::GuestMemoryMmap, serial: &[u8], ) -> Result { + 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(()) + } }