test: a v2 header message running into the checksum is refused, as in libhdf5

The review read libhdf5's H5O__chunk_deserialize as accepting a v2
message that runs up to 4 bytes into the chunk's checksum, since it
bounds message bodies by the whole chunk buffer. It does not accept it:
the message loop stops at the checksum, and the checksum read that
follows starts past it and overruns the chunk ("ran off end of input
buffer while decoding"). h5py refuses such files whether the message
runs 1, 4 or 5 bytes in, and so does clawhdf5, with its own error text.
No code change; the test pins the agreement and a comment records why.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-26 01:32:00 -05:00
co-authored by Claude Opus 5.5
parent 3938f7f8a2
commit 993214723e
2 changed files with 55 additions and 0 deletions
@@ -421,6 +421,13 @@ impl ObjectHeader {
}; };
pos += msg_header_size; pos += msg_header_size;
// `end` is where the messages stop and the checksum starts.
// libhdf5 bounds a message by the chunk including its checksum,
// but a message that runs into the checksum still fails there:
// its loop stops at the checksum, and reading the checksum from
// past its start overruns the chunk ("ran off end of input
// buffer while decoding"). Both refuse it; only the text
// differs.
if msg_data_size > end - pos { if msg_data_size > end - pos {
return Err(FormatError::InvalidObjectHeader( return Err(FormatError::InvalidObjectHeader(
"message size exceeds buffer end", "message size exceeds buffer end",
@@ -252,6 +252,54 @@ for libver in ("earliest", "latest"):
} }
} }
/// A v2 object header message whose size runs past the messages into the
/// chunk's checksum is refused by libhdf5 whether it runs 1 byte or more
/// into the checksum (its loop stops at the checksum, and then the checksum
/// read and the size check fail), so it is refused here too.
#[test]
fn v2_header_message_running_into_the_checksum_is_refused() {
skip_if_no_python!();
let dir = tempfile::tempdir().unwrap();
let body = format!(
"{FIX_OHDR_PY}{}",
r#"
good = os.path.join(d, "good.h5")
with h5py.File(good, "w", libver="latest") as f:
f.create_dataset("d", data=np.arange(4, dtype="<i4"))
raw = bytearray(open(good, "rb").read())
off = raw.rfind(b"OHDR")
flags = raw[off + 5]; p = off + 6
if flags & 0x20: p += 16
if flags & 0x10: p += 4
width = 1 << (flags & 3)
end = p + width + int.from_bytes(raw[p:p + width], "little"); p += width
hdr = 6 if flags & 0x04 else 4
last = p
while p + hdr <= end:
last = p
p += hdr + int.from_bytes(raw[p + 1:p + 3], "little")
assert p == end
size = int.from_bytes(raw[last + 1:last + 3], "little")
for k in (1, 4, 5):
bad = bytearray(raw)
bad[last + 1:last + 3] = (size + k).to_bytes(2, "little")
fix_ohdr(bad, off)
open(os.path.join(d, f"into_checksum_{k}.h5"), "wb").write(bad)
"#
);
let verdicts = h5py_verdicts(dir.path(), &body);
assert_agrees_with_h5py(
dir.path(),
&verdicts,
&[
"good ok",
"into_checksum_1 ERROR",
"into_checksum_4 ERROR",
"into_checksum_5 ERROR",
],
);
}
#[test] #[test]
fn truncated_files_are_refused_and_nothing_past_the_end_of_file_is_read() { fn truncated_files_are_refused_and_nothing_past_the_end_of_file_is_read() {
skip_if_no_python!(); skip_if_no_python!();