From f9d04ffc986cc1367e519fb0f088fd791ce89934 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:48:38 +0200 Subject: [PATCH] Validate the full WAVE_FORMAT_EXTENSIBLE fmt chunk (#465) --- crates/audio-io/src/wav.rs | 24 +++++++++++++++------ crates/audio-io/tests/malformed.rs | 34 ++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/crates/audio-io/src/wav.rs b/crates/audio-io/src/wav.rs index 1aa847d4..22c89ae4 100644 --- a/crates/audio-io/src/wav.rs +++ b/crates/audio-io/src/wav.rs @@ -50,13 +50,25 @@ fn parse_fmt(c: &[u8]) -> Result { fmt.valid_bits = fmt.bits; if fmt.tag == WAVE_FORMAT_EXTENSIBLE { // cbSize(2) validBits(2) channelMask(4) subFormat GUID(16) - if let (Some(valid), Some(sub)) = (le_u16(c, 18), le_u16(c, 24)) { - if valid != 0 { - fmt.valid_bits = valid; - } - fmt.tag = sub; - } else { + let (Some(cb_size), Some(valid), Some(sub)) = (le_u16(c, 16), le_u16(c, 18), le_u16(c, 24)) else { return Err(AudioError::Malformed("WAVE_FORMAT_EXTENSIBLE fmt chunk too short".into())); + }; + let Some(tail) = c.get(26..40) else { + return Err(AudioError::Malformed("WAVE_FORMAT_EXTENSIBLE fmt chunk too short".into())); + }; + if cb_size < 22 { + return Err(AudioError::Malformed("WAVE_FORMAT_EXTENSIBLE cbSize is below 22".into())); + } + if valid > fmt.bits { + return Err(AudioError::Malformed("WAVE_FORMAT_EXTENSIBLE valid bits exceed the container size".into())); + } + if valid != 0 { + fmt.valid_bits = valid; + } + // A subtype outside the KSDATAFORMAT_SUBTYPE_* family is not PCM/float, whatever its first two bytes say: + // keep the extensible tag so the file goes to the foreign decoder. + if tail == GUID_TAIL { + fmt.tag = sub; } } Ok(fmt) diff --git a/crates/audio-io/tests/malformed.rs b/crates/audio-io/tests/malformed.rs index ed0d6e31..d9ac2459 100644 --- a/crates/audio-io/tests/malformed.rs +++ b/crates/audio-io/tests/malformed.rs @@ -485,3 +485,37 @@ proptest! { prop_assert_eq!(flac, wav); } } + +/// An extensible `fmt ` chunk: cbSize, valid bits, mask, then the 16-byte subtype GUID. +fn extensible_fmt(cb_size: u16, valid_bits: u16, guid: &[u8]) -> Vec { + let mut f = fmt(0xFFFE, 1, 44_100, 16); + f.extend_from_slice(&cb_size.to_le_bytes()); + f.extend_from_slice(&valid_bits.to_le_bytes()); + f.extend_from_slice(&4u32.to_le_bytes()); + f.extend_from_slice(guid); + f +} + +const PCM_GUID: [u8; 16] = [1, 0, 0, 0, 0, 0, 0x10, 0, 0x80, 0, 0, 0xAA, 0, 0x38, 0x9B, 0x71]; + +#[test] +fn extensible_fmt_is_checked_in_full() { + let data = vec![0u8; 8]; + let good = riff(&[(b"fmt ", extensible_fmt(22, 16, &PCM_GUID)), (b"data", data.clone())]); + assert_eq!(probe(&good, None).unwrap().sample_format, SampleFormat::Int16); + // Subtype GUID cut short after the format code. + let cut = riff(&[(b"fmt ", extensible_fmt(22, 16, &PCM_GUID[..4])), (b"data", data.clone())]); + assert!(matches!(probe(&cut, None), Err(AudioError::Malformed(_)))); + assert!(matches!(decode(&cut, None), Err(AudioError::Malformed(_)))); + // cbSize too small for the extension. + let small_cb = riff(&[(b"fmt ", extensible_fmt(0, 16, &PCM_GUID)), (b"data", data.clone())]); + assert!(matches!(probe(&small_cb, None), Err(AudioError::Malformed(_)))); + // More valid bits than the 16-bit container holds. + let wide = riff(&[(b"fmt ", extensible_fmt(22, 24, &PCM_GUID)), (b"data", data.clone())]); + assert!(matches!(probe(&wide, None), Err(AudioError::Malformed(_)))); + // PCM-like prefix but a foreign GUID tail is not read as native PCM. + let mut foreign = PCM_GUID; + foreign[15] ^= 0xFF; + let foreign = riff(&[(b"fmt ", extensible_fmt(22, 16, &foreign)), (b"data", data)]); + assert!(!matches!(probe(&foreign, None), Ok(i) if i.sample_format == SampleFormat::Int16)); +}