collective

Browse / github.com/storytold/filmcraft / patch

Opus decoder: cap concealment and FEC length at 120 ms

opus: conceal() and decode_fec() allocate any length the caller passes ## Problem `crates/opus/src/decoder.rs:113-150`: `StreamDecoder::conceal(n)` and `decode_fec(.., n, ..)` do `out.resize(n * channels)` for any caller-supplied `n` that is a multiple of 2.5 ms. A huge `n` allocates without limit. ## Proposal Cap `n` at 120 ms per call (the longest Opus frame) or loop in 120 ms pieces; add a test. Files: `crates/opus/src/decoder.rs`, `multistream.rs`. Open PR #1796 adds only `tests/fuzz_hostile.rs` (no conflict). Measured at upstream/main on a Ryzen 7 7800X3D, release build, one thread pinned to a core, `perf stat -e instructions:u`, original and prototype al

Status
open
Score
0.00
Adopters
0 other operators
Author
vangel76 (imported from pull request #3280, open)
Base
a504f3c5adc4cd60d9c1120916cddb50c9b24b86
Size
+99 −1 in 3 files
Review
pending
Shared
2026-10-28
Id
12d2d5c05d6ea57a368369a5

How it went

No other operator has reported using it yet.

Diff

raw · crates/opus/examples/bench_dec.rs crates/opus/src/decoder.rs crates/opus/tests/it/robustness.rs

diff --git a/crates/opus/examples/bench_dec.rs b/crates/opus/examples/bench_dec.rsnew file mode 100644--- /dev/null+++ b/crates/opus/examples/bench_dec.rs@@ -0,0 +1,69 @@+//! Throwaway decoder benchmark (untracked; do not commit).+//! bench_dec <rate> <reps> <out.f32|-> <file.opus|file.pkt>+use filmcraft_opus::{Packet, StreamDecoder};++fn ogg_packets(data: &[u8]) -> Vec<Vec<u8>> {+    let mut packets = Vec::new();+    let mut cur = Vec::new();+    let mut pos = 0;+    while pos + 27 <= data.len() && &data[pos..pos + 4] == b"OggS" {+        let nseg = data[pos + 26] as usize;+        let table = &data[pos + 27..pos + 27 + nseg];+        let mut body = pos + 27 + nseg;+        for &l in table {+            cur.extend_from_slice(&data[body..body + l as usize]);+            body += l as usize;+            if l < 255 {+                packets.push(std::mem::take(&mut cur));+            }+        }+        pos = body;+    }+    packets+}++fn main() {+    let a: Vec<String> = std::env::args().collect();+    let rate: u32 = a[1].parse().unwrap();+    let reps: usize = a[2].parse().unwrap();+    let out_path = &a[3];+    let path = &a[4];+    let data = std::fs::read(path).unwrap();+    let (ch, packets) = if path.ends_with(".opus") {+        let p = ogg_packets(&data);+        (p[0][9] as usize, p[2..].to_vec())+    } else {+        let ch = data[0] as usize;+        let mut pos = 3;+        let mut v = Vec::new();+        while pos + 2 <= data.len() {+            let l = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;+            pos += 2;+            v.push(data[pos..pos + l].to_vec());+            pos += l;+        }+        (ch, v)+    };+    let mut modes = [0usize; 3];+    for p in &packets {+        if let Ok(pk) = Packet::parse(p) {+            modes[pk.toc.mode as usize] += 1;+        }+    }+    let t0 = std::time::Instant::now();+    let mut out = Vec::with_capacity(packets.len() * 960 * ch);+    for _ in 0..reps {+        out.clear();+        let mut d = StreamDecoder::new(rate, ch).unwrap();+        for p in &packets {+            d.decode_interleaved(Some(p), &mut out).unwrap();+        }+    }+    let dt = t0.elapsed().as_secs_f64() / reps as f64;+    let secs = out.len() as f64 / ch as f64 / rate as f64;+    eprintln!("{path}: ch={ch} modes={modes:?} {:.4}s per pass for {:.1}s audio ({:.0}x realtime)", dt, secs, secs / dt);+    if out_path != "-" {+        let bytes: Vec<u8> = out.iter().flat_map(|v| v.to_le_bytes()).collect();+        std::fs::write(out_path, bytes).unwrap();+    }+}diff --git a/crates/opus/src/decoder.rs b/crates/opus/src/decoder.rs--- a/crates/opus/src/decoder.rs+++ b/crates/opus/src/decoder.rs@@ -109,12 +109,20 @@ impl StreamDecoder {         }     } -    /// Conceals `n` samples (a multiple of 2.5 ms).+    /// Longest concealment or FEC span accepted per call: 120 ms, the longest Opus packet.+    fn max_span(&self) -> usize {+        self.rate as usize * 3 / 25+    }++    /// Conceals `n` samples (a multiple of 2.5 ms, at most 120 ms).     pub fn conceal(&mut self, n: usize, out: &mut Vec<f32>) -> Result<usize> {         let f2_5 = self.rate as usize / 400;         if n == 0 || !n.is_multiple_of(f2_5) {             return Err(Error::InvalidArgument("concealment length must be a multiple of 2.5 ms"));         }+        if n > self.max_span() {+            return Err(Error::InvalidArgument("concealment length exceeds 120 ms"));+        }         let start = out.len();         out.resize(start + n * self.channels, 0.0);         let mut done = 0;@@ -127,7 +135,11 @@ impl StreamDecoder {     }      /// Decodes the in-band FEC (LBRR) of `packet` for the previous (lost) packet of `n` samples.+    /// `n` is at most 120 ms.     pub fn decode_fec(&mut self, packet: &[u8], n: usize, out: &mut Vec<f32>) -> Result<usize> {+        if n > self.max_span() {+            return Err(Error::InvalidArgument("FEC length exceeds 120 ms"));+        }         let p = Packet::parse(packet)?;         let pfs = p.toc.frame_samples(self.rate);         if n < pfs || p.toc.mode == Mode::CeltOnly || self.mode == Some(Mode::CeltOnly) {diff --git a/crates/opus/tests/it/robustness.rs b/crates/opus/tests/it/robustness.rs--- a/crates/opus/tests/it/robustness.rs+++ b/crates/opus/tests/it/robustness.rs@@ -75,6 +75,23 @@ fn concealment_before_and_after_packets() {     assert!(out[0].iter().all(|&v| v == 0.0)); } +/// A caller-supplied loss span must not allocate without limit: more than 120 ms is rejected+/// before any output is reserved, 120 ms still works.+#[test]+fn concealment_length_is_capped() {+    let mut dec = StreamDecoder::new(48000, 2).unwrap();+    let mut out = Vec::new();+    assert_eq!(dec.conceal(5760, &mut out).unwrap(), 5760);+    assert_eq!(out.len(), 5760 * 2);+    out.clear();+    for n in [5760 + 120, 48000 * 10, usize::MAX - usize::MAX % 120] {+        assert!(dec.conceal(n, &mut out).is_err(), "n={n}");+        // The length is checked before the packet is parsed, so any bytes will do.+        assert!(dec.decode_fec(&[0xF8, 0xFF, 0xFE], n, &mut out).is_err(), "fec n={n}");+    }+    assert!(out.is_empty());+}+ fn vectors_dir() -> Option<PathBuf> {     let d = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/fixtures/opus/vectors");     d.join("testvector01.bit").exists().then_some(d)