Merge branch 'fix/p2b-remaining-conformance' into feat/p2b-scale
# Conflicts: # CHANGELOG.md # crates/clawhdf5-format/src/chunked_read.rs
This commit is contained in:
@@ -32,6 +32,80 @@ fn ensure_len(data: &[u8], offset: usize, needed: usize) -> Result<(), FormatErr
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The storage checks libhdf5 makes when it opens a dataset, before any
|
||||
/// data is read (`H5D__contig_check`, `H5D__compact_init`), so a dataset
|
||||
/// they refuse fails to open, as in libhdf5, instead of opening and
|
||||
/// reporting a shape nothing can be read from:
|
||||
///
|
||||
/// - the element count times the element size must not overflow 64 bits
|
||||
/// ("size of dataset's storage overflowed" — `cve-2024-32624`
|
||||
/// `/Dset_OBJREF`, 2^62 references of 8 bytes);
|
||||
/// - contiguous storage at a defined address must end within the file's
|
||||
/// `file_len` bytes (the HDF5 data up to the end of file the superblock
|
||||
/// records);
|
||||
/// - compact data must be exactly the dataset's size.
|
||||
///
|
||||
/// Deliberately not refused, unlike libhdf5: an empty contiguous dataset at
|
||||
/// a defined address (libhdf5's overflow test `addr + 0 <= addr` refuses
|
||||
/// it), which clawhdf5 up to v2.7.0 wrote. Chunked and virtual layouts are
|
||||
/// checked when their data is read.
|
||||
pub fn check_dataset_storage(
|
||||
layout: &DataLayout,
|
||||
dataspace: &Dataspace,
|
||||
datatype: &Datatype,
|
||||
file_len: u64,
|
||||
) -> Result<(), FormatError> {
|
||||
if !matches!(
|
||||
layout,
|
||||
DataLayout::Contiguous { .. } | DataLayout::Compact { .. }
|
||||
) {
|
||||
return Ok(());
|
||||
}
|
||||
const OVERFLOWED: &str = "size of dataset's storage overflowed";
|
||||
let n = dataspace
|
||||
.checked_num_elements()
|
||||
.map_err(|_| FormatError::InvalidDatasetStorage(OVERFLOWED))?;
|
||||
let data_size = n
|
||||
.checked_mul(u64::from(datatype.type_size()))
|
||||
.ok_or(FormatError::InvalidDatasetStorage(OVERFLOWED))?;
|
||||
match layout {
|
||||
DataLayout::Contiguous {
|
||||
address: Some(address),
|
||||
..
|
||||
} if address
|
||||
.checked_add(data_size)
|
||||
.is_none_or(|end| end > file_len) =>
|
||||
{
|
||||
Err(FormatError::InvalidDatasetStorage(
|
||||
"invalid dataset size, likely file corruption",
|
||||
))
|
||||
}
|
||||
DataLayout::Compact { data } if data.len() as u64 != data_size => {
|
||||
Err(FormatError::InvalidDatasetStorage(
|
||||
"bad value from dataset header - size of compact dataset's data buffer \
|
||||
doesn't match size of dataset data",
|
||||
))
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// How many bytes to read from a contiguous dataset's storage of
|
||||
/// `storage_size` bytes (the layout message's size) holding `needed` bytes
|
||||
/// of elements. libhdf5 reads the elements' bytes from the start of the
|
||||
/// storage and ignores storage past them (`H5D__contig_check` checks only
|
||||
/// that the elements fit in the file), so a larger storage reads; one too
|
||||
/// small to hold the elements is an error.
|
||||
pub fn contiguous_read_len(storage_size: u64, needed: usize) -> Result<usize, FormatError> {
|
||||
if storage_size < needed as u64 {
|
||||
return Err(FormatError::DataSizeMismatch {
|
||||
expected: needed,
|
||||
actual: usize::try_from(storage_size).unwrap_or(usize::MAX),
|
||||
});
|
||||
}
|
||||
Ok(needed)
|
||||
}
|
||||
|
||||
/// Zero-copy read of contiguous raw data, returning a borrowed slice.
|
||||
///
|
||||
/// For contiguous layouts, returns a direct `&[u8]` slice into `file_data`.
|
||||
@@ -55,13 +129,7 @@ pub fn read_raw_data_zerocopy<'a>(
|
||||
DataLayout::Contiguous { address, size } => {
|
||||
let addr = address.ok_or(FormatError::NoDataAllocated)?;
|
||||
let addr = addr as usize;
|
||||
let sz = *size as usize;
|
||||
if sz != expected_size {
|
||||
return Err(FormatError::DataSizeMismatch {
|
||||
expected: expected_size,
|
||||
actual: sz,
|
||||
});
|
||||
}
|
||||
let sz = contiguous_read_len(*size, expected_size)?;
|
||||
ensure_len(file_data, addr, sz)?;
|
||||
Ok(Some(&file_data[addr..addr + sz]))
|
||||
}
|
||||
@@ -172,13 +240,7 @@ fn read_raw_data_full_impl(
|
||||
DataLayout::Contiguous { address, size } => {
|
||||
let addr = address.ok_or(FormatError::NoDataAllocated)?;
|
||||
let addr = addr as usize;
|
||||
let sz = *size as usize;
|
||||
if sz != expected_size {
|
||||
return Err(FormatError::DataSizeMismatch {
|
||||
expected: expected_size,
|
||||
actual: sz,
|
||||
});
|
||||
}
|
||||
let sz = contiguous_read_len(*size, expected_size)?;
|
||||
ensure_len(file_data, addr, sz)?;
|
||||
let mut out = crate::bulk_alloc::vec_for_bulk(sz);
|
||||
out.extend_from_slice(&file_data[addr..addr + sz]);
|
||||
@@ -2746,6 +2808,71 @@ mod tests {
|
||||
assert_eq!(result.unwrap(), &[1.5f32, 2.5, 3.5]);
|
||||
}
|
||||
|
||||
/// libhdf5 reads a contiguous dataset's elements from the start of its
|
||||
/// storage and ignores storage past them (cve-2024-32623's scalar
|
||||
/// `/Dset1` has 240 bytes of storage for one 4-byte element). Storage too
|
||||
/// small for the elements is still an error.
|
||||
#[test]
|
||||
fn contiguous_storage_larger_than_the_elements_reads() {
|
||||
let dt = make_f64_le_type();
|
||||
let ds = make_simple_dataspace(&[2]);
|
||||
let mut file_data = vec![0u8; 64];
|
||||
file_data[..8].copy_from_slice(&1.5f64.to_le_bytes());
|
||||
file_data[8..16].copy_from_slice(&2.5f64.to_le_bytes());
|
||||
file_data[16..24].copy_from_slice(&9.0f64.to_le_bytes());
|
||||
let layout = DataLayout::Contiguous {
|
||||
address: Some(0),
|
||||
size: 40,
|
||||
};
|
||||
let raw = read_raw_data(&file_data, &layout, &ds, &dt).unwrap();
|
||||
assert_eq!(raw, file_data[..16]);
|
||||
let zc = read_raw_data_zerocopy(&file_data, &layout, &ds, &dt).unwrap();
|
||||
assert_eq!(zc, Some(&file_data[..16]));
|
||||
let small = DataLayout::Contiguous {
|
||||
address: Some(0),
|
||||
size: 8,
|
||||
};
|
||||
assert!(matches!(
|
||||
read_raw_data(&file_data, &small, &ds, &dt),
|
||||
Err(FormatError::DataSizeMismatch { .. })
|
||||
));
|
||||
}
|
||||
|
||||
/// `H5D__contig_check` / `H5D__compact_init`, run when a dataset opens.
|
||||
#[test]
|
||||
fn dataset_storage_checks_at_open() {
|
||||
let dt = make_f64_le_type();
|
||||
let contiguous = |address| DataLayout::Contiguous { address, size: 0 };
|
||||
// cve-2024-32624 `/Dset_OBJREF`: 2^62 + 2 elements of 8 bytes.
|
||||
let huge = make_simple_dataspace(&[(1 << 62) + 2]);
|
||||
assert_eq!(
|
||||
check_dataset_storage(&contiguous(None), &huge, &dt, 1 << 20),
|
||||
Err(FormatError::InvalidDatasetStorage(
|
||||
"size of dataset's storage overflowed"
|
||||
))
|
||||
);
|
||||
let ds = make_simple_dataspace(&[4]);
|
||||
assert!(check_dataset_storage(&contiguous(Some(100)), &ds, &dt, 132).is_ok());
|
||||
assert!(matches!(
|
||||
check_dataset_storage(&contiguous(Some(100)), &ds, &dt, 131),
|
||||
Err(FormatError::InvalidDatasetStorage(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
check_dataset_storage(&contiguous(Some(u64::MAX - 8)), &ds, &dt, u64::MAX),
|
||||
Err(FormatError::InvalidDatasetStorage(_))
|
||||
));
|
||||
// Not allocated, and (unlike libhdf5) empty at a defined address.
|
||||
assert!(check_dataset_storage(&contiguous(None), &ds, &dt, 0).is_ok());
|
||||
let empty = make_simple_dataspace(&[0]);
|
||||
assert!(check_dataset_storage(&contiguous(Some(64)), &empty, &dt, 64).is_ok());
|
||||
let compact = |n: usize| DataLayout::Compact { data: vec![0; n] };
|
||||
assert!(check_dataset_storage(&compact(32), &ds, &dt, 0).is_ok());
|
||||
assert!(matches!(
|
||||
check_dataset_storage(&compact(24), &ds, &dt, 0),
|
||||
Err(FormatError::InvalidDatasetStorage(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zerocopy_size_mismatch() {
|
||||
let dt = make_f64_le_type();
|
||||
|
||||
Reference in New Issue
Block a user