fix(codecs): harden media decoders against resource amplification and unbound state (#1285)

* fix(codecs): harden media decoders against resource amplification and unbound state

- Ogg recovery (Finding 29): Reuse capture search window buffer and bound
  cumulative recovery work to prevent quadratic re-scanning.
- Opus packet concealment (Finding 40): Skip empty stored Opus packets
  instead of triggering packet-loss concealment audio.
- Opus decoded PCM budget (Finding 63): Enforce aggregate MAX_PCM_SAMPLES
  limit before expanding planar audio samples.
- H.264 DPB reference eviction (Finding 56): Validate MMCO 6 against
  max_long_term_frame_idx and evict oldest entries when capacity is reached.
- HEVC missing reference frames (Finding 57): Share gray replacement frames
  and bound missing references to DPB buffering limits.
- AV1 show_existing_frame budget (Finding 69): Cap frame outputs per chunk
  to 32 to prevent output batch memory explosion.
- AV1 nonuniform tile bounds (Finding 89): Enforce MAX_TILE_COLS and
  MAX_TILE_ROWS limits on nonuniform tile layouts.

* fix(codecs): refine show_existing_frame limit, PCM capacity check, and buffered Ogg search
This commit is contained in:
Dylan M. Taylor
2026-10-25 15:59:10 -04:00
committed by GitHub
parent b58df1095d
commit d4945d0443
6 changed files with 95 additions and 21 deletions
+3
View File
@@ -466,6 +466,9 @@ impl Decoder {
let mut r = BitReader::new(payload);
let shown = self.frame_header(&mut r, temporal_id, spatial_id)?;
if let Some(o) = shown {
if out.iter().filter(|(o, _, _)| matches!(o, Output::Existing { .. })).count() >= 32 {
return Err(Error::Invalid("too many frame outputs in chunk"));
}
out.push((o, tu, spatial_id));
continue;
}
+6
View File
@@ -906,6 +906,9 @@ impl FrameHeader {
let mut widest = 0;
let mut start = 0;
while start < sb_cols {
if t.mi_col_starts.len() >= MAX_TILE_COLS {
return Err(Error::Invalid("tile_cols exceeds MAX_TILE_COLS"));
}
t.mi_col_starts.push(start << sb_shift);
let max_width = (sb_cols - start).min(max_tile_width_sb);
let size = r.ns(max_width)? + 1;
@@ -923,6 +926,9 @@ impl FrameHeader {
let max_tile_height_sb = (max_tile_area_sb / widest.max(1)).max(1);
let mut start = 0;
while start < sb_rows {
if t.mi_row_starts.len() >= MAX_TILE_ROWS {
return Err(Error::Invalid("tile_rows exceeds MAX_TILE_ROWS"));
}
t.mi_row_starts.push(start << sb_shift);
let max_height = (sb_rows - start).min(max_tile_height_sb);
let size = r.ns(max_height)? + 1;
+15 -4
View File
@@ -327,12 +327,18 @@ impl Dpb {
had5 = true;
}
6 => {
for e in &mut self.entries {
if e.mark == RefMark::Long && e.long_term_frame_idx == m.long_term_frame_idx {
e.mark = RefMark::Unused;
let allowed = match self.max_long_term_frame_idx {
Some(max) => m.long_term_frame_idx <= max,
None => false,
};
if allowed {
for e in &mut self.entries {
if e.mark == RefMark::Long && e.long_term_frame_idx == m.long_term_frame_idx {
e.mark = RefMark::Unused;
}
}
*cur_long = Some(m.long_term_frame_idx);
}
*cur_long = Some(m.long_term_frame_idx);
}
_ => {}
}
@@ -410,6 +416,11 @@ impl Dpb {
} else {
while self.entries.len() >= self.capacity {
if !self.bump(out) {
if let Some(pos) = self.entries.iter().position(|e| !e.needed_for_output) {
self.entries.remove(pos);
} else {
self.entries.remove(0);
}
break;
}
}
+12 -1
View File
@@ -440,11 +440,22 @@ impl Decoder {
let template = Frame::new(0, 0, w, h, sps.log2_ctb, sps.bit_depth_luma, sps.bit_depth_chroma);
let next_id = &mut self.next_id;
let mut missing = 0u64;
let max_missing = (sps.max_dec_pic_buffering as u64).clamp(1, 16);
let mut shared_gray: Option<FrameRef> = None;
let mut make_missing = |p: i32| -> FrameRef {
missing += 1;
if missing > max_missing
&& let Some(ref g) = shared_gray
{
return g.clone();
}
let id = *next_id;
*next_id += 1;
Arc::new(Frame::gray(id, p, &template))
let f = Arc::new(Frame::gray(id, p, &template));
if shared_gray.is_none() {
shared_gray = Some(f.clone());
}
f
};
let rps = self.dpb.apply_rps(sh, poc, max_lsb, irap_no_rasl, &mut make_missing)?;
if missing > 0 {
+18 -2
View File
@@ -24,6 +24,8 @@ pub struct Pcm {
pub samples: Vec<f32>,
}
const MAX_PCM_SAMPLES: usize = 64 * 1024 * 1024;
impl Pcm {
/// Sample frames (samples per channel).
pub fn frames(&self) -> usize {
@@ -47,8 +49,10 @@ impl Pcm {
fn push_planar(planes: &[Vec<f32>], out: &mut Vec<f32>) {
let n = planes.iter().map(Vec::len).min().unwrap_or(0);
out.reserve(n * planes.len());
for i in 0..n {
let remaining = MAX_PCM_SAMPLES.saturating_sub(out.len());
let to_push = n.min(remaining / planes.len().max(1));
out.reserve(to_push * planes.len());
for i in 0..to_push {
for p in planes {
out.push(p[i]);
}
@@ -96,7 +100,13 @@ fn decode_ogg_opus(bytes: &Bytes) -> Option<Result<Pcm>> {
let timing = deckcraft_ogg::OpusTiming::of(stream, dec.pre_skip() as u32).map_err(|e| MediaError::Corrupt(e.to_string()))?;
let mut out = Vec::new();
for i in 0..stream.packets.len() {
if MAX_PCM_SAMPLES.saturating_sub(out.len()) < channels.max(1) as usize {
break;
}
let Ok(p) = file.read_packet(src, s, i) else { continue };
if p.is_empty() {
continue;
}
match dec.decode(Some(&p)) {
Ok(planes) => Pcm::push_planar(&planes, &mut out),
Err(e) => log::debug!("opus packet {i}: {e}"),
@@ -126,7 +136,13 @@ fn decode_mkv_opus(bytes: &Bytes) -> Option<Result<Pcm>> {
let mut out = Vec::new();
let n = file.tracks.get(ti).map(|t| t.samples.len()).unwrap_or(0);
for i in 0..n {
if MAX_PCM_SAMPLES.saturating_sub(out.len()) < channels.max(1) as usize {
break;
}
let Ok(p) = file.read_sample(src, ti, i) else { continue };
if p.is_empty() {
continue;
}
match dec.decode(Some(&p)) {
Ok(planes) => Pcm::push_planar(&planes, &mut out),
Err(e) => log::debug!("opus packet {i}: {e}"),
+41 -14
View File
@@ -212,10 +212,11 @@ pub fn open(src: &(impl ByteSource + ?Sized)) -> Result<OggFile> {
let mut pos = 0u64;
let mut found_any = false;
let mut hdr = [0u8; 27 + 255];
let mut scanner = CaptureScanner::new();
while pos + 27 <= len {
src.read_at(pos, &mut hdr[..27])?;
if &hdr[..4] != b"OggS" || hdr[4] != 0 {
match find_capture(src, pos + 1, len)? {
match scanner.find(src, pos + 1, len)? {
Some(p) => {
if found_any {
warnings.push(format!("lost sync at byte {pos}; resumed at {p}"));
@@ -336,21 +337,47 @@ pub fn open(src: &(impl ByteSource + ?Sized)) -> Result<OggFile> {
Ok(OggFile { streams, warnings })
}
fn find_capture(src: &(impl ByteSource + ?Sized), from: u64, len: u64) -> Result<Option<u64>> {
let mut at = from;
while at + 4 <= len {
let n = ((len - at) as usize).min(1 << 20);
let mut buf = vec![0u8; n];
src.read_at(at, &mut buf)?;
if let Some(i) = buf.windows(4).position(|w| w == b"OggS") {
return Ok(Some(at + i as u64));
struct CaptureScanner {
buf: Vec<u8>,
buf_start: u64,
budget: usize,
}
impl CaptureScanner {
fn new() -> Self {
Self { buf: Vec::new(), buf_start: 0, budget: 32 * 1024 * 1024 }
}
fn find(&mut self, src: &(impl ByteSource + ?Sized), from: u64, len: u64) -> Result<Option<u64>> {
let mut at = from;
while at + 4 <= len {
let buf_end = self.buf_start + self.buf.len() as u64;
if at >= self.buf_start && at + 4 <= buf_end {
let off = (at - self.buf_start) as usize;
if let Some(i) = self.buf[off..].windows(4).position(|w| w == b"OggS") {
return Ok(Some(at + i as u64));
}
at = buf_end.saturating_sub(3);
continue;
}
if self.budget == 0 {
return Ok(None);
}
let n = ((len - at) as usize).min(64 * 1024).min(self.budget);
if n < 4 {
break;
}
self.buf.resize(n, 0);
src.read_at(at, &mut self.buf)?;
self.buf_start = at;
self.budget = self.budget.saturating_sub(n);
if let Some(i) = self.buf.windows(4).position(|w| w == b"OggS") {
return Ok(Some(at + i as u64));
}
at += (n - 3) as u64;
}
if n < 4 {
break;
}
at += (n - 3) as u64;
Ok(None)
}
Ok(None)
}
fn read_parts(src: &(impl ByteSource + ?Sized), parts: &[(u64, u32)]) -> Result<Vec<u8>> {