Panic on valid SILK 40/60 ms frames: SILK workspace hardcoded for 20 ms (src/lib.rs:978)
Summary
OpusDecoder::decode panics with range end index 960 out of range for slice of length 640 at src/lib.rs:978 when given a valid, spec-compliant SILK packet whose frame duration is 60 ms (and for 40 ms stereo). RFC 6716 defines 10/20/40/60 ms SILK frames, and libopus's encoder produces them with -frame_duration 40|60. The decoder's SILK scratch buffer w_pcm_i16 is hardcoded to 640 i16 samples — enough only for ≤ 20 ms at 16 kHz stereo (or 40 ms mono WB).
Environment
- opus-rs 0.1.23 (crates.io tarball, sha256
1cc978697f3ada0ad98b323b9d85e85c6b08d478830148636c19b6d5f2ac9b85, verified against the sparse index; used as a path dep with identical content)
- rustc 1.96.0, macOS aarch64 (Apple Silicon), release build
- ffmpeg 8.1.1 with libopus 1.6.1 (to produce the triggering input)
Minimal reproduction
Generate a real 60 ms SILK stream and extract its first audio packet (TOC 0x58) with any Ogg demuxer:
ffmpeg -y -i any_speech.wav -c:a libopus -ar 16000 -ac 1 -b:a 24k -frame_duration 60 silk60.ogg
# first audio packet has TOC 0x58 (SILK, wideband, 60 ms, code 0)
use opus_rs::OpusDecoder;
fn main() {
let pkt = std::fs::read(std::env::args().nth(1).unwrap()).unwrap(); // repro02_silk60_packet.bin
let mut dec = OpusDecoder::new(48000, 1).unwrap();
let mut pcm = vec![0.0f32; 2880]; // 60 ms @ 48 kHz
let n = dec.decode(&pkt, 2880, &mut pcm).unwrap(); // panics
println!("decoded {n} samples");
}
Expected vs actual
- Expected: decodes successfully (libopus
opus_decode handles this exact packet fine; ffmpeg decodes the whole stream without error).
- Actual: panic.
Backtrace (RUST_BACKTRACE=1, release build with debuginfo, fresh run on 2026-07-19):
thread 'main' panicked at deps/opus-rs-0.1.23/src/lib.rs:978:41:
range end index 960 out of range for slice of length 640
stack backtrace:
0: __rustc::rust_begin_unwind
1: core::panicking::panic_fmt
2: core::slice::index::slice_index_fail
3: opus_rs::OpusDecoder::decode
4: repro_02::main
at ./src/bin/repro_02.rs:10:17
Root cause
src/lib.rs (OpusDecoder::new, 0.1.23):
w_pcm_i16: vec![0i16; 640],
640 i16 = 20 ms × 16 kHz × 2 ch. In the SILK path (src/lib.rs:978):
let pcm_i16_len = internal_frame_size * self.channels; // 60 ms WB mono = 60*16 = 960
...
silk_dec.decode(&mut rc, &mut self.w_pcm_i16[..pcm_i16_len], ...)
// ^^^ panics: 960 > 640
internal_frame_size = frame_duration_ms * internal_sample_rate / 1000 with frame_duration_ms from the TOC (up to 60). Worst cases: 60 ms WB mono = 960, 60 ms MB mono = 720, 40/60 ms stereo = 1280–1920 — all > 640. (40 ms WB mono = 640 fits exactly and works, which is why the bug is invisible with common 20 ms encodes.) The same slice pattern exists in the HYBRID path a few lines below. There is even a debug_assert!(pcm_i16_len <= self.w_pcm_i16.len()) right before it — so the condition is known, but in release it becomes an out-of-bounds slice panic instead of a handled error.
Impact
Any server decoding untrusted Ogg/Opus uploads (voice messages, podcasts) can be crashed remotely by a valid Opus stream — no malformed data required. libopus-based encoders produce such streams with -frame_duration 40/60, and the TOC byte alone (which any proxy/repacketizer may legitimately rewrite) selects the frame duration. A panic in safe Rust is a denial of service for the whole process.
Reproduction assets
Self-contained: the ffmpeg command above generates silk60.ogg; the triggering input is its first audio packet (TOC 0x58) — extract it with any Ogg demuxer (e.g. the ~15-line Python packet dumper from issue #1's reproduction section). (The exact .ogg and the extracted 194-byte packet used here are kept as repro-02.zip; happy to attach or link on request.)
Panic on valid SILK 40/60 ms frames: SILK workspace hardcoded for 20 ms (
src/lib.rs:978)Summary
OpusDecoder::decodepanics withrange end index 960 out of range for slice of length 640atsrc/lib.rs:978when given a valid, spec-compliant SILK packet whose frame duration is 60 ms (and for 40 ms stereo). RFC 6716 defines 10/20/40/60 ms SILK frames, and libopus's encoder produces them with-frame_duration 40|60. The decoder's SILK scratch bufferw_pcm_i16is hardcoded to 640 i16 samples — enough only for ≤ 20 ms at 16 kHz stereo (or 40 ms mono WB).Environment
1cc978697f3ada0ad98b323b9d85e85c6b08d478830148636c19b6d5f2ac9b85, verified against the sparse index; used as a path dep with identical content)Minimal reproduction
Generate a real 60 ms SILK stream and extract its first audio packet (TOC 0x58) with any Ogg demuxer:
ffmpeg -y -i any_speech.wav -c:a libopus -ar 16000 -ac 1 -b:a 24k -frame_duration 60 silk60.ogg # first audio packet has TOC 0x58 (SILK, wideband, 60 ms, code 0)Expected vs actual
opus_decodehandles this exact packet fine; ffmpeg decodes the whole stream without error).Backtrace (RUST_BACKTRACE=1, release build with debuginfo, fresh run on 2026-07-19):
Root cause
src/lib.rs(OpusDecoder::new, 0.1.23):640 i16 = 20 ms × 16 kHz × 2 ch. In the SILK path (
src/lib.rs:978):internal_frame_size = frame_duration_ms * internal_sample_rate / 1000withframe_duration_msfrom the TOC (up to 60). Worst cases: 60 ms WB mono = 960, 60 ms MB mono = 720, 40/60 ms stereo = 1280–1920 — all > 640. (40 ms WB mono = 640 fits exactly and works, which is why the bug is invisible with common 20 ms encodes.) The same slice pattern exists in the HYBRID path a few lines below. There is even adebug_assert!(pcm_i16_len <= self.w_pcm_i16.len())right before it — so the condition is known, but in release it becomes an out-of-bounds slice panic instead of a handled error.Impact
Any server decoding untrusted Ogg/Opus uploads (voice messages, podcasts) can be crashed remotely by a valid Opus stream — no malformed data required. libopus-based encoders produce such streams with
-frame_duration 40/60, and the TOC byte alone (which any proxy/repacketizer may legitimately rewrite) selects the frame duration. A panic in safe Rust is a denial of service for the whole process.Reproduction assets
Self-contained: the ffmpeg command above generates
silk60.ogg; the triggering input is its first audio packet (TOC 0x58) — extract it with any Ogg demuxer (e.g. the ~15-line Python packet dumper from issue #1's reproduction section). (The exact.oggand the extracted 194-byte packet used here are kept asrepro-02.zip; happy to attach or link on request.)