Merge branch 'fix/p2b-remaining-conformance' into feat/p2b-scale
# Conflicts: # CHANGELOG.md # crates/clawhdf5-format/src/chunked_read.rs
This commit is contained in:
+114
@@ -124,6 +124,98 @@
|
||||
allocation for these frames and for 45,000 fuzzed ones: at most 6x the
|
||||
chunk size, twice the input and 2 MiB of Zstandard state.
|
||||
|
||||
### Remaining conformance errors (2026-09-26)
|
||||
Conformance on tank, `conformance/run.sh --no-fetch`: 598 of 697 files
|
||||
ok (575 before). Of the 5 our-errors left, 3 are corrupt data HDF5 2.0
|
||||
reads only through a bug (listed in `CONFORMANCE.md`), 2 are the Blosc2 and
|
||||
ZFP filters; the 2 mismatches are the known h5py big-endian VL bug. The
|
||||
five files whose cache image libhdf5 cannot load (`cve-2025-6269-*`,
|
||||
`cve-2025-6516`) count as ok because the library, like libhdf5, opens them
|
||||
and fails their objects (see below).
|
||||
- **Metadata cache images are read.** A file written with a metadata cache
|
||||
image keeps its metadata cache entries in an image block the superblock
|
||||
extension points at, and libhdf5 reads them in place of the file's own
|
||||
bytes; in `h5clear_mdc_image.h5` the root group exists only there, and
|
||||
every reader failed with `InvalidObjectHeaderVersion(0)`. `File`,
|
||||
`MmapFile` and `LazyFile` (and `h5rs`) now apply the image at open
|
||||
(`clawhdf5_format::superblock_ext::CacheImage`), with libhdf5's checks.
|
||||
The file is not copied to do it: a mapped file gets the image's entries
|
||||
written into a private copy-on-write mapping
|
||||
(`clawhdf5_io::HDF5Read::private_copy`, `MAP_PRIVATE`), so only the pages
|
||||
they land on are copied, and a buffer the opener owns (`File::from_bytes`,
|
||||
`open_buffered`) is patched in place; files without an image are read
|
||||
from the mapping exactly as before. (An interim version copied the whole
|
||||
file onto the heap: 2 GB of memory to open a 1 GiB sparse file with an
|
||||
image, and an abort for an 8 GiB one; `tests/cache_image_memory.rs`
|
||||
guards it.) An image entry that runs past the end of file is refused
|
||||
(libhdf5 checks only its start; the images it writes never do this). A
|
||||
file whose image libhdf5 cannot load (`cve-2025-6269-*`, `cve-2025-6516`)
|
||||
opens, as in libhdf5, and every object lookup fails with the image's
|
||||
error (`File`, `MmapFile`; `LazyFile` reads the root group at open, so
|
||||
its open fails). libhdf5 fails only its first metadata read and then
|
||||
reads the file's own, possibly stale, bytes; those are never read here.
|
||||
An interim version refused such a file at `File::open` while the
|
||||
conformance probe reported it as libhdf5 does, so the gate counted five
|
||||
files as agreeing with h5py that the library did not open; probe and
|
||||
library now take the decision from the same
|
||||
`superblock_ext::cache_image_state`.
|
||||
- **Every other opener applies the superblock extension and the cache
|
||||
image too** (`superblock_ext::apply_cache_image_in_place`, writing into
|
||||
the buffer each already owns): `clawhdf5_io`'s `NativeVol` (at `open`,
|
||||
and on read for `from_bytes`), `AsyncHDF5File`, `MpiVol` (a minimal edit
|
||||
through the same `vol::load_hdf5`; the `mpi-io` feature cannot be built
|
||||
without an MPI installation, so it was not compiled), and the external
|
||||
source files of a virtual dataset. They read a file with an image from
|
||||
its own bytes — stale metadata, or none (`h5clear_mdc_image.h5` failed
|
||||
with `InvalidObjectHeaderVersion(0)`) — and skipped the extension checks
|
||||
`File::open` makes. These readers cannot open a file and fail each
|
||||
object, so an image libhdf5 cannot load is refused with the image's
|
||||
error.
|
||||
- **The superblock extension is decoded at open, as libhdf5 does:** a File
|
||||
Space Info or Metadata Cache Image message libhdf5 cannot decode makes the
|
||||
open fail (`cve-2020-10810`, `cve-2020-10812` were opened).
|
||||
`FormatError::InvalidSuperblockExtension`, `InvalidCacheImage`.
|
||||
- **Dataset storage libhdf5 refuses at open is refused at open**
|
||||
(`FormatError::InvalidDatasetStorage`, `data_read::check_dataset_storage`):
|
||||
an element count times element size that overflows (`cve-2024-32624`
|
||||
`/Dset_OBJREF` opened and reported its shape), contiguous storage past the
|
||||
end of the file, compact data of the wrong size. An empty contiguous
|
||||
dataset at a defined address, which clawhdf5 up to v2.7.0 wrote, still
|
||||
opens.
|
||||
- **Wrong or missing data fixed:**
|
||||
- a simple dataspace of rank 0 holds one element (it held 0;
|
||||
`cve-2020-18494`), and contiguous storage larger than the dataset reads
|
||||
(`cve-2024-32623`, `cve-2025-2309`; libhdf5 ignores the excess);
|
||||
- scale-offset returned wrong values for ordinary h5py files — see
|
||||
*Correctness* below; E-scale is refused, as in libhdf5; codes past the
|
||||
end of the chunk stay an error (`cve-2025-2308`, where HDF5 2.0 reads
|
||||
past its buffer);
|
||||
- shuffle uses its own parameter as the element size, as libhdf5 does
|
||||
(`cve-2025-44905`);
|
||||
- an unfiltered chunk the index records at other than the chunk's size is
|
||||
refused (it read with zeros for the missing bytes; `cve-2025-44904`);
|
||||
- a v1 B-tree chunk index is read as libhdf5 reads it: each chunk is
|
||||
looked up the way `H5B_find` / `H5D__btree_cmp3` / `H5D__btree_found`
|
||||
look it up, and a chunk that lookup does not find reads as fill values.
|
||||
A key with a non-zero element-size coordinate is found in a 1-D dataset
|
||||
and not in one of rank 2 or more (`cve-2025-44905`
|
||||
`/Shuffle_float_data_le`, which read the chunk's data where h5py reads
|
||||
fill values); an interim fix refused every such key, including 1-D
|
||||
files libhdf5 reads correctly.
|
||||
- **Refused as libhdf5 refuses them:** a v1 group with an empty link name
|
||||
fails its listing (`FormatError::InvalidLinkName`; lookups still work,
|
||||
`cve-2021-46244`); dataspaces with more than 32 dimensions, a rank on a
|
||||
scalar or null dataspace, or a dimension over its maximum
|
||||
(`FormatError::InvalidDataspace`).
|
||||
- `ObjectHeader::object_class` classifies a header as libhdf5 does (a
|
||||
dataset needs a datatype *and* a dataspace).
|
||||
- Conformance harness: user-defined links were listed as objects by the
|
||||
reference, unopenable objects were not deduplicated, nested array types
|
||||
were hashed wrong (`tarray3.h5`), and the attributes of objects h5py
|
||||
cannot open were compared; all fixed. `CONFORMANCE.md` lists the corrupt
|
||||
objects HDF5 2.0 reads through a bug (`bad_nbit_parms_walk.h5` among
|
||||
them: libhdf5's own test now requires that read to fail).
|
||||
|
||||
### Concurrent reads (2026-09-26)
|
||||
- **Full reads of chunked datasets scale with threads again when rayon's
|
||||
pool has one thread.** Each full read handed its chunks to rayon to
|
||||
@@ -910,6 +1002,28 @@
|
||||
- CI keeps zlib-ng building and tested; the arm64 job no longer needs cmake.
|
||||
|
||||
### Correctness
|
||||
- **Scale-offset data read wrong values in every release that decoded it
|
||||
(v2.2.0 to v2.7.0), silently, on ordinary h5py files** (fixed
|
||||
2026-09-26). Of 1480 scale-offset datasets h5py writes across every
|
||||
integer type (`i1` .. `u8`), `f4` and `f8`, both byte orders, with and
|
||||
without a fill value, and `scaleoffset` from 0 to the full width, 332 did
|
||||
not read as h5py reads them: **151 returned wrong values with no error**
|
||||
and 181 failed to read. The common cause was a chunk libhdf5 stores at
|
||||
full width (`minbits` equal to the type's width), which it does for any
|
||||
full-width `scaleoffset` and on its own whenever a chunk's values span
|
||||
most of the type's range: `scaleoffset=0` integer data with a wide range
|
||||
(82 datasets, all wrong values), full-width `u4`/`i4`/`u8`/`i8` (51 wrong
|
||||
values; the narrower types and the rest failed with "truncated minval" or
|
||||
"implausible minbits"), and `f4` D-scale data with a large range (18,
|
||||
wrong values). Such a chunk holds the elements as they are; they were
|
||||
decoded as offsets from `minval`. Also fixed, found on crafted files: the
|
||||
packed codes start at byte 21 whatever size the chunk records for
|
||||
`minval` (`cve-2025-44905` `/Scale_offset_short_data_be`), and a chunk
|
||||
with `minbits` 0 and a fill value is all fill values (it read as
|
||||
`minval`). The whole matrix is now an interop test
|
||||
(`crates/clawhdf5/tests/scaleoffset_interop.rs`, generated by h5py at
|
||||
test time, every dataset compared); on v2.7.0's decoder it reports the
|
||||
332. See `docs/known-issues.md`.
|
||||
- **Corrupt files libhdf5 refuses are now refused instead of read.** On the
|
||||
HDF Group's CVE reproducers, 18 objects that libhdf5 (HDF5 2.0, through
|
||||
h5py) refuses to open were read by clawhdf5, some as wrong data (a chunk
|
||||
|
||||
Reference in New Issue
Block a user