fix: refuse truncated files and read nothing past the recorded end of file
The superblock records where the file's data ends. libhdf5 refuses to
open a file shorter than that ("truncated file", H5F__super_read) and
fails any read past it ("addr overflow" / "address plus size exceeds
file eoa"). clawhdf5 read whatever was left of a truncated file, and read
bytes after the recorded end as if they belonged to the file.
New Superblock::data_end: FormatError::TruncatedFile for a file shorter
than its recorded end, otherwise where the HDF5 data ends. As in libhdf5,
the recorded end moves with the superblock when its recorded base address
is not where it is (a user block added afterwards; cve-2021-36977, which
h5py reads, depends on it), and the check is skipped for a v3 superblock
still being written in SWMR mode. File, LazyFile, MmapFile and the
conformance probe refuse a truncated file and parse only up to the end.
Files clawhdf5 writes record their true length, and the v2.5.0 agent-store
fixture and files written by v2.7.0 (plain, paged, user-block free) pass
the check.
Interop test: h5py writes a file; a copy missing its last 8 bytes must be
refused by both, a copy with bytes appended and one moved behind a new
512-byte user block must read in both.
Conformance (cached corpus, tank): 570 -> 571 ok
(h5clear_fsm_persist_less.h5, whose data past the recorded end was being
read); ten files h5py refuses as truncated (cve-2018-13874,
cve-2018-13876, the family/multi/subfiling members, h5clear_fsm_persist_
greater/user_greater) are now refused at open instead of read.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -100,6 +100,42 @@ pub mod swmr_flags {
|
||||
}
|
||||
|
||||
impl Superblock {
|
||||
/// Where the HDF5 data ends, relative to the superblock, for a file of
|
||||
/// `file_len` bytes whose superblock is at `user_block` (both counted
|
||||
/// from the start of the file), with libhdf5's truncation check
|
||||
/// (`H5F__super_read`).
|
||||
///
|
||||
/// The superblock records the end of the file's data as an absolute
|
||||
/// address. A file shorter than that was truncated, and libhdf5 refuses
|
||||
/// to open it ("truncated file"); so does this, with
|
||||
/// [`FormatError::TruncatedFile`]. Bytes past that address are not part
|
||||
/// of the file: libhdf5 fails any read of them ("addr overflow" /
|
||||
/// "address plus size exceeds file eoa"), so a reader should parse only
|
||||
/// the data up to the returned end. As libhdf5 does for a SWMR reader,
|
||||
/// the check is skipped for a version-3 superblock whose writer is still
|
||||
/// writing it in SWMR mode (it extends the file as it goes); the data
|
||||
/// then ends at the end of the file.
|
||||
///
|
||||
/// When the superblock's recorded base address differs from where the
|
||||
/// superblock actually is (a user block added or removed after the file
|
||||
/// was written), libhdf5 moves the recorded end of file by the same
|
||||
/// amount, and so does this.
|
||||
pub fn data_end(&self, user_block: u64, file_len: u64) -> Result<u64, FormatError> {
|
||||
let eof =
|
||||
i128::from(self.eof_address) - i128::from(self.base_address) + i128::from(user_block);
|
||||
if eof < 0 || eof > i128::from(file_len) {
|
||||
if self.version >= 3 && self.is_swmr_write() {
|
||||
return Ok(file_len.saturating_sub(user_block));
|
||||
}
|
||||
return Err(FormatError::TruncatedFile {
|
||||
stored_eof: u64::try_from(eof).unwrap_or(self.eof_address),
|
||||
actual_len: file_len,
|
||||
});
|
||||
}
|
||||
// 0 <= eof <= file_len, so it fits a u64.
|
||||
Ok((eof as u64).saturating_sub(user_block))
|
||||
}
|
||||
|
||||
/// Whether the file was opened with write access when the superblock was written.
|
||||
pub fn is_write_access(&self) -> bool {
|
||||
self.consistency_flags & swmr_flags::WRITE_ACCESS != 0
|
||||
@@ -537,6 +573,30 @@ mod tests {
|
||||
buf
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_end_refuses_truncated_files_like_libhdf5() {
|
||||
// build_v2_bytes records base 0, end of file 2048.
|
||||
let sb = Superblock::parse(&build_v2_bytes(8, 2), 0).unwrap();
|
||||
assert_eq!(sb.data_end(0, 2048), Ok(2048));
|
||||
// Bytes past the recorded end are not part of the file.
|
||||
assert_eq!(sb.data_end(0, 4096), Ok(2048));
|
||||
assert_eq!(
|
||||
sb.data_end(0, 2047),
|
||||
Err(FormatError::TruncatedFile {
|
||||
stored_eof: 2048,
|
||||
actual_len: 2047
|
||||
})
|
||||
);
|
||||
// A user block added in front after the file was written (the
|
||||
// recorded base address is still 0): the end moves with it.
|
||||
assert_eq!(sb.data_end(512, 2560), Ok(2048));
|
||||
assert!(sb.data_end(512, 2559).is_err());
|
||||
// A v3 superblock still being written in SWMR mode is not checked.
|
||||
let mut swmr = Superblock::parse(&build_v2_bytes(8, 3), 0).unwrap();
|
||||
swmr.consistency_flags = swmr_flags::WRITE_ACCESS | swmr_flags::SWMR_WRITE;
|
||||
assert_eq!(swmr.data_end(0, 1000), Ok(1000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_v0_8byte_offsets() {
|
||||
let data = build_v0_bytes(8);
|
||||
|
||||
Reference in New Issue
Block a user