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
new file mode 100644@@ -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();+ }+}@@ -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) {@@ -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)