fix(format): overflow-checked sizes and fallible allocation on chunked reads

Dataspace and chunk dimensions are untrusted 64-bit fields, but the chunked
read paths computed `num_elements() as usize * elem_size` and
`chunk_dims.product() * elem_size` with plain arithmetic and fed the result to
`vec![0u8; n]`. A crafted file could wrap the product (under-sizing the output
buffer that chunks are then copied into) or request an allocation large enough
to abort the process.

- Dataspace::checked_num_elements, checked_byte_len, checked_chunk_byte_len
  and alloc_output (try_reserve_exact) replace the plain products and
  vec![0; n] at every chunked read site, plus the VDS and hyperslab paths.
  Overflow and allocation failure are FormatError::Overflow.
- Dataspace::num_elements saturates instead of wrapping.
- A zero-element dataset returns early, which also keeps the stride products
  in range when another dimension is huge.
- parallel_read.rs: the three `c_addr + size > len` bounds checks used a raw
  add; they now use checked_add like the rest of the crate.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
osobh
2026-09-19 06:13:39 -07:00
co-authored by Claude Fable 5.1
parent 3ed0489faa
commit 6e84f31ed6
4 changed files with 181 additions and 32 deletions
+29 -1
View File
@@ -1,5 +1,7 @@
//! HDF5 Dataspace message parsing (message type 0x0001).
#[cfg(not(feature = "std"))]
use alloc::format;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
@@ -167,6 +169,27 @@ impl Dataspace {
}
}
/// [`Dataspace::num_elements`] with the product overflow-checked. The
/// dimensions are untrusted 64-bit fields; read paths that size a buffer
/// from them must use this one.
pub fn checked_num_elements(&self) -> Result<u64, FormatError> {
match self.space_type {
DataspaceType::Null => Ok(0),
DataspaceType::Scalar => Ok(1),
DataspaceType::Simple if self.dimensions.is_empty() => Ok(0),
DataspaceType::Simple => self
.dimensions
.iter()
.try_fold(1u64, |acc, &d| acc.checked_mul(d))
.ok_or_else(|| {
FormatError::Overflow(format!(
"dataspace dimensions {:?} overflow the element count",
self.dimensions
))
}),
}
}
/// Total number of elements. Scalar = 1, Null = 0.
pub fn num_elements(&self) -> u64 {
match self.space_type {
@@ -176,7 +199,12 @@ impl Dataspace {
if self.dimensions.is_empty() {
0
} else {
self.dimensions.iter().product()
// Saturate rather than wrap: a wrapped product could
// under-size a buffer. Size-critical callers use
// `checked_num_elements`.
self.dimensions
.iter()
.fold(1u64, |acc, &d| acc.saturating_mul(d))
}
}
}