You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

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

  1. Attacker supplies a .h5 file (e.g. a "pretrained model" someone is asked to load).
  2. H5Fopen() β†’ H5F_open() β†’ H5F__super_read() (src/H5Fsuper.c).
  3. Superblock is decoded by H5F__cache_superblock_deserialize() (src/H5Fsuper_cache.c:529):
    H5F_addr_decode(udata->f, (const uint8_t **)&image, &sblock->driver_addr /*out*/);
    
    This only checks that the decode cursor stays inside the read buffer (H5_IS_BUFFER_OVERFLOW), it never checks the decoded address value against the file size or the driver's maxaddr.
  4. Back in H5F__super_read() (src/H5Fsuper.c:588-606), because H5_addr_defined(sblock->driver_addr) is true (any value other than HADDR_UNDEF = UINT64_MAX), the "optional driver information block" branch runs and calls H5FD_set_eoa() with sblock->driver_addr + H5F_DRVINFOBLOCK_HDR_SIZE (line 606).
  5. H5FD_set_eoa() (src/H5FDint.c:2415) asserts the address is defined and <= file->maxaddr. A sufficiently large driver_addr violates 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).
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support