diff --git a/conformance/report.py b/conformance/report.py index dc82413..3331898 100644 --- a/conformance/report.py +++ b/conformance/report.py @@ -98,13 +98,40 @@ def is_h5py_be_vlen(i): and ">" in (i.get("ours_dtype") or "")) +# Objects the reference (h5py 3.16 / HDF5 2.0) reads only because of an +# HDF5 2.0 bug, and that clawhdf5 refuses: each one reads past a buffer or +# returns bytes the file does not hold, and libhdf5's develop branch refuses the +# first three. (file, object) -> why. Checked 2026-09-26 against HDF5 2.0.0 +# and HDFGroup/hdf5 develop sources; see docs/known-issues.md. +LIBHDF5_BUGS = { + ("cve_hdf5/cvefiles/cve-2025-2308.h5", "/Scale_offset_long_long_data_le"): + "scale-offset codes run past the end of the chunk: HDF5 2.0 reads past its buffer; " + "libhdf5's develop branch refuses the chunk (\"Buffer too short\")", + ("cve_hdf5/cvefiles/cve-2025-44904.h5", "/Scale_offset_float_data_le"): + "unfiltered chunks of 38 and 37 bytes for 48-byte chunks: HDF5 2.0 fills the rest with " + "whatever its buffer held; libhdf5's develop branch refuses them (\"incorrect chunk size returned " + "from index for unfiltered chunk\")", + ("hdf5/test/testfiles/bad_nbit_parms_walk.h5", "/Nbit_int_data_le"): + "an N-Bit parameter list one value short: HDF5 2.0 reads past the list; libhdf5's own " + "test (`test_filter_bad_params`, test/dsets.c) now requires the read to fail", + ("cve_hdf5/cvefiles/cve-2025-44905.h5", "/Shuffle_float_data_le"): + "a chunk B-tree key with element offset 4096: libhdf5's lookup misses the chunk and " + "returns fill values for data the file holds; clawhdf5 refuses the key", +} + + +def is_libhdf5_bug(rel, i): + return i["kind"] == "our-error" and any( + f == rel and i["detail"].startswith(obj + ":") for (f, obj) in LIBHDF5_BUGS) + + known = collections.defaultdict(list) for r in rows: - if r["class"] != "mismatch": - continue iss = issues.get(r["file"], []) - if iss and all(is_h5py_be_vlen(i) for i in iss): + if r["class"] == "mismatch" and iss and all(is_h5py_be_vlen(i) for i in iss): known["h5py-be-vlen"].append(r["file"]) + if r["class"] == "our-error" and iss and all(is_libhdf5_bug(r["file"], i) for i in iss): + known["libhdf5-2.0"].append(r["file"]) # --- the CVE corpus: clawhdf5 vs h5dump vs h5py ------------------------------ @@ -230,9 +257,13 @@ for c in sorted(by_corpus): w(f"| {c} | {sum(cnt.values())} | " + " | ".join(str(cnt.get(k, 0)) for k in CLASSES) + " |") w(f"| **all** | **{len(rows)}** | " + " | ".join(f"**{total.get(k, 0)}**" for k in CLASSES) + " |") w("") -n_known = sum(len(v) for v in known.values()) -if n_known: - w(f"{n_known} of the {total.get('mismatch', 0)} mismatches are a known h5py bug, not ours (see *Known not-our-bug*).") +if known["h5py-be-vlen"]: + w(f"{len(known['h5py-be-vlen'])} of the {total.get('mismatch', 0)} mismatches are a known h5py bug, " + "not ours (see *Known not-our-bug*).") + w("") +if known["libhdf5-2.0"]: + w(f"{len(known['libhdf5-2.0'])} of the {total.get('our-error', 0)} our-errors are corrupt data that " + "HDF5 2.0 reads only through a bug and clawhdf5 refuses (see *Known not-our-bug*).") w("") w("Corpora (fetched by `conformance/fetch-corpus.sh` into the gitignored `conformance/.cache/`):") w("") @@ -311,6 +342,11 @@ if res["incomparable"]: w(" (FP8 -> float16, bfloat16 -> float32, x87 long double -> float128) the values are not") w(" compared (shape and presence still are): " + ", ".join(f"{k} ({n}x)" for k, n in res["incomparable"]) + ".") +w("- **Corrupt data HDF5 2.0 reads through a bug.** clawhdf5 refuses these objects; h5py 3.16 /") +w(" HDF5 2.0 returns values for them that the file does not hold:") +for (f, obj), why in sorted(LIBHDF5_BUGS.items()): + here = "" if f in known["libhdf5-2.0"] else " (not an our-error in this run)" + w(f" - `{f}` `{obj}`: {why}{here}.") w("- **References** are compared by presence only (`R`), not by target.") w("") if res.get("ref_only_errors"):