format: read object headers with long continuation chains
libhdf5 follows any number of continuation chunks, and a header that is full gains one per message added (each new chunk holding the next continuation message), so a version-1 header with a few dozen attributes added one at a time is a chain dozens of chunks long. The reader recursed once per chunk and refused a chain deeper than 32 (NestingDepthExceeded): h5py read such files, we did not. Version-2 headers stopped at 256 continuation chunks. Version-1 chunks are now followed with an explicit stack (the same depth-first message order as before), version-2 ones as before; both refuse a chunk address seen twice (a cycle, what the limits guarded against) and more than 65 536 chunks. Regression: long_v1_continuation_chains_are_read (a 200-chunk chain), v1_continuation_cycles_are_refused; the dense-attribute interop test's 'earliest' case produces such a chain. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
//! HDF5 Object Header parsing (v1 and v2).
|
//! HDF5 Object Header parsing (v1 and v2).
|
||||||
|
|
||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::vec::Vec;
|
use alloc::{collections::BTreeSet, vec, vec::Vec};
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
use byteorder::{ByteOrder, LittleEndian};
|
use byteorder::{ByteOrder, LittleEndian};
|
||||||
|
|
||||||
@@ -196,7 +198,6 @@ impl ObjectHeader {
|
|||||||
header_data_size,
|
header_data_size,
|
||||||
offset_size,
|
offset_size,
|
||||||
length_size,
|
length_size,
|
||||||
MAX_V1_CONTINUATION_DEPTH,
|
|
||||||
&mut messages,
|
&mut messages,
|
||||||
)?;
|
)?;
|
||||||
// libhdf5 reads every message in the first chunk and refuses a header
|
// libhdf5 reads every message in the first chunk and refuses a header
|
||||||
@@ -230,49 +231,55 @@ impl ObjectHeader {
|
|||||||
/// 8; libhdf5 refuses a message that is not aligned, that runs past the
|
/// 8; libhdf5 refuses a message that is not aligned, that runs past the
|
||||||
/// end of the chunk, or leftover bytes too few for a message header (a
|
/// end of the chunk, or leftover bytes too few for a message header (a
|
||||||
/// "gap", which only version 2 allows).
|
/// "gap", which only version 2 allows).
|
||||||
#[allow(clippy::too_many_arguments)]
|
///
|
||||||
|
/// Continuation chunks are followed depth-first, as met, with an
|
||||||
|
/// explicit stack: a chain of continuation chunks as long as libhdf5
|
||||||
|
/// writes (each new chunk holding the next continuation message, one
|
||||||
|
/// per attribute added to a full header) is read without recursion.
|
||||||
|
/// A chunk address seen twice is a cycle and refused, as is a header of
|
||||||
|
/// more than [`MAX_V1_CHUNKS`] chunks.
|
||||||
fn parse_v1_chunk<S: Storage + ?Sized>(
|
fn parse_v1_chunk<S: Storage + ?Sized>(
|
||||||
file: &S,
|
file: &S,
|
||||||
offset: u64,
|
offset: u64,
|
||||||
length: usize,
|
length: usize,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
depth_remaining: u16,
|
|
||||||
messages: &mut Vec<HeaderMessage>,
|
messages: &mut Vec<HeaderMessage>,
|
||||||
) -> Result<usize, FormatError> {
|
) -> Result<usize, FormatError> {
|
||||||
if depth_remaining == 0 {
|
let mut stack = vec![(read_exact_at(file, offset, length)?, 0usize)];
|
||||||
return Err(FormatError::NestingDepthExceeded);
|
let mut seen = BTreeSet::new();
|
||||||
}
|
seen.insert(offset);
|
||||||
let chunk = read_exact_at(file, offset, length)?;
|
|
||||||
let data: &[u8] = &chunk;
|
|
||||||
let end = length;
|
|
||||||
let mut pos = 0usize;
|
|
||||||
let mut count = 0usize;
|
let mut count = 0usize;
|
||||||
|
|
||||||
while pos < end {
|
while let Some((chunk, pos)) = stack.last_mut() {
|
||||||
if end - pos < V1_MSG_HEADER_SIZE {
|
let data: &[u8] = chunk;
|
||||||
|
let end = data.len();
|
||||||
|
if *pos >= end {
|
||||||
|
stack.pop();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if end - *pos < V1_MSG_HEADER_SIZE {
|
||||||
return Err(FormatError::InvalidObjectHeader(
|
return Err(FormatError::InvalidObjectHeader(
|
||||||
"gap found in early version of file format",
|
"gap found in early version of file format",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let msg_type_raw = LittleEndian::read_u16(&data[pos..pos + 2]);
|
let p = *pos;
|
||||||
let msg_data_size = LittleEndian::read_u16(&data[pos + 2..pos + 4]) as usize;
|
let msg_type_raw = LittleEndian::read_u16(&data[p..p + 2]);
|
||||||
let msg_flags = data[pos + 4];
|
let msg_data_size = LittleEndian::read_u16(&data[p + 2..p + 4]) as usize;
|
||||||
// reserved(3) at pos+5..pos+8
|
let msg_flags = data[p + 4];
|
||||||
pos += V1_MSG_HEADER_SIZE;
|
// reserved(3) at p+5..p+8
|
||||||
|
let p = p + V1_MSG_HEADER_SIZE;
|
||||||
|
|
||||||
if !msg_data_size.is_multiple_of(8) {
|
if !msg_data_size.is_multiple_of(8) {
|
||||||
return Err(FormatError::InvalidObjectHeader("message not aligned"));
|
return Err(FormatError::InvalidObjectHeader("message not aligned"));
|
||||||
}
|
}
|
||||||
if msg_data_size > end - pos {
|
if msg_data_size > end - p {
|
||||||
return Err(FormatError::InvalidObjectHeader(
|
return Err(FormatError::InvalidObjectHeader(
|
||||||
"message size exceeds buffer end",
|
"message size exceeds buffer end",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let body = &data[pos..pos + msg_data_size];
|
let body = &data[p..p + msg_data_size];
|
||||||
check_message(1, msg_type_raw, msg_flags, body, offset_size, length_size)?;
|
check_message(1, msg_type_raw, msg_flags, body, offset_size, length_size)?;
|
||||||
count += 1;
|
|
||||||
|
|
||||||
let msg_type = MessageType::from_u16(msg_type_raw);
|
let msg_type = MessageType::from_u16(msg_type_raw);
|
||||||
if msg_type != MessageType::Nil {
|
if msg_type != MessageType::Nil {
|
||||||
messages.push(HeaderMessage {
|
messages.push(HeaderMessage {
|
||||||
@@ -283,22 +290,26 @@ impl ObjectHeader {
|
|||||||
data: body.to_vec(),
|
data: body.to_vec(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
pos += msg_data_size;
|
|
||||||
|
|
||||||
// Follow continuations (v1 continuation chunks are just raw
|
// Follow continuations (v1 continuation chunks are just raw
|
||||||
// messages, no signature); check_message has checked the body.
|
// messages, no signature); check_message has checked the body.
|
||||||
if msg_type == MessageType::ObjectHeaderContinuation {
|
let cont = if msg_type == MessageType::ObjectHeaderContinuation {
|
||||||
let cont_offset = to_usize(read_offset(body, 0, offset_size)?)?;
|
Some((
|
||||||
let cont_length = to_usize(read_offset(body, offset_size as usize, length_size)?)?;
|
read_offset(body, 0, offset_size)?,
|
||||||
Self::parse_v1_chunk(
|
to_usize(read_offset(body, offset_size as usize, length_size)?)?,
|
||||||
file,
|
))
|
||||||
cont_offset as u64,
|
} else {
|
||||||
cont_length,
|
None
|
||||||
offset_size,
|
};
|
||||||
length_size,
|
*pos = p + msg_data_size;
|
||||||
depth_remaining - 1,
|
// Only the first chunk's messages are held to the prefix count.
|
||||||
messages,
|
if stack.len() == 1 {
|
||||||
)?;
|
count += 1;
|
||||||
|
}
|
||||||
|
if let Some((cont_offset, cont_length)) = cont {
|
||||||
|
if !seen.insert(cont_offset) || seen.len() > MAX_V1_CHUNKS {
|
||||||
|
return Err(FormatError::NestingDepthExceeded);
|
||||||
|
}
|
||||||
|
stack.push((read_exact_at(file, cont_offset, cont_length)?, 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,13 +436,15 @@ impl ObjectHeader {
|
|||||||
&mut continuations,
|
&mut continuations,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Follow continuations (limit to prevent cycles in malformed data)
|
// Follow continuations. A chunk address seen twice is a cycle in
|
||||||
let mut cont_remaining = 256u16;
|
// malformed data; a valid header can have many chunks (libhdf5 adds
|
||||||
|
// one whenever a message no longer fits), up to the same bound as a
|
||||||
|
// version-1 header.
|
||||||
|
let mut seen = BTreeSet::new();
|
||||||
while let Some((cont_offset, cont_length)) = continuations.pop() {
|
while let Some((cont_offset, cont_length)) = continuations.pop() {
|
||||||
if cont_remaining == 0 {
|
if !seen.insert(cont_offset) || seen.len() > MAX_V1_CHUNKS {
|
||||||
return Err(FormatError::NestingDepthExceeded);
|
return Err(FormatError::NestingDepthExceeded);
|
||||||
}
|
}
|
||||||
cont_remaining -= 1;
|
|
||||||
Self::parse_v2_continuation(
|
Self::parse_v2_continuation(
|
||||||
file,
|
file,
|
||||||
cont_offset as u64,
|
cont_offset as u64,
|
||||||
@@ -594,8 +607,10 @@ const V2_PREFIX_MAX: usize = 34;
|
|||||||
/// Size of a version-1 message header: type(2) + size(2) + flags(1) + reserved(3).
|
/// Size of a version-1 message header: type(2) + size(2) + flags(1) + reserved(3).
|
||||||
const V1_MSG_HEADER_SIZE: usize = 8;
|
const V1_MSG_HEADER_SIZE: usize = 8;
|
||||||
|
|
||||||
/// How deep version-1 continuation chunks may chain (malformed-data guard).
|
/// Most chunks a version-1 object header may have (malformed-data guard;
|
||||||
const MAX_V1_CONTINUATION_DEPTH: u16 = 32;
|
/// libhdf5 has no limit, and a header that gains one continuation chunk per
|
||||||
|
/// attribute added can have many).
|
||||||
|
const MAX_V1_CHUNKS: usize = 1 << 16;
|
||||||
|
|
||||||
/// Every defined version-2 object header status flag (libhdf5
|
/// Every defined version-2 object header status flag (libhdf5
|
||||||
/// `H5O_HDR_ALL_FLAGS`): chunk-0 size width (bits 0-1), attribute creation
|
/// `H5O_HDR_ALL_FLAGS`): chunk-0 size width (bits 0-1), attribute creation
|
||||||
@@ -901,6 +916,71 @@ mod tests {
|
|||||||
assert_eq!(hdr.messages[1].data[..2], [5, 6]);
|
assert_eq!(hdr.messages[1].data[..2], [5, 6]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A version-1 header whose continuation chunks form a chain: chunk k
|
||||||
|
/// holds a Dataspace message `[k]` and the continuation to chunk k + 1.
|
||||||
|
/// With `cycle`, the last chunk points back at the first continuation
|
||||||
|
/// chunk.
|
||||||
|
fn v1_chain(n: usize, cycle: bool) -> Vec<u8> {
|
||||||
|
// Each continuation chunk: dataspace (8 + 8) + continuation (8 + 16).
|
||||||
|
let chunk_len = 40u64;
|
||||||
|
let first = 64u64;
|
||||||
|
let cont = |addr: u64| {
|
||||||
|
let mut b = addr.to_le_bytes().to_vec();
|
||||||
|
b.extend_from_slice(&chunk_len.to_le_bytes());
|
||||||
|
b
|
||||||
|
};
|
||||||
|
let mut data = build_v1_header(&[(0x0010, &cont(first)[..], 0)], 8, 8);
|
||||||
|
data.resize(first as usize, 0);
|
||||||
|
for k in 0..n {
|
||||||
|
let mut c = Vec::new();
|
||||||
|
c.extend_from_slice(&1u16.to_le_bytes());
|
||||||
|
c.extend_from_slice(&8u16.to_le_bytes());
|
||||||
|
c.extend_from_slice(&[0; 4]);
|
||||||
|
c.extend_from_slice(&(k as u64).to_le_bytes());
|
||||||
|
let next = if k + 1 < n {
|
||||||
|
first + (k as u64 + 1) * chunk_len
|
||||||
|
} else if cycle {
|
||||||
|
first
|
||||||
|
} else {
|
||||||
|
// The last chunk ends in a NIL message instead.
|
||||||
|
c.extend_from_slice(&[0, 0, 16, 0, 0, 0, 0, 0]);
|
||||||
|
c.extend_from_slice(&[0; 16]);
|
||||||
|
data.extend_from_slice(&c);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
c.extend_from_slice(&0x10u16.to_le_bytes());
|
||||||
|
c.extend_from_slice(&16u16.to_le_bytes());
|
||||||
|
c.extend_from_slice(&[0; 4]);
|
||||||
|
c.extend_from_slice(&cont(next));
|
||||||
|
data.extend_from_slice(&c);
|
||||||
|
}
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// libhdf5 reads any chain of continuation chunks (a header grows one
|
||||||
|
/// per attribute added when full); the reader used to stop at 32.
|
||||||
|
#[test]
|
||||||
|
fn long_v1_continuation_chains_are_read() {
|
||||||
|
let data = v1_chain(200, false);
|
||||||
|
let hdr = ObjectHeader::parse(&data, 0, 8, 8).unwrap();
|
||||||
|
let spaces: Vec<u8> = hdr
|
||||||
|
.messages
|
||||||
|
.iter()
|
||||||
|
.filter(|m| m.msg_type == MessageType::Dataspace)
|
||||||
|
.map(|m| m.data[0])
|
||||||
|
.collect();
|
||||||
|
assert_eq!(spaces, (0..200).map(|k| k as u8).collect::<Vec<_>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn v1_continuation_cycles_are_refused() {
|
||||||
|
let data = v1_chain(5, true);
|
||||||
|
assert!(matches!(
|
||||||
|
ObjectHeader::parse(&data, 0, 8, 8),
|
||||||
|
Err(FormatError::NestingDepthExceeded)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_v1_unknown_message_ok() {
|
fn parse_v1_unknown_message_ok() {
|
||||||
let messages = [(0x00FFu16, &[0xAA, 0xBB][..], 0u8)];
|
let messages = [(0x00FFu16, &[0xAA, 0xBB][..], 0u8)];
|
||||||
|
|||||||
Reference in New Issue
Block a user