mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-31 14:06:56 -06:00
Refuse files whose packets claim millions of frames
A 59 KB ALAC file in an M4A made the app use 25 GB of memory before it gave up: symphonia's ALAC decoder sizes its buffers from the frame length in the file's codec settings, and nothing bounds it. The PCM and ADPCM decoders likewise allocate the frames per packet the container declares, which an MP4 version-2 LPCM entry takes from the file. Before a decoder is made, the frames a packet may hold, and an ALAC frame length, must be at most 2^20 (about 22 seconds at 48 kHz; Apple's ALAC encoder uses 4096), or the file is refused as malformed.
This commit is contained in:
@@ -16,9 +16,13 @@ use symphonia::core::io::{MediaSourceStream, MediaSourceStreamOptions, ReadBytes
|
||||
use symphonia::core::meta::MetadataOptions;
|
||||
use symphonia::core::probe::Instantiate;
|
||||
|
||||
use crate::pcm::MAX_DECODE_SAMPLES;
|
||||
use crate::pcm::{MAX_DECODE_SAMPLES, be_u32};
|
||||
use crate::{AudioBuffer, AudioError, AudioInfo, FileFormat, Result, SampleFormat};
|
||||
|
||||
/// The most frames a packet may declare: decoders allocate a packet's frames up front, and ALAC
|
||||
/// takes its frame length from the file (encoders use 4096).
|
||||
const MAX_PACKET_FRAMES: u64 = 1 << 20;
|
||||
|
||||
fn map_err(e: SymError) -> AudioError {
|
||||
match e {
|
||||
SymError::Unsupported(m) => AudioError::Unsupported(m.to_string()),
|
||||
@@ -142,6 +146,10 @@ fn run(bytes: &[u8], detected: FileFormat, count_only: bool) -> Result<(AudioInf
|
||||
.find(|t| t.id == track_id)
|
||||
.map(|t| t.codec_params.clone())
|
||||
.ok_or_else(|| AudioError::Malformed("track vanished".into()))?;
|
||||
let alac = params.extra_data.as_deref().filter(|_| params.codec == CODEC_TYPE_ALAC).and_then(|d| be_u32(d, 0));
|
||||
if params.max_frames_per_packet.into_iter().chain(alac.map(u64::from)).any(|n| n > MAX_PACKET_FRAMES) {
|
||||
return Err(AudioError::Malformed("the file declares packets far longer than any codec uses".into()));
|
||||
}
|
||||
let mut decoder = symphonia::default::get_codecs().make(¶ms, &DecoderOptions::default()).map_err(map_err)?;
|
||||
|
||||
let mut planar: Vec<Vec<f32>> = Vec::new();
|
||||
|
||||
@@ -214,15 +214,20 @@ fn atom(kind: &[u8; 4], body: &[u8]) -> Vec<u8> {
|
||||
[&(8 + body.len() as u32).to_be_bytes()[..], kind, body].concat()
|
||||
}
|
||||
|
||||
/// An MP4 of `frames` frames of 16-bit mono PCM; `swap` may replace any version-0 atom.
|
||||
fn pcm_mp4(frames: u32, swap: impl Fn(&[u8; 4], Vec<u8>) -> Vec<u8>) -> Vec<u8> {
|
||||
/// A mono 16-bit, 48 kHz audio sample entry of `version`, its version fields and codec atoms.
|
||||
fn sample_entry(kind: &[u8; 4], version: u8, fields: &[u8], codec: &[u8]) -> Vec<u8> {
|
||||
let base = [0, 0, 0, 0, 0, 0, 0, 1, 0, version, 0, 0, 0, 0, 0, 0, 0, 1, 0, 16, 0, 0, 0, 0];
|
||||
atom(kind, &[&base[..], &(48_000u32 << 16).to_be_bytes(), fields, codec].concat())
|
||||
}
|
||||
|
||||
/// An MP4 of `frames` 2-byte samples described by `entry`; `swap` may replace any version-0 atom.
|
||||
fn mp4(frames: u32, entry: &[u8], swap: impl Fn(&[u8; 4], Vec<u8>) -> Vec<u8>) -> Vec<u8> {
|
||||
let full = |kind: &[u8; 4], fields: &[u32]| {
|
||||
let body: Vec<u8> = [0].iter().chain(fields).flat_map(|v: &u32| v.to_be_bytes()).collect();
|
||||
swap(kind, atom(kind, &body))
|
||||
};
|
||||
let entry = atom(b"sowt", &[&[0u8; 6][..], &[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 16, 0, 0, 0, 0], &(48_000u32 << 16).to_be_bytes()].concat());
|
||||
let moov = |offset: u32| {
|
||||
let stsd = atom(b"stsd", &[&[0, 0, 0, 0, 0, 0, 0, 1][..], &entry].concat());
|
||||
let stsd = atom(b"stsd", &[&[0, 0, 0, 0, 0, 0, 0, 1][..], entry].concat());
|
||||
let stbl =
|
||||
[stsd, full(b"stts", &[1, frames, 1]), full(b"stsc", &[1, 1, frames, 1]), full(b"stsz", &[2, frames]), full(b"stco", &[1, offset])];
|
||||
let minf = [full(b"smhd", &[0]), atom(b"stbl", &stbl.concat())].concat();
|
||||
@@ -238,7 +243,8 @@ fn pcm_mp4(frames: u32, swap: impl Fn(&[u8; 4], Vec<u8>) -> Vec<u8>) -> Vec<u8>
|
||||
|
||||
#[test]
|
||||
fn mp4_sizes_beyond_the_file_are_errors_not_aborts() {
|
||||
let (_, buf) = decode(&pcm_mp4(480, |_, a| a), None).unwrap();
|
||||
let pcm = sample_entry(b"sowt", 0, &[], &[]);
|
||||
let (_, buf) = decode(&mp4(480, &pcm, |_, a| a), None).unwrap();
|
||||
assert_eq!(buf.frames(), 480);
|
||||
let huge = |kind: &[u8; 4], a: Vec<u8>| [&1u32.to_be_bytes()[..], kind, &(1u64 << 60).to_be_bytes(), &a[8..]].concat();
|
||||
// The first field after version and flags: stts's entry count, stsz's sample size.
|
||||
@@ -252,10 +258,10 @@ fn mp4_sizes_beyond_the_file_are_errors_not_aborts() {
|
||||
let mdia = atom(b"mdia", &1u32.to_be_bytes());
|
||||
let straddling = [&b"hdlr"[..], &(1u64 << 60).to_be_bytes(), &[0; 24]].concat();
|
||||
let files = [
|
||||
pcm_mp4(480, |k, a| if k == b"hdlr" { huge(k, a) } else { a }),
|
||||
pcm_mp4(480, |k, a| if k == b"stts" { first_field_max(a) } else { a }),
|
||||
pcm_mp4(480, |k, a| if k == b"stsz" { first_field_max(a) } else { a }),
|
||||
pcm_mp4(480, |k, a| if k == b"stsz" { per_sample.clone() } else { a }),
|
||||
mp4(480, &pcm, |k, a| if k == b"hdlr" { huge(k, a) } else { a }),
|
||||
mp4(480, &pcm, |k, a| if k == b"stts" { first_field_max(a) } else { a }),
|
||||
mp4(480, &pcm, |k, a| if k == b"stsz" { first_field_max(a) } else { a }),
|
||||
mp4(480, &pcm, |k, a| if k == b"stsz" { per_sample.clone() } else { a }),
|
||||
[atom(b"ftyp", b"M4A \0\0\0\0M4A isom"), atom(b"moov", &atom(b"trak", &mdia)), straddling].concat(),
|
||||
];
|
||||
for f in files {
|
||||
@@ -271,6 +277,20 @@ fn atoms_running_past_their_parent_are_walked_once() {
|
||||
assert!(decode(&f, None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packets_longer_than_any_codec_uses_are_refused() {
|
||||
let be = |v: &[u32]| v.iter().flat_map(|x| x.to_be_bytes()).collect::<Vec<u8>>();
|
||||
let v2 = |frames: u32| [&be(&[72])[..], &48_000f64.to_be_bytes(), &be(&[1, 0x7f00_0000, 16, 4, 2, frames])].concat();
|
||||
let lpcm = |frames: u32| sample_entry(b"lpcm", 2, &v2(frames), &[]);
|
||||
let config = |frames: u32| [&be(&[0, frames])[..], &[0, 16, 40, 10, 14, 1, 0, 255], &be(&[0, 0, 48_000])].concat();
|
||||
let alac = |frames: u32| sample_entry(b"alac", 0, &[], &atom(b"alac", &config(frames)));
|
||||
let (_, buf) = decode(&mp4(480, &lpcm(1), |_, a| a), None).unwrap();
|
||||
assert_eq!(buf.frames(), 480);
|
||||
for entry in [lpcm(1 << 21), alac(1 << 21)] {
|
||||
assert!(matches!(decode(&mp4(480, &entry, |_, a| a), None), Err(AudioError::Malformed(m)) if m.contains("declares packets")));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_tiny_chunks_terminate() {
|
||||
let mut chunks: Vec<(&[u8; 4], Vec<u8>)> = vec![(b"fmt ", fmt(1, 1, 8_000, 16))];
|
||||
|
||||
Reference in New Issue
Block a user