security(format): add recursion-depth guard to Datatype::parse

Datatype::parse recurses into itself for Compound/Enumeration/
VariableLength/Array/Complex member and base types with no depth
counter. A message data size capped at u16::MAX (65535 bytes) allows
~8000 levels of nesting in a crafted file, enough to blow the stack —
worse on the project's no_std/embedded targets with only a few KB of
stack. Thread a depth counter through a new parse_with_depth, mirroring
object_header.rs's continuation-depth guard, and reject past 64 levels
with FormatError::NestingDepthExceeded. The public Datatype::parse
signature is unchanged.

INT-03
This commit is contained in:
ClawHDF5 Coding Agent
2026-08-17 00:27:16 +00:00
parent 864faf3656
commit 7314971fe7
+54 -7
View File
@@ -204,11 +204,25 @@ fn read_uint(data: &[u8], offset: usize, nbytes: usize) -> Result<u64, FormatErr
}) })
} }
/// Maximum recursion depth for nested datatypes (Compound/Enumeration/
/// VariableLength/Array). A crafted file can nest a message-size-capped
/// (65535 byte) datatype message ~8000 levels deep, which would blow the
/// stack — especially on the project's no_std/embedded targets where
/// available stack is a few KB.
const MAX_DATATYPE_DEPTH: u16 = 64;
impl Datatype { impl Datatype {
/// Parse a datatype message from raw bytes. /// Parse a datatype message from raw bytes.
/// ///
/// Returns `(Datatype, bytes_consumed)` for recursive parsing. /// Returns `(Datatype, bytes_consumed)` for recursive parsing.
pub fn parse(data: &[u8]) -> Result<(Datatype, usize), FormatError> { pub fn parse(data: &[u8]) -> Result<(Datatype, usize), FormatError> {
Self::parse_with_depth(data, 0)
}
fn parse_with_depth(data: &[u8], depth: u16) -> Result<(Datatype, usize), FormatError> {
if depth >= MAX_DATATYPE_DEPTH {
return Err(FormatError::NestingDepthExceeded);
}
// Minimum header: 4 bytes (class_and_version + 3 bytes bit field) + 4 bytes size = 8 // Minimum header: 4 bytes (class_and_version + 3 bytes bit field) + 4 bytes size = 8
ensure_len(data, 0, 8)?; ensure_len(data, 0, 8)?;
@@ -358,7 +372,7 @@ impl Datatype {
pos += name_len; pos += name_len;
let byte_offset = read_uint(data, pos, ob)?; let byte_offset = read_uint(data, pos, ob)?;
pos += ob; pos += ob;
let (member_dt, consumed) = Datatype::parse(&data[pos..])?; let (member_dt, consumed) = Self::parse_with_depth(&data[pos..], depth + 1)?;
pos += consumed; pos += consumed;
members.push(CompoundMember { members.push(CompoundMember {
name, name,
@@ -384,7 +398,7 @@ impl Datatype {
// dimensionality(1) + reserved(3) + dim_perm(4) + 4 dim slots(16) = 24 // dimensionality(1) + reserved(3) + dim_perm(4) + 4 dim slots(16) = 24
ensure_len(data, pos, 24)?; ensure_len(data, pos, 24)?;
pos += 24; pos += 24;
let (member_dt, consumed) = Datatype::parse(&data[pos..])?; let (member_dt, consumed) = Self::parse_with_depth(&data[pos..], depth + 1)?;
pos += consumed; pos += consumed;
members.push(CompoundMember { members.push(CompoundMember {
name, name,
@@ -415,7 +429,7 @@ impl Datatype {
// Enumeration // Enumeration
let num_members = (bf0 as u16) | ((bf1 as u16) << 8); let num_members = (bf0 as u16) | ((bf1 as u16) << 8);
// Parse base type // Parse base type
let (base_type, base_consumed) = Datatype::parse(&data[pos..])?; let (base_type, base_consumed) = Self::parse_with_depth(&data[pos..], depth + 1)?;
pos += base_consumed; pos += base_consumed;
let base_size = base_type.type_size(); let base_size = base_type.type_size();
let mut members = Vec::with_capacity(num_members as usize); let mut members = Vec::with_capacity(num_members as usize);
@@ -468,7 +482,7 @@ impl Datatype {
} else { } else {
None None
}; };
let (base_type, consumed) = Datatype::parse(&data[pos..])?; let (base_type, consumed) = Self::parse_with_depth(&data[pos..], depth + 1)?;
pos += consumed; pos += consumed;
Ok(( Ok((
Datatype::VariableLength { Datatype::VariableLength {
@@ -494,7 +508,7 @@ impl Datatype {
} }
// skip permutation indices // skip permutation indices
pos += ndims * 4; pos += ndims * 4;
let (base_type, consumed) = Datatype::parse(&data[pos..])?; let (base_type, consumed) = Self::parse_with_depth(&data[pos..], depth + 1)?;
pos += consumed; pos += consumed;
Ok(( Ok((
Datatype::Array { Datatype::Array {
@@ -515,7 +529,7 @@ impl Datatype {
dimensions.push(LittleEndian::read_u32(&data[pos..pos + 4])); dimensions.push(LittleEndian::read_u32(&data[pos..pos + 4]));
pos += 4; pos += 4;
} }
let (base_type, consumed) = Datatype::parse(&data[pos..])?; let (base_type, consumed) = Self::parse_with_depth(&data[pos..], depth + 1)?;
pos += consumed; pos += consumed;
Ok(( Ok((
Datatype::Array { Datatype::Array {
@@ -545,7 +559,7 @@ impl Datatype {
pos += name_len; pos += name_len;
let byte_offset = read_uint(data, pos, ob)?; let byte_offset = read_uint(data, pos, ob)?;
pos += ob; pos += ob;
let (member_dt, consumed) = Datatype::parse(&data[pos..])?; let (member_dt, consumed) = Self::parse_with_depth(&data[pos..], depth + 1)?;
pos += consumed; pos += consumed;
members.push(CompoundMember { members.push(CompoundMember {
name, name,
@@ -814,6 +828,39 @@ mod tests {
buf buf
} }
/// A crafted datatype message nesting Variable-Length wrappers deeper
/// than `MAX_DATATYPE_DEPTH` must return `NestingDepthExceeded`
/// instead of overflowing the stack.
#[test]
fn nested_variable_length_exceeds_depth_limit() {
// Each VL level is just an 8-byte header (class 9, vl_type=0 =>
// sequence, no padding/charset fields) immediately followed by the
// next level's bytes, terminated by a fixed-point base type.
let levels = MAX_DATATYPE_DEPTH as usize + 10;
let mut data = Vec::new();
for _ in 0..levels {
data.extend_from_slice(&build_dt_header(9, 3, [0, 0, 0], 0));
}
data.extend_from_slice(&build_fixed_point(4, false, false, 0, 32));
let result = Datatype::parse(&data);
assert!(matches!(result, Err(FormatError::NestingDepthExceeded)));
}
/// A datatype nested just within the depth limit must still parse fine.
#[test]
fn nested_variable_length_within_depth_limit_ok() {
let levels = MAX_DATATYPE_DEPTH as usize - 1;
let mut data = Vec::new();
for _ in 0..levels {
data.extend_from_slice(&build_dt_header(9, 3, [0, 0, 0], 0));
}
data.extend_from_slice(&build_fixed_point(4, false, false, 0, 32));
let result = Datatype::parse(&data);
assert!(result.is_ok());
}
#[test] #[test]
fn test_fixed_point_u8() { fn test_fixed_point_u8() {
let data = build_fixed_point(1, false, false, 0, 8); let data = build_fixed_point(1, false, false, 0, 8);