security(opus): validate direct multistream headers (#2905)

This commit is contained in:
Earl Co
2026-10-23 10:53:00 -04:00
committed by GitHub
parent 980c388325
commit 71d5f52b4d
2 changed files with 51 additions and 1 deletions
+50 -1
View File
@@ -90,6 +90,38 @@ impl OpusHead {
v
}
/// Validate a header independently of its wire parser. This is required for callers of
/// [`crate::Decoder::from_head`], which may construct an `OpusHead` directly with `usize`
/// values that cannot occur in the byte format.
pub fn validate(&self) -> Result<()> {
if !(1..=255).contains(&self.channels) || self.mapping.len() != self.channels {
return Err(Error::InvalidHeader("bad channel count or mapping length"));
}
if self.mapping_family == 0 {
if self.channels > 2 || self.streams != 1 || self.coupled_streams != self.channels - 1 || self.mapping.iter().copied().ne(0..self.channels as u8) {
return Err(Error::InvalidHeader("invalid family 0 mapping"));
}
return Ok(());
}
if self.mapping_family == 3 {
return Err(Error::Unsupported("channel mapping family 3 (projection)"));
}
if self.streams == 0
|| self.streams > 255
|| self.coupled_streams > self.streams
|| self.streams.checked_add(self.coupled_streams).is_none_or(|n| n > 255)
{
return Err(Error::InvalidHeader("bad stream counts"));
}
if self.mapping_family == 1 && !(1..=8).contains(&self.channels) {
return Err(Error::InvalidHeader("family 1 allows 1 to 8 channels"));
}
if self.mapping.iter().any(|&m| m != 255 && m as usize >= self.streams + self.coupled_streams) {
return Err(Error::InvalidHeader("channel mapping out of range"));
}
Ok(())
}
fn build(channels: usize, pre_skip: u16, input_sample_rate: u32, output_gain: i16, family: u8, rest: &[u8]) -> Result<OpusHead> {
if channels == 0 {
return Err(Error::InvalidHeader("zero channels"));
@@ -120,7 +152,9 @@ impl OpusHead {
}
(streams, coupled, mapping)
};
Ok(OpusHead { channels, pre_skip, input_sample_rate, output_gain, mapping_family: family, streams, coupled_streams: coupled, mapping })
let head = OpusHead { channels, pre_skip, input_sample_rate, output_gain, mapping_family: family, streams, coupled_streams: coupled, mapping };
head.validate()?;
Ok(head)
}
/// Linear output gain factor.
@@ -176,4 +210,19 @@ mod tests {
assert_eq!(head.pre_skip, 312);
assert_eq!(head.channels, 2);
}
#[test]
fn directly_constructed_headers_are_checked_before_decoder_allocation() {
let head = OpusHead {
channels: 1,
pre_skip: 0,
input_sample_rate: 48_000,
output_gain: 0,
mapping_family: 255,
streams: usize::MAX,
coupled_streams: 0,
mapping: vec![0],
};
assert!(crate::Decoder::from_head(head, 48_000).is_err());
}
}
+1
View File
@@ -32,6 +32,7 @@ impl Decoder {
/// Creates a decoder for a parsed header at `sample_rate` (8/12/16/24/48 kHz).
pub fn from_head(head: OpusHead, sample_rate: u32) -> Result<Decoder> {
head.validate()?;
let mut streams = Vec::with_capacity(head.streams);
for s in 0..head.streams {
let ch = if s < head.coupled_streams { 2 } else { 1 };