diff --git a/mp4parse/src/lib.rs b/mp4parse/src/lib.rs index 33dda4c0..661f9000 100644 --- a/mp4parse/src/lib.rs +++ b/mp4parse/src/lib.rs @@ -267,6 +267,7 @@ pub enum Status { PsshSizeOverflow, ReadBufErr, SchiQuantity, + StblBadEntryCount, StsdBadAudioSampleEntry, StsdBadVideoSampleEntry, TkhdBadVersion, @@ -756,6 +757,9 @@ impl From for &str { Status::SchiQuantity => { "tenc box should be only one at most in sinf box" } + Status::StblBadEntryCount => { + "sample table box declares more entries than its size can hold" + } Status::StsdBadAudioSampleEntry => { "malformed audio sample entry" } @@ -5124,6 +5128,12 @@ fn read_mdhd(src: &mut BMFFBox) -> Result { fn read_stco(src: &mut BMFFBox) -> Result { let (_, _) = read_fullbox_extra(src)?; let offset_count = be_u32(src)?; + if offset_count + .checked_mul(4) + .is_none_or(|bytes| u64::from(bytes) > src.bytes_left()) + { + return Status::StblBadEntryCount.into(); + } let mut offsets = TryVec::with_capacity(offset_count.to_usize())?; for _ in 0..offset_count { offsets.push(be_u32(src)?.into())?; @@ -5140,6 +5150,12 @@ fn read_stco(src: &mut BMFFBox) -> Result { fn read_co64(src: &mut BMFFBox) -> Result { let (_, _) = read_fullbox_extra(src)?; let offset_count = be_u32(src)?; + if offset_count + .checked_mul(8) + .is_none_or(|bytes| u64::from(bytes) > src.bytes_left()) + { + return Status::StblBadEntryCount.into(); + } let mut offsets = TryVec::with_capacity(offset_count.to_usize())?; for _ in 0..offset_count { offsets.push(be_u64(src)?)?; @@ -5156,6 +5172,12 @@ fn read_co64(src: &mut BMFFBox) -> Result { fn read_stss(src: &mut BMFFBox) -> Result { let (_, _) = read_fullbox_extra(src)?; let sample_count = be_u32(src)?; + if sample_count + .checked_mul(4) + .is_none_or(|bytes| u64::from(bytes) > src.bytes_left()) + { + return Status::StblBadEntryCount.into(); + } let mut samples = TryVec::with_capacity(sample_count.to_usize())?; for _ in 0..sample_count { samples.push(be_u32(src)?)?; @@ -5172,6 +5194,12 @@ fn read_stss(src: &mut BMFFBox) -> Result { fn read_stsc(src: &mut BMFFBox) -> Result { let (_, _) = read_fullbox_extra(src)?; let sample_count = be_u32(src)?; + if sample_count + .checked_mul(12) + .is_none_or(|bytes| u64::from(bytes) > src.bytes_left()) + { + return Status::StblBadEntryCount.into(); + } let mut samples = TryVec::with_capacity(sample_count.to_usize())?; for _ in 0..sample_count { let first_chunk = be_u32(src)?; @@ -5246,6 +5274,12 @@ fn read_stsz(src: &mut BMFFBox) -> Result { let sample_count = be_u32(src)?; let mut sample_sizes = TryVec::new(); if sample_size == 0 { + if sample_count + .checked_mul(4) + .is_none_or(|bytes| u64::from(bytes) > src.bytes_left()) + { + return Status::StblBadEntryCount.into(); + } sample_sizes.reserve(sample_count.to_usize())?; for _ in 0..sample_count { sample_sizes.push(be_u32(src)?)?; @@ -5267,6 +5301,12 @@ fn read_stsz(src: &mut BMFFBox) -> Result { fn read_stts(src: &mut BMFFBox) -> Result { let (_, _) = read_fullbox_extra(src)?; let sample_count = be_u32(src)?; + if sample_count + .checked_mul(8) + .is_none_or(|bytes| u64::from(bytes) > src.bytes_left()) + { + return Status::StblBadEntryCount.into(); + } let mut samples = TryVec::with_capacity(sample_count.to_usize())?; for _ in 0..sample_count { let sample_count = be_u32(src)?; diff --git a/mp4parse/src/tests.rs b/mp4parse/src/tests.rs index d9fdcdfc..b012f1d6 100644 --- a/mp4parse/src/tests.rs +++ b/mp4parse/src/tests.rs @@ -1067,6 +1067,42 @@ fn skip_padding_in_boxes() { } } +#[test] +fn reject_oversized_entry_count_in_stbl_boxes() { + // A tiny sample table box that declares an entry count far larger than its + // size can hold must be rejected before the count is used to preallocate, + // matching the guard read_ctts already has. Each box carries only its + // count field (0xffff_ffff) and no entries. + fn assert_bad_entry_count(result: super::Result) { + match result { + Err(Error::InvalidData(s)) => assert_eq!(s, Status::StblBadEntryCount), + other => panic!("expected StblBadEntryCount, got {:?}", other), + } + } + + macro_rules! read_box { + ($name:expr, $count_field:expr, $read:expr) => {{ + let mut stream = make_fullbox(BoxSize::Auto, $name, 1, $count_field); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_bad_entry_count($read(&mut stream)); + }}; + } + + read_box!(b"stco", |s: Section| s.B32(0xffff_ffff), super::read_stco); + read_box!(b"co64", |s: Section| s.B32(0xffff_ffff), super::read_co64); + read_box!(b"stss", |s: Section| s.B32(0xffff_ffff), super::read_stss); + read_box!(b"stsc", |s: Section| s.B32(0xffff_ffff), super::read_stsc); + read_box!(b"stts", |s: Section| s.B32(0xffff_ffff), super::read_stts); + // stsz reads sample_size first; a zero sample_size means per-sample sizes + // follow, so the sample_count is what drives the allocation. + read_box!( + b"stsz", + |s: Section| s.B32(0).B32(0xffff_ffff), + super::read_stsz + ); +} + #[test] fn skip_padding_in_stsd() { // Padding data could be added in the end of stsd boxes. Parser needs to skip