fix(format): refuse chunk index entries libhdf5 mis-reads
- A dataset without filters stores every chunk at the chunk's full size.
A chunk its index records at another size was read at that size, with
the rest of the chunk left as zeros (cve-2025-44904's
Scale_offset_float_data_le: 38- and 37-byte chunks for 48-byte chunks,
where HDF5 2.0 fills the rest with whatever its buffer held). It is now
refused, as later libhdf5 releases refuse it ("incorrect chunk size
returned from index for unfiltered chunk"):
chunked_read::list_chunks_for_read, used by every read path.
- A v1 B-tree chunk key carries 0 in the element-size dimension. libhdf5
compares that coordinate when it looks a chunk up, so whether it finds a
chunk keyed otherwise depends on where the key falls (in cve-2025-44905
/Shuffle_float_data_le, offset 4096, it does not, and h5py reads fill
values); we read the chunk. Such a key is now refused.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -628,3 +628,47 @@ save("mdc_past_eof", bad)
|
||||
"DSET",
|
||||
);
|
||||
}
|
||||
|
||||
/// Chunk index entries HDF5 2.0 mis-reads, refused here. An unfiltered
|
||||
/// chunk the index records at less than the chunk's size (`cve-2025-44904`):
|
||||
/// HDF5 2.0 fills the rest of the chunk with whatever its buffer held, and
|
||||
/// later libhdf5 releases refuse it ("incorrect chunk size returned from
|
||||
/// index for unfiltered chunk"); we read the rest as zeros. A chunk keyed
|
||||
/// with a non-zero element offset (`cve-2025-44905`): libhdf5's lookup
|
||||
/// compares that coordinate too, so whether it finds the chunk depends on
|
||||
/// where the key falls (in `cve-2025-44905` it does not, and h5py reads
|
||||
/// fill values; in this file it does); we read the chunk.
|
||||
#[test]
|
||||
fn chunk_index_entries_libhdf5_misreads_are_refused() {
|
||||
skip_if_no_python!();
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
run_python(
|
||||
dir.path(),
|
||||
r#"
|
||||
good = os.path.join(d, "good.h5")
|
||||
with h5py.File(good, "w", libver="earliest") as f:
|
||||
f.create_dataset("d", data=np.arange(100, dtype="<i4"), chunks=(37,), fillvalue=-1)
|
||||
data = bytearray(open(good, "rb").read())
|
||||
tree = data.find(b"TREE")
|
||||
while data[tree + 4] != 1: # the chunk index, not the root group's B-tree
|
||||
tree = data.find(b"TREE", tree + 1)
|
||||
assert tree > 0 and data[tree + 5] == 0
|
||||
key = tree + 8 + 16 # the first chunk's key: size, filter mask, offsets
|
||||
second = key + 24 + 8 # a key (4 + 4 + 2 x 8 bytes), then a child address
|
||||
assert struct.unpack_from("<IIQQ", data, second) == (148, 0, 37, 0)
|
||||
bad = bytearray(data); struct.pack_into("<I", bad, second, 100)
|
||||
open(os.path.join(d, "short_chunk.h5"), "wb").write(bad)
|
||||
bad = bytearray(data); struct.pack_into("<Q", bad, second + 16, 4096)
|
||||
open(os.path.join(d, "element_offset.h5"), "wb").write(bad)
|
||||
"#,
|
||||
);
|
||||
assert_eq!(
|
||||
clawhdf5_reads(&dir.path().join("good.h5"), "d"),
|
||||
Ok(()),
|
||||
"good"
|
||||
);
|
||||
for name in ["short_chunk", "element_offset"] {
|
||||
let err = clawhdf5_reads(&dir.path().join(format!("{name}.h5")), "d").unwrap_err();
|
||||
assert!(err.starts_with("read:"), "{name}: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user