fix(format): validate chunk dimensions and chunk index offsets like libhdf5
A chunk dimension of 0 read a dataset as all fill values, 0x80000000 made
an 8 GiB chunk, and a chunk dimension the chunk index's offsets are not
multiples of read chunks at the wrong place (cve-2018-11205). libhdf5
refuses all of these; now so does clawhdf5:
- DataLayout::parse (H5O__layout_decode): no chunk dimension 0 ("bad chunk
dimension value"), at most 33 dimensions, and before layout v4 at least
2 ("bad dimensions for chunked storage"). New
FormatError::InvalidChunkDimensions.
- Reading a chunked dataset (H5D__chunk_init / H5D__chunk_set_sizes): the
chunk rank must match the dataspace's and a chunk must be under 4 GiB.
One chunked_read::chunk_geometry replaces the four copies of the rank
check.
- v1 B-tree chunk index (H5D__btree_decode_key): every key's offsets must
be multiples of the chunk dimensions, including the keys that only bound
a node, which is where cve-2018-11205's bad dimension shows. New
chunked_read::collect_chunk_info_checked; the chunked read and selection
paths use it.
New interop test header_validation_interop.rs: h5py writes chunked files
(layout v3 and v4), the script corrupts the chunk dimension, and
clawhdf5 must read exactly the copies h5py reads.
Conformance (cached corpus, tank): 570 ok, unchanged; cve-2018-11205 now
refuses the dataset h5py refuses; six more objects that already failed now
fail with libhdf5's reason.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -24,6 +24,34 @@ pub struct VdsMapping {
|
||||
pub virtual_selection: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Most dimensions a layout message can list (libhdf5 `H5O_LAYOUT_NDIMS`):
|
||||
/// 32 dataspace dimensions plus the element size.
|
||||
const MAX_LAYOUT_NDIMS: usize = 33;
|
||||
|
||||
/// libhdf5's checks on a chunked layout message's dimensions
|
||||
/// (`H5O__layout_decode`): at most [`MAX_LAYOUT_NDIMS`], no dimension 0, and
|
||||
/// before version 4 at least one dataspace dimension plus the element size.
|
||||
/// A zero chunk dimension used to read the dataset as all fill values.
|
||||
fn check_chunk_dims(dims: Vec<u32>, layout_version: u8) -> Result<Vec<u32>, FormatError> {
|
||||
if dims.len() > MAX_LAYOUT_NDIMS {
|
||||
return Err(FormatError::InvalidChunkDimensions(
|
||||
"dimensionality is too large".into(),
|
||||
));
|
||||
}
|
||||
if layout_version < 4 && dims.len() < 2 {
|
||||
return Err(FormatError::InvalidChunkDimensions(
|
||||
"bad dimensions for chunked storage".into(),
|
||||
));
|
||||
}
|
||||
if let Some(u) = dims.iter().position(|&d| d == 0) {
|
||||
return Err(FormatError::InvalidChunkDimensions(format!(
|
||||
"bad chunk dimension value when parsing layout message - chunk dimension must be \
|
||||
positive: mesg->u.chunk.dim[{u}] = 0"
|
||||
)));
|
||||
}
|
||||
Ok(dims)
|
||||
}
|
||||
|
||||
/// Parsed HDF5 data layout message.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum DataLayout {
|
||||
@@ -394,7 +422,7 @@ impl DataLayout {
|
||||
Ok(DataLayout::Contiguous { address, size })
|
||||
}
|
||||
_ => Ok(DataLayout::Chunked {
|
||||
chunk_dimensions: dims,
|
||||
chunk_dimensions: check_chunk_dims(dims, 2)?,
|
||||
btree_address: address,
|
||||
version: 3,
|
||||
chunk_index_type: None,
|
||||
@@ -457,7 +485,7 @@ impl DataLayout {
|
||||
p += 4;
|
||||
}
|
||||
Ok(DataLayout::Chunked {
|
||||
chunk_dimensions,
|
||||
chunk_dimensions: check_chunk_dims(chunk_dimensions, 3)?,
|
||||
btree_address,
|
||||
version: 3,
|
||||
chunk_index_type: None,
|
||||
@@ -506,6 +534,11 @@ impl DataLayout {
|
||||
let dimensionality = data[pos + 1] as usize;
|
||||
let dim_size_encoded_length = data[pos + 2] as usize;
|
||||
let mut p = pos + 3;
|
||||
if dimensionality > MAX_LAYOUT_NDIMS {
|
||||
return Err(FormatError::InvalidChunkDimensions(
|
||||
"dimensionality is too large".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// dimension sizes
|
||||
ensure_len(data, p, dimensionality * dim_size_encoded_length)?;
|
||||
@@ -547,6 +580,7 @@ impl DataLayout {
|
||||
chunk_dimensions.push(val);
|
||||
p += dim_size_encoded_length;
|
||||
}
|
||||
let chunk_dimensions = check_chunk_dims(chunk_dimensions, 4)?;
|
||||
|
||||
// chunk index type
|
||||
ensure_len(data, p, 1)?;
|
||||
@@ -755,6 +789,55 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// A v3 chunked layout message with these dims (element size last).
|
||||
fn v3_chunked_msg(dims: &[u32]) -> Vec<u8> {
|
||||
let mut buf = vec![3u8, 2, dims.len() as u8];
|
||||
buf.extend_from_slice(&0x1000u64.to_le_bytes());
|
||||
for d in dims {
|
||||
buf.extend_from_slice(&d.to_le_bytes());
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunk_dimensions_are_checked_when_the_layout_is_parsed() {
|
||||
assert!(DataLayout::parse(&v3_chunked_msg(&[4, 4, 8]), 8, 8).is_ok());
|
||||
// A zero chunk dimension used to read as all fill values.
|
||||
let err = DataLayout::parse(&v3_chunked_msg(&[4, 0, 8]), 8, 8).unwrap_err();
|
||||
assert!(
|
||||
matches!(&err, FormatError::InvalidChunkDimensions(m) if m.contains("dim[1] = 0")),
|
||||
"{err:?}"
|
||||
);
|
||||
// Only the element-size dimension: libhdf5 "bad dimensions".
|
||||
assert_eq!(
|
||||
DataLayout::parse(&v3_chunked_msg(&[8]), 8, 8).unwrap_err(),
|
||||
FormatError::InvalidChunkDimensions("bad dimensions for chunked storage".into())
|
||||
);
|
||||
assert_eq!(
|
||||
DataLayout::parse(&v3_chunked_msg(&[1; 34]), 8, 8).unwrap_err(),
|
||||
FormatError::InvalidChunkDimensions("dimensionality is too large".into())
|
||||
);
|
||||
// v1/v2 and v4 messages get the zero check too.
|
||||
let mut v1 = v1v2_header(1, 2, 2);
|
||||
v1.extend_from_slice(&0x1000u64.to_le_bytes());
|
||||
v1.extend_from_slice(&0u32.to_le_bytes());
|
||||
v1.extend_from_slice(&8u32.to_le_bytes());
|
||||
assert!(matches!(
|
||||
DataLayout::parse(&v1, 8, 8),
|
||||
Err(FormatError::InvalidChunkDimensions(_))
|
||||
));
|
||||
let mut v4 = vec![4u8, 2, 0, 2, 4];
|
||||
v4.extend_from_slice(&0u32.to_le_bytes());
|
||||
v4.extend_from_slice(&8u32.to_le_bytes());
|
||||
v4.push(3); // fixed array index
|
||||
v4.push(0); // page bits
|
||||
v4.extend_from_slice(&0x1000u64.to_le_bytes());
|
||||
assert!(matches!(
|
||||
DataLayout::parse(&v4, 8, 8),
|
||||
Err(FormatError::InvalidChunkDimensions(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v1v2_rejects_bad_class_dimensionality_and_truncation() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user