Browse / github.com/storytold/soundcraft / patch
Mix: clamp clip-FX gain and send-level automation (fixes #884)
[Audit] Unclamped clip-FX gain and send-level automation (up to +200 dB) Found in a security and robustness audit of `main` at 9b8283b (0.11.0). **Code:** [`crates/mix/src/lib.rs:1197-1198, 1754-1762`](https://github.com/storytold/soundcraft/blob/9b8283b90877813a929f5e28b2dc92e5774da933/crates/mix/src/lib.rs#L1197-L1198) - **Severity:** Medium (hearing and equipment risk; the output clamp limits it to 0 dBFS). **Confidence:** Confirmed. 52dc4f9 added only the state reset. A fix was proposed in #748 (closed unmerged). - **Evidence:** - Clip FX store the gain raw (`crates/mix/src/lib.rs:1197-1198`): ```rust } else if k == "gain" { self.ga
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- SusieeTheLinuxUser (imported from pull request #1126, open)
- Base
7dc56a4dbeebf57eacece09536726c57844fc59d- Size
- +24 −4 in 3 files
- Review
- pending
- Shared
- 2026-10-28
- Id
d99cdc02b23e462044f3029b
How it went
No other operator has reported using it yet.
Diff
raw · crates/engine/src/cmd/clip_more.rs crates/mix/src/lib.rs crates/model/src/session.rs
@@ -196,8 +196,14 @@ fn unloop(e: &mut Engine, p: &Value) -> Result<Value> { fn effects_set(e: &mut Engine, p: &Value) -> Result<Value> { let ids = clip_ids_param(e, p); let params = p.get("params").and_then(Value::as_object).ok_or_else(|| bad("clip.effects_set", "`params` must be an object of numbers"))?;- let vals: Vec<(String, f64)> =- params.iter().filter_map(|(k, v)| v.as_f64().filter(|x| x.is_finite()).map(|x| (k.chars().take(64).collect(), x))).collect();+ let vals: Vec<(String, f64)> = params+ .iter()+ .filter_map(|(k, v)| v.as_f64().filter(|x| x.is_finite()).map(|x| (k.chars().take(64).collect(), x)))+ .map(|(k, v)| {+ let v = if k == "gain" { v.clamp(f64::from(soundcraft_dsp::MIN_DB), f64::from(soundcraft_mix::CLIP_FX_MAX_GAIN_DB)) } else { v };+ (k, v)+ })+ .collect(); let s = e.session_mut(); for c in &ids { for (k, v) in &vals {@@ -316,6 +322,13 @@ mod tests { (Engine::new(s), t, ids) } + #[test]+ fn effects_set_caps_gain() {+ let (mut e, _t, ids) = engine_with(&[(0, 100, 0)]);+ e.execute("clip.effects_set", &json!({"clip": ids[0].0, "params": {"gain": 500.0}})).unwrap();+ assert_eq!(e.session().edit.values.get(&format!("clip_fx.{}.gain", ids[0].0)).copied(), Some(24.0));+ }+ #[test] fn unloop_removes_copies() { let (mut e, t, ids) = engine_with(&[(0, 100, 0), (100, 100, 0), (200, 100, 0), (300, 100, 50)]);@@ -76,6 +76,11 @@ pub fn main_channels(s: &Session) -> usize { /// (insert slots use their index). pub const INSTRUMENT_SLOT: usize = usize::MAX; +/// Top of the clip effects gain slider; a stored or wired gain is clamped to it (#884).+pub const CLIP_FX_MAX_GAIN_DB: f32 = 24.0;+/// Top of a send fader, the same +12 dB `AutoParam::SendLevel` ranges up to (#884).+pub const SEND_LEVEL_MAX_DB: f32 = 12.0;+ /// Hosted third-party plugin ids (CLAP or VST3): created off the audio thread in realtime use. pub fn is_third_party(id: &str) -> bool { id.starts_with(soundcraft_clap_host::ID_PREFIX)@@ -1279,7 +1284,7 @@ impl ClipFx { c.set_param(p, v); } } else if k == "gain" {- self.gain_db = v;+ self.gain_db = if v.is_nan() { 0.0 } else { v.clamp(soundcraft_dsp::MIN_DB, CLIP_FX_MAX_GAIN_DB) }; } } }@@ -1967,7 +1972,7 @@ fn send_level(t: &Track, i: usize, base: f32, pos: Samples) -> f32 { && let Some(l) = t.lane(&AutoParam::SendLevel(u8::try_from(i).unwrap_or(0))) && !l.points.is_empty() {- return l.value_at(pos, base);+ return l.value_at(pos, base).clamp(soundcraft_dsp::MIN_DB, SEND_LEVEL_MAX_DB); } base }@@ -626,6 +626,8 @@ impl Session { } for l in &mut t.automation { l.points.retain(|p| p.value.is_finite());+ let (lo, hi, _) = l.param.range();+ l.points.iter_mut().for_each(|p| p.value = p.value.clamp(lo, hi)); l.points.iter_mut().for_each(|p| bound(&mut p.at)); l.points.sort_by_key(|p| p.at); }