From 552f008d2757e307194256b3e672c9c1de4fa9b2 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 24 Oct 2026 05:28:22 -0400 Subject: [PATCH] security(audio): bound sequence sample rates and AAF/OMF export channel buffers (#3082) * security(audio): bound sequence sample rates and AAF/OMF export channel buffers * fix(engine): bound export audio channels and samples independently --- crates/audio-dsp/src/resample.rs | 5 ++++- crates/engine/src/aaf_omf.rs | 10 +++++++++- crates/interchange/src/fcpxml.rs | 5 +++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/crates/audio-dsp/src/resample.rs b/crates/audio-dsp/src/resample.rs index 01dfee55d..b1686583e 100644 --- a/crates/audio-dsp/src/resample.rs +++ b/crates/audio-dsp/src/resample.rs @@ -20,6 +20,9 @@ pub struct StreamResampler { next_out: Option, } +/// Maximum source frames fetched in a single resampler pull. +pub const MAX_RESAMPLE_SOURCE_SPAN: usize = 384_000 * 10; + impl StreamResampler { /// Rates of zero are treated as 1 Hz rather than dividing by zero. pub fn new(from: u32, to: u32) -> Self { @@ -53,7 +56,7 @@ impl StreamResampler { } let held_end = self.held_start + self.held.first().map_or(0, Vec::len) as i64; if last + 1 > held_end { - let want = (last + 1 - held_end) as usize; + let want = ((last + 1 - held_end) as usize).min(MAX_RESAMPLE_SOURCE_SPAN); let got = pull(held_end, want); if self.held.is_empty() { self.held = vec![Vec::new(); got.len().max(1)]; diff --git a/crates/engine/src/aaf_omf.rs b/crates/engine/src/aaf_omf.rs index 823b895dc..6aeafdcd2 100644 --- a/crates/engine/src/aaf_omf.rs +++ b/crates/engine/src/aaf_omf.rs @@ -130,6 +130,9 @@ fn dequantize(pcm: &[u8], channels: usize, bits: u16) -> Vec> { out } +const MAX_EXPORT_AUDIO_CHANNELS: usize = 64; +const MAX_EXPORT_AUDIO_SAMPLES: usize = 64 * 1024 * 1024; + /// Decode a media range at `sample_rate` (planar). fn decode(s: &Session, need: &AudioNeed, sample_rate: u32) -> Result>> { let src = s @@ -138,7 +141,12 @@ fn decode(s: &Session, need: &AudioNeed, sample_rate: u32) -> Result> = vec![Vec::with_capacity(frames); need.channels.max(1) as usize]; + let ch = (need.channels.max(1) as usize).min(MAX_EXPORT_AUDIO_CHANNELS); + let total_samples = ch.checked_mul(frames).ok_or_else(|| EngineError::Other("audio export buffer size overflow".into()))?; + if total_samples > MAX_EXPORT_AUDIO_SAMPLES { + return Err(EngineError::Other(format!("audio export buffer size {total_samples} samples exceeds maximum allowed ({MAX_EXPORT_AUDIO_SAMPLES})"))); + } + let mut out: Vec> = vec![Vec::with_capacity(frames); ch]; let chunk = sample_rate as usize * 10; let mut done = 0usize; while done < frames { diff --git a/crates/interchange/src/fcpxml.rs b/crates/interchange/src/fcpxml.rs index c75bdd651..c50186081 100644 --- a/crates/interchange/src/fcpxml.rs +++ b/crates/interchange/src/fcpxml.rs @@ -105,10 +105,11 @@ fn audio_rate_str(sr: u32) -> String { fn parse_audio_rate(s: &str) -> Option { let s = s.trim(); - match s.strip_suffix('k') { + let r = match s.strip_suffix('k') { Some(k) => k.parse::().ok().map(|v| (v * 1000.0).round() as u32), None => s.parse().ok(), - } + }; + r.filter(|&sr| sr > 0 && sr <= 384_000) } // ---------------------------------------------------------------------------------------------