block: vhdx: validate buffer length against logical sector size

Reject buf.len() that isn't sector_count * logical_sector_size in
io::read/write (VhdxIoError::InvalidBufferLength), and reject buf.len()
not a multiple of the sector size in Vhdx's Read/Write impl, before it's
converted to a sector count/index.

Signed-off-by: Alexander Lvov <alexander.lvov.git@gmail.com>
This commit is contained in:
Alexander Lvov
2026-07-20 18:20:02 +03:00
committed by Rob Bradford
parent 8d6a38770f
commit 013604319d
2 changed files with 96 additions and 4 deletions

View File

@@ -19,6 +19,8 @@ pub enum VhdxIoError {
InvalidBatEntryState,
#[error("Invalid BAT entry count")]
InvalidBatIndex,
#[error("Buffer length does not match the requested sector count")]
InvalidBufferLength,
#[error("Invalid disk size")]
InvalidDiskSize,
#[error("Failed reading sector blocks from file {0}")]
@@ -95,6 +97,12 @@ pub(super) fn read(
if disk_spec.has_parent {
return Err(VhdxIoError::UnsupportedMode);
}
let expected_len = sector_count
.checked_mul(disk_spec.logical_sector_size as u64)
.ok_or(VhdxIoError::InvalidBufferLength)?;
if buf.len() as u64 != expected_len {
return Err(VhdxIoError::InvalidBufferLength);
}
let mut read_count: usize = 0;
while sector_count > 0 {
@@ -147,6 +155,12 @@ pub(super) fn write(
if disk_spec.has_parent {
return Err(VhdxIoError::UnsupportedMode);
}
let expected_len = sector_count
.checked_mul(disk_spec.logical_sector_size as u64)
.ok_or(VhdxIoError::InvalidBufferLength)?;
if buf.len() as u64 != expected_len {
return Err(VhdxIoError::InvalidBufferLength);
}
let mut write_count: usize = 0;
while sector_count > 0 {
@@ -274,4 +288,24 @@ mod tests {
f.file().read_exact_at(&mut readback, DATA_OFFSET).unwrap();
assert_eq!(readback, data);
}
#[test]
fn read_short_buffer_is_rejected() {
let (f, disk_spec, bat) = fixture();
let mut buf = vec![0u8; SECTOR_SIZE as usize - 1];
let err = read(&f, &mut buf, &disk_spec, &bat, 0, 1).unwrap_err();
assert!(matches!(err, VhdxIoError::InvalidBufferLength));
}
#[test]
fn write_short_buffer_is_rejected() {
let (f, mut disk_spec, mut bat) = fixture();
let data = vec![0xCDu8; SECTOR_SIZE as usize - 1];
let err = write(&f, &data, &mut disk_spec, 0, &mut bat, 0, 1).unwrap_err();
assert!(matches!(err, VhdxIoError::InvalidBufferLength));
}
}

View File

@@ -98,8 +98,18 @@ impl Read for Vhdx {
/// Wrapper function to satisfy Read trait implementation for VHDx disk.
/// Convert the offset to sector index and buffer length to sector count.
fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
let sector_count = (buf.len() as u64).div_ceil(self.disk_spec.logical_sector_size as u64);
let sector_index = self.current_offset / self.disk_spec.logical_sector_size as u64;
let sector_size = self.disk_spec.logical_sector_size as u64;
if !(buf.len() as u64).is_multiple_of(sector_size) {
return Err(IoError::new(
IoErrorKind::InvalidInput,
format!(
"Read buffer length {} is not a multiple of the {sector_size}-byte logical sector size",
buf.len()
),
));
}
let sector_count = buf.len() as u64 / sector_size;
let sector_index = self.current_offset / sector_size;
let result = io::read(
&self.aligned,
@@ -129,8 +139,18 @@ impl Write for Vhdx {
/// Wrapper function to satisfy Write trait implementation for VHDx disk.
/// Convert the offset to sector index and buffer length to sector count.
fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
let sector_count = (buf.len() as u64).div_ceil(self.disk_spec.logical_sector_size as u64);
let sector_index = self.current_offset / self.disk_spec.logical_sector_size as u64;
let sector_size = self.disk_spec.logical_sector_size as u64;
if !(buf.len() as u64).is_multiple_of(sector_size) {
return Err(IoError::new(
IoErrorKind::InvalidInput,
format!(
"Write buffer length {} is not a multiple of the {sector_size}-byte logical sector size",
buf.len()
),
));
}
let sector_count = buf.len() as u64 / sector_size;
let sector_index = self.current_offset / sector_size;
if self.first_write {
self.first_write = false;
@@ -305,4 +325,42 @@ mod tests {
assert_eq!(vhdx.read(&mut readback).unwrap(), readback.len());
assert_eq!(readback, data);
}
#[test]
fn read_misaligned_buffer_is_rejected() {
let Some(tf) = create_dynamic_vhdx(16) else {
eprintln!("skipping read_misaligned_buffer_is_rejected: qemu-img unavailable");
return;
};
let file = fs::OpenOptions::new()
.read(true)
.write(true)
.open(tf.as_path())
.unwrap();
let mut vhdx = Vhdx::new(file, false).unwrap();
let mut buf = vec![0u8; vhdx.disk_spec.logical_sector_size as usize - 1];
let err = vhdx.read(&mut buf).unwrap_err();
assert_eq!(err.kind(), IoErrorKind::InvalidInput);
}
#[test]
fn write_misaligned_buffer_is_rejected() {
let Some(tf) = create_dynamic_vhdx(16) else {
eprintln!("skipping write_misaligned_buffer_is_rejected: qemu-img unavailable");
return;
};
let file = fs::OpenOptions::new()
.read(true)
.write(true)
.open(tf.as_path())
.unwrap();
let mut vhdx = Vhdx::new(file, false).unwrap();
let buf = vec![0u8; vhdx.disk_spec.logical_sector_size as usize - 1];
let err = vhdx.write(&buf).unwrap_err();
assert_eq!(err.kind(), IoErrorKind::InvalidInput);
}
}