From 8b929f4565e8f879bb10f724bc4e63807089f364 Mon Sep 17 00:00:00 2001 From: rhoopr <> Date: Thu, 30 Apr 2026 10:24:19 -0400 Subject: [PATCH 1/2] saiz/saio/hvcc: bound Vec::with_capacity on attacker-controlled counts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three more `Vec::with_capacity` sites pass a count read straight from input as the capacity argument, with no upper bound: - `saiz` `sample_count` (`Vec`, `u32`): worst case ~4 GiB upfront. - `saio` `entry_count` (`Vec`, `u32`): worst case ~34 GiB upfront for v1 (8 bytes/entry). - `hvcc` `num_nalus` (`Vec>`, `u16`): worst case ~1.5 MiB upfront (24 bytes per `Vec` slot). Same defensive idiom as #157: pre-flight reject counts that cannot possibly fit in the remaining buffer (`count > buf.remaining() / N`, where N is the per-element minimum), then cap the upfront reservation. - saiz/saio: cap at 4096 to match `trun.rs` — covers typical CMAF segments (hundreds to low thousands of samples) without reallocating. - hvcc: cap at 64 — real HEVC arrays hold a handful of VPS/SPS/PPS/SEI entries, so 4096 would be well past anything realistic. Adds regression tests pinned to `Error::OverDecode` (the wrapper that `Atom::decode_maybe` produces from `Error::OutOfBounds`). Closes #156. --- src/moov/trak/mdia/minf/stbl/saiz.rs | 41 ++++++++++++++++++- .../trak/mdia/minf/stbl/stsd/hevc/hvcc.rs | 39 +++++++++++++++++- 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/moov/trak/mdia/minf/stbl/saiz.rs b/src/moov/trak/mdia/minf/stbl/saiz.rs index 04d2476b..f1a550fe 100644 --- a/src/moov/trak/mdia/minf/stbl/saiz.rs +++ b/src/moov/trak/mdia/minf/stbl/saiz.rs @@ -51,7 +51,12 @@ impl AtomExt for Saiz { let default_sample_info_size = u8::decode(buf)?; let sample_count = u32::decode(buf)?; if default_sample_info_size == 0 { - let mut sample_info_size = Vec::with_capacity(sample_count as usize); + // Each entry is a single byte; reject counts that cannot + // possibly fit in the remaining buffer before allocating. + if sample_count as usize > buf.remaining() { + return Err(Error::OutOfBounds); + } + let mut sample_info_size = Vec::with_capacity((sample_count as usize).min(4096)); for _ in 0..sample_count { sample_info_size.push(u8::decode(buf)?); } @@ -115,7 +120,13 @@ impl AtomExt for Saio { }); } let entry_count = u32::decode(buf)?; - let mut offsets = Vec::with_capacity(entry_count as usize); + // Entries are 4 bytes (v0) or 8 bytes (v1); reject counts that + // cannot possibly fit in the remaining buffer before allocating. + let per_entry = if ext.version == SaioVersion::V1 { 8 } else { 4 }; + if entry_count as usize > buf.remaining() / per_entry { + return Err(Error::OutOfBounds); + } + let mut offsets = Vec::with_capacity((entry_count as usize).min(4096)); for _ in 0..entry_count { if ext.version == SaioVersion::V0 { let offset = u32::decode(buf)? as u64; @@ -427,6 +438,32 @@ mod tests { ); } + // Regression for issue #156: a u32::MAX sample_count must fail + // cleanly without attempting a ~4 GiB upfront allocation. + const ENCODED_SAIZ_HUGE_COUNT: &[u8] = &[ + 0x00, 0x00, 0x00, 0x11, 0x73, 0x61, 0x69, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, + 0xFF, 0xFF, + ]; + + #[test] + fn test_saiz_huge_count() { + let buf: &mut std::io::Cursor<&&[u8]> = &mut std::io::Cursor::new(&ENCODED_SAIZ_HUGE_COUNT); + assert!(matches!(Saiz::decode(buf), Err(Error::OverDecode(_)))); + } + + // Regression for issue #156: a u32::MAX entry_count must fail + // cleanly without attempting a multi-GiB upfront allocation. + const ENCODED_SAIO_HUGE_COUNT: &[u8] = &[ + 0x00, 0x00, 0x00, 0x10, 0x73, 0x61, 0x69, 0x6f, 0x01, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, + 0xFF, + ]; + + #[test] + fn test_saio_huge_count() { + let buf: &mut std::io::Cursor<&&[u8]> = &mut std::io::Cursor::new(&ENCODED_SAIO_HUGE_COUNT); + assert!(matches!(Saio::decode(buf), Err(Error::OverDecode(_)))); + } + #[test] fn test_saio_encode_cenc() { let saio = Saio { diff --git a/src/moov/trak/mdia/minf/stbl/stsd/hevc/hvcc.rs b/src/moov/trak/mdia/minf/stbl/stsd/hevc/hvcc.rs index e45eb70e..0f1da8e9 100644 --- a/src/moov/trak/mdia/minf/stbl/stsd/hevc/hvcc.rs +++ b/src/moov/trak/mdia/minf/stbl/stsd/hevc/hvcc.rs @@ -72,7 +72,14 @@ impl Atom for Hvcc { for _ in 0..num_of_arrays { let params = u8::decode(buf)?; let num_nalus = u16::decode(buf)?; - let mut nalus = Vec::with_capacity(num_nalus as usize); + // Each NALU has at least a u16 length prefix (2 bytes); reject + // counts that cannot possibly fit in the remaining buffer before + // allocating. Real HEVC arrays hold a handful of VPS/SPS/PPS/SEI + // entries, so cap the upfront reservation at 64. + if num_nalus as usize > buf.remaining() / 2 { + return Err(Error::OutOfBounds); + } + let mut nalus = Vec::with_capacity((num_nalus as usize).min(64)); for _ in 0..num_nalus { let size = u16::decode(buf)? as usize; @@ -420,6 +427,36 @@ mod tests { assert_eq!(decoded, hvcc); } + // Regression for issue #156: a u16::MAX num_nalus must fail cleanly + // without attempting a ~1.5 MiB upfront allocation for empty input. + const ENCODED_HVCC_HUGE_NALU_COUNT: &[u8] = &[ + 0x00, 0x00, 0x00, 0x22, // size = 34 + 0x68, 0x76, 0x63, 0x43, // "hvcC" + // 22-byte configuration record up through length_size_minus_one: + 0x01, // configuration_version + 0x00, // profile_space|tier|profile_idc + 0x00, 0x00, 0x00, 0x00, // general_profile_compatibility_flags + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // general_constraint_indicator_flags + 0x00, // general_level_idc + 0x00, 0x00, // min_spatial_segmentation_idc + 0x00, // parallelism_type + 0x00, // chroma_format_idc + 0x00, // bit_depth_luma_minus8 + 0x00, // bit_depth_chroma_minus8 + 0x00, 0x00, // avg_frame_rate + 0x00, // constant_frame_rate|num_temporal_layers|nested|length_size + 0x01, // num_of_arrays = 1 + 0x00, // first array params byte + 0xFF, 0xFF, // num_nalus = u16::MAX + ]; + + #[test] + fn test_hvcc_huge_nalu_count() { + let buf: &mut std::io::Cursor<&&[u8]> = + &mut std::io::Cursor::new(&ENCODED_HVCC_HUGE_NALU_COUNT); + assert!(matches!(Hvcc::decode(buf), Err(Error::OverDecode(_)))); + } + #[test] fn test_hvcc_mpeg_encode() { let hvcc = Hvcc { From cfd3f5c1583f0727377a13a71660a257b6b26990 Mon Sep 17 00:00:00 2001 From: rhoopr <> Date: Thu, 30 Apr 2026 10:45:10 -0400 Subject: [PATCH 2/2] saiz/hvcc: add docstrings on AuxInfo, Hvcc, HvcCArray, Hvcc::new These public items in the touched files lacked docstrings, dragging the diff's docstring coverage below the 80% threshold the review bot checks against. Add minimal spec references matching the existing docstring style on `Saiz` / `Saio`. --- src/moov/trak/mdia/minf/stbl/saiz.rs | 2 ++ src/moov/trak/mdia/minf/stbl/stsd/hevc/hvcc.rs | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/src/moov/trak/mdia/minf/stbl/saiz.rs b/src/moov/trak/mdia/minf/stbl/saiz.rs index f1a550fe..7121e0c5 100644 --- a/src/moov/trak/mdia/minf/stbl/saiz.rs +++ b/src/moov/trak/mdia/minf/stbl/saiz.rs @@ -1,5 +1,7 @@ use crate::*; +/// Auxiliary information type and parameter shared by `saiz` and `saio`, +/// ISO/IEC 14496-12:2022 Sect 8.7.8 / 8.7.9. #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct AuxInfo { diff --git a/src/moov/trak/mdia/minf/stbl/stsd/hevc/hvcc.rs b/src/moov/trak/mdia/minf/stbl/stsd/hevc/hvcc.rs index 0f1da8e9..be43ad93 100644 --- a/src/moov/trak/mdia/minf/stbl/stsd/hevc/hvcc.rs +++ b/src/moov/trak/mdia/minf/stbl/stsd/hevc/hvcc.rs @@ -1,5 +1,7 @@ use crate::*; +/// HEVCConfigurationBox / `HEVCDecoderConfigurationRecord`, +/// ISO/IEC 14496-15:2022 Sect 8.3.3. #[derive(Default, Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Hvcc { @@ -24,6 +26,9 @@ pub struct Hvcc { } impl Hvcc { + /// Returns an `Hvcc` with `configuration_version` set to 1 and all + /// other fields zeroed, matching `HEVCDecoderConfigurationRecord`'s + /// only currently-defined version. pub fn new() -> Self { Self { configuration_version: 1, @@ -32,6 +37,8 @@ impl Hvcc { } } +/// One entry of `HEVCDecoderConfigurationRecord.arrays`: a group of NAL +/// units of a single `nal_unit_type` (typically VPS, SPS, PPS, or SEI). #[derive(Debug, Clone, PartialEq, Eq, Default)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct HvcCArray {