fix(tools,wasm): resolve VL data through the library's VlResolver
h5rs (dump, ls, diff, check --data) kept its own lenient VL decoder: a heap object longer than its element was cut to the element's length (libhdf5 and h5py refuse it), a null string printed "" where h5dump prints NULL, the stored element size was trusted, and every heap collection was kept as an owned copy for the whole run. It now resolves each element with VlResolver::element / string_element (new: one element in place, borrowing from the file), and refuses a VL type whose stored element size is not 4 + offset size + 4, as File does. H5::heap_object and its cache are gone. h5diff compares a null VL string equal to an empty one; so does h5rs diff. clawhdf5-wasm already resolved VL strings with read_vl_strings; it now uses VlResolver and checks the stored element size before reading, as File::read_string does. Tests (h5py writes the files, patched for "a\0b", a null element and mis-sized heap objects, with 8- and 4-byte offsets): - h5rs_interop dump_prints_vl_data_like_h5dump: byte-identical to h5dump; - dump_json_vl_values_match_h5py: h5py's values, errors where h5py fails; - check_data_flags_mis_sized_vl_heap_objects; - clawhdf5-wasm tests/vl_strings.rs: wasm, File and h5py agree. All four fail before. check --data over the 150 cve_hdf5 CVE and fuzzer files now passes 15 (h5dump rejects 8 of them), was 16 and 9: the stored-size check flags cve-2024-32608. h5rs-check-ok-files.sh --data: 0 of 422 flagged; h5rs-fuzz.sh: clean on 180 files. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -15,11 +15,13 @@ use clawhdf5_format::btree_v2::{BTreeV2Header, collect_btree_v2_records};
|
||||
use clawhdf5_format::data_layout::DataLayout;
|
||||
use clawhdf5_format::dataspace::{Dataspace, DataspaceType};
|
||||
use clawhdf5_format::datatype::Datatype;
|
||||
use clawhdf5_format::error::FormatError;
|
||||
use clawhdf5_format::group_info::GroupInfoMessage;
|
||||
use clawhdf5_format::link_info::LinkInfoMessage;
|
||||
use clawhdf5_format::message_type::MessageType;
|
||||
use clawhdf5_format::object_header::ObjectHeader;
|
||||
use clawhdf5_format::symbol_table::SymbolTableMessage;
|
||||
use clawhdf5_format::vl_data::{VlResolver, check_element_size, parse_vl_references};
|
||||
|
||||
use crate::cli::{Args, Out};
|
||||
use crate::h5::{Error, ErrorKind, H5, Kind};
|
||||
@@ -49,6 +51,15 @@ found, 3 internal error.";
|
||||
|
||||
const MAX_CHUNKS_CHECKED: usize = 10_000_000;
|
||||
|
||||
/// A variable-length element's problem, worded as `check` reports heap
|
||||
/// problems ("global heap ...").
|
||||
fn heap_problem(e: FormatError) -> String {
|
||||
match e {
|
||||
FormatError::VlDataError(m) if m.starts_with("global heap") => m,
|
||||
e => format!("global heap: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether values of `dt` hold variable-length data (in the global heap).
|
||||
fn has_vl(dt: &Datatype, depth: u32) -> bool {
|
||||
if depth > 32 {
|
||||
@@ -104,6 +115,8 @@ struct Checker<'a> {
|
||||
btrees_seen: HashSet<u64>,
|
||||
/// Global heap collections already read (with --data).
|
||||
gcols_seen: HashSet<u64>,
|
||||
/// Resolves variable-length elements (with --data), for the whole file.
|
||||
vl: VlResolver<'a>,
|
||||
panicked: bool,
|
||||
}
|
||||
|
||||
@@ -157,6 +170,7 @@ pub fn run(args: &mut Args, out: &mut Out) -> std::io::Result<i32> {
|
||||
heaps_seen: HashSet::new(),
|
||||
btrees_seen: HashSet::new(),
|
||||
gcols_seen: HashSet::new(),
|
||||
vl: VlResolver::new(h5.data(), h5.os(), h5.ls()),
|
||||
panicked: false,
|
||||
};
|
||||
c.superblock();
|
||||
@@ -707,62 +721,48 @@ impl Checker<'_> {
|
||||
}
|
||||
match dt {
|
||||
Datatype::VariableLength {
|
||||
size,
|
||||
is_string,
|
||||
base_type,
|
||||
..
|
||||
} => {
|
||||
let os = usize::from(self.h5.os());
|
||||
let (Some(lenb), Some(addrb), Some(idxb)) =
|
||||
(b.get(..4), b.get(4..4 + os), b.get(4 + os..8 + os))
|
||||
else {
|
||||
// Resolved by the library's VlResolver, as every other
|
||||
// reader resolves them (and as libhdf5 does): a heap object
|
||||
// whose size is not the element's length × base size, a
|
||||
// collection that overlaps another, or a missing object is
|
||||
// a problem at the collection's address.
|
||||
let Ok(vl) = parse_vl_references(b, 1, self.h5.os()) else {
|
||||
return;
|
||||
};
|
||||
let le = |x: &[u8]| {
|
||||
x.iter()
|
||||
.enumerate()
|
||||
.fold(0u64, |a, (i, &v)| a | (u64::from(v) << (8 * i)))
|
||||
};
|
||||
let (len, gcol, idx) = (le(lenb), le(addrb), le(idxb));
|
||||
let undef = if os >= 8 {
|
||||
u64::MAX
|
||||
} else {
|
||||
(1u64 << (8 * os)) - 1
|
||||
};
|
||||
if len == 0 || gcol == 0 || gcol == undef || bad.contains_key(&gcol) {
|
||||
let gcol = vl[0].collection_address;
|
||||
if gcol == 0 || bad.contains_key(&gcol) {
|
||||
return;
|
||||
}
|
||||
let obj = match self.h5.heap_object(gcol, idx as u32) {
|
||||
Ok(o) => o,
|
||||
if let Err(e) = check_element_size(*size, self.h5.os()) {
|
||||
bad.insert(gcol, e.to_string());
|
||||
return;
|
||||
}
|
||||
let bs = if *is_string {
|
||||
1
|
||||
} else {
|
||||
base_type.type_size() as usize
|
||||
};
|
||||
if bs == 0 {
|
||||
return;
|
||||
}
|
||||
let obj = match self.vl.element(b, bs) {
|
||||
Ok(o) => o.unwrap_or(&[]),
|
||||
Err(e) => {
|
||||
bad.insert(e.addr.unwrap_or(gcol), e.msg);
|
||||
bad.insert(gcol, heap_problem(e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
if self.gcols_seen.insert(gcol) {
|
||||
self.counts.global_heaps += 1;
|
||||
}
|
||||
let bs = if *is_string {
|
||||
1
|
||||
} else {
|
||||
u64::from(base_type.type_size())
|
||||
};
|
||||
if len
|
||||
.checked_mul(bs)
|
||||
.is_none_or(|need| need > obj.len() as u64)
|
||||
{
|
||||
bad.insert(
|
||||
gcol,
|
||||
format!(
|
||||
"global heap object {idx} holds {} bytes; the element needs {len} x {bs}",
|
||||
obj.len()
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if !*is_string && bs > 0 && has_vl(base_type, depth + 1) {
|
||||
let bs = bs as usize;
|
||||
for k in 0..len as usize {
|
||||
self.vl_element(base_type, &obj[k * bs..(k + 1) * bs], depth + 1, bad);
|
||||
if !*is_string && has_vl(base_type, depth + 1) {
|
||||
for eb in obj.chunks_exact(bs) {
|
||||
self.vl_element(base_type, eb, depth + 1, bad);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user