Skip to content

Panic on valid SILK 40/60 ms frames: SILK workspace hardcoded for 20 ms (src/lib.rs:978) #5

Description

@ekhodzitsky

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.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions