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:
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user