HDF5 H5Fopen: reachable assert() abort via unvalidated superblock driver_addr field
Status: private PoC for bug-bounty triage (huntr). Not a general disclosure. Do not redistribute outside the triage process.
Summary
H5Fopen() (and therefore h5py.File(path), which is how Keras .h5 models are
opened) decodes the HDF5 superblock's "driver information block address"
(sblock->driver_addr) directly from untrusted file bytes with no validation of the
value (only that enough bytes remain in the decode buffer). If that field is a large,
"defined" (i.e. not exactly HADDR_UNDEF / all-1-bits) 64-bit value, it flows
unvalidated into:
/* src/H5Fsuper.c, H5F__super_read(), current develop HEAD */
if (H5FD_set_eoa(f->shared->lf, H5FD_MEM_SUPER, sblock->driver_addr + H5F_DRVINFOBLOCK_HDR_SIZE) < 0)
HGOTO_ERROR(H5E_FILE, H5E_CANTINIT, FAIL, "set end of space allocation request failed");
which in turn hits:
/* src/H5FDint.c */
herr_t
H5FD_set_eoa(H5FD_t *file, H5FD_mem_t type, haddr_t addr)
{
...
assert(file && file->cls);
assert(H5_addr_defined(addr) && addr <= file->maxaddr); /* <-- line 2415 */
...
The only "check" on this attacker-controlled address is a plain C assert(). In any
build where assertions are enabled (the project's own default CMake Debug build type,
many from-source/CI/research builds, tools like h5debug), this assert() fires and
the process aborts the instant H5Fopen() is called on the crafted file β i.e. a
denial-of-service crash reachable from the very first call any Keras/h5py loader makes.
Attacker input β sink trace
- Attacker supplies a
.h5file (e.g. a "pretrained model" someone is asked to load). H5Fopen()βH5F_open()βH5F__super_read()(src/H5Fsuper.c).- Superblock is decoded by
H5F__cache_superblock_deserialize()(src/H5Fsuper_cache.c:529):This only checks that the decode cursor stays inside the read buffer (H5F_addr_decode(udata->f, (const uint8_t **)&image, &sblock->driver_addr /*out*/);H5_IS_BUFFER_OVERFLOW), it never checks the decoded address value against the file size or the driver'smaxaddr. - Back in
H5F__super_read()(src/H5Fsuper.c:588-606), becauseH5_addr_defined(sblock->driver_addr)is true (any value other thanHADDR_UNDEF = UINT64_MAX), the "optional driver information block" branch runs and callsH5FD_set_eoa()withsblock->driver_addr + H5F_DRVINFOBLOCK_HDR_SIZE(line 606). H5FD_set_eoa()(src/H5FDint.c:2415) asserts the address is defined and<= file->maxaddr. A sufficiently largedriver_addrviolates this and aborts the process.
Real, reproduced evidence
Built the current develop HEAD of HDFGroup/hdf5
(b58ab3cc1f80e876b7bddb6f120817b5ba84f5ef, 2026-07-06) with
-fsanitize=address,undefined (and a separate -fsanitize=fuzzer-no-link build for
coverage-guided fuzzing), linked against harness.c (included here) β a libFuzzer
harness that opens the fuzz input via the unmodified public API (H5Fopen) and then
walks the file the way a Keras loader does: H5Literate2 over groups/datasets,
H5Aiterate2 over attributes, H5Dread/H5Aread on data including variable-length
strings (the type Keras uses for JSON model_config/layer-name attributes).
Coverage-guided fuzzing (3 workers, corpus seeded from h5py-generated
Keras-style .h5 files) found this crash within seconds:
harness: src/H5FDint.c:2415: herr_t H5FD_set_eoa(H5FD_t *, H5FD_mem_t, haddr_t):
Assertion `H5_addr_defined(addr) && addr <= file->maxaddr' failed.
==...== ERROR: libFuzzer: deadly signal
...
#8 H5FD_set_eoa src/H5FDint.c:2415
#9 H5F__super_read src/H5Fsuper.c:606
#10 H5F_open src/H5Fint.c:2117
#11 H5VL__native_file_open src/H5VLnative_file.c:127
...
#15 H5Fopen src/H5F.c:820
#16 LLVMFuzzerTestOneInput harness.c:147
Two independent fuzzer-discovered crashing inputs are attached
(fuzzer_found_crash_1.h5, fuzzer_found_crash_2.h5).
Minimal deterministic PoC
poc_driverinfo_oob.h5 is a single-field patch of an ordinary, valid, h5py-written
"Keras-style" .h5 file (group hierarchy, JSON attrs, weight datasets, vlen-string
dataset) β only the 8-byte superblock driver_info_addr field (byte offset 48, for
size_of_offsets = 8) is changed, from 0xFFFFFFFFFFFFFFFF (undefined/absent, the
normal value) to 0xFF00FFFFFFFFFFFF (large, but "defined"). Everything else in the
file β signature, all other superblock fields, root group, datasets, attributes β is
byte-for-byte what h5py.File(..., "w") produced. Reproduces the abort 100% of the
time against the ASan build:
$ ASAN_OPTIONS=detect_leaks=0 ./harness poc_driverinfo_oob.h5
harness: .../src/H5FDint.c:2415: ... Assertion `H5_addr_defined(addr) && addr <= file->maxaddr' failed.
==...== ERROR: libFuzzer: deadly signal
Build-configuration nuance (tested honestly)
The official h5py PyPI wheel (bundles HDF5 2.0.0, built as a release/NDEBUG
binary) does not abort on this exact PoC β assert() compiles to a no-op there, so
execution falls through, H5FD_set_eoa accepts the bogus offset, and the subsequent
attempt to actually read the driver-info block at that offset fails I/O cleanly,
surfacing as OSError: Unable to synchronously open file. So the crash is confirmed
and 100%-reproducible in assertion-enabled builds (the project's own default CMake
Debug type, many from-source/dev/research/CI builds, and debug tooling such as
h5debug); in NDEBUG release builds the same missing validation is currently masked by
a downstream I/O failure rather than causing a crash. Either way, the root defect is
real: an attacker-controlled 64-bit file offset is used with no bounds/sanity
validation, and the only safety net is a debug-only assert() rather than a
proper HGOTO_ERROR check (the pattern already used for the sibling fields decoded a
few lines earlier in the same function, e.g. H5_IS_BUFFER_OVERFLOW checks).
Dedup / prior-art check
Searched HDFGroup/hdf5 GitHub issues/PRs (open and closed) for
H5FD_set_eoa, maxaddr, H5F__super_read, driver_addr, drvinfo, "driver info
block", H5F_DRVINFOBLOCK_HDR_SIZE β no existing report matches this call path or
crash site. (Confirmed several other, unrelated HDF5 fuzzer bugs are already publicly
open right now β e.g. #6401 H5O__mtime_new_decode, #6377 H5HL_protect/
H5HL_unprotect NULL derefs, #5831/#5834 β none of which involve driver_addr /
H5FD_set_eoa / the driver-info-block path.)
Suggested fix
Validate sblock->driver_addr (and the computed
driver_addr + H5F_DRVINFOBLOCK_HDR_SIZE) against the file's real extent /
file->maxaddr in H5F__cache_superblock_deserialize() or at the top of the "Decode
the optional driver information block" branch in H5F__super_read(), returning
HGOTO_ERROR (as the surrounding code already does for other superblock fields)
instead of relying on the assert() inside H5FD_set_eoa().
Files
harness.cβ libFuzzer harness (unmodified public HDF5 API only).poc_driverinfo_oob.h5β minimal, deterministic, single-field-patched PoC.fuzzer_found_crash_1.h5,fuzzer_found_crash_2.h5β original fuzzer-discovered crashing inputs (kept for provenance).