mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-30 05:26:52 -06:00
Route sequential H.264 sample reads through the 256 MiB cap
decode_h264 read each sample with Mp4File::read_sample directly, so a sample table entry above 256 MiB was allocated and read in full when the source was long enough. It now goes through Movie::read like the keyframe and intra-frame paths, and an oversized sample is skipped like any other unreadable access unit. Fixes #912
This commit is contained in:
@@ -485,7 +485,7 @@ impl Movie {
|
||||
let s = st.next;
|
||||
st.next += 1;
|
||||
let pts = self.rank.get(s).copied().unwrap_or(0) as i64;
|
||||
let data = match self.mp4.read_sample(&*self.src, self.track, s) {
|
||||
let data = match self.read(s) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
last_err = Some(e.to_string());
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
//! `image` encoder, muxed with the bundled MP4/MOV writer. When `ffmpeg` with libx264 is on PATH,
|
||||
//! real High-profile streams with B-frames are checked too (skipped otherwise).
|
||||
|
||||
use crate::isobmff::{Brand, Mp4Writer, SampleEntry, TrackConfig, WriteSample, WriterOptions};
|
||||
use crate::isobmff::{Brand, ByteSource, Mp4Writer, SampleEntry, TrackConfig, WriteSample, WriterOptions};
|
||||
use crate::synth::{frame_color, h264_mp4, mjpeg_mov, prores_mov};
|
||||
use crate::{Movie, VideoError};
|
||||
use std::io::Cursor;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
fn expected_rgb(i: usize) -> [u8; 3] {
|
||||
let [y, cb, cr] = frame_color(i);
|
||||
@@ -118,6 +120,47 @@ fn prores_frame_size_is_checked_before_decoding() {
|
||||
assert!(matches!(m.frame(0), Err(VideoError::Decode(e)) if e.contains("56x48 frame in a 64x48 track")));
|
||||
}
|
||||
|
||||
/// In-memory movie bytes, zero-padded up to `len` without storing the padding. Records the
|
||||
/// largest read asked of it and refuses reads over 1 MiB (the test movies are much smaller).
|
||||
struct PaddedSource {
|
||||
bytes: Vec<u8>,
|
||||
len: u64,
|
||||
largest: AtomicUsize,
|
||||
}
|
||||
|
||||
impl ByteSource for PaddedSource {
|
||||
fn len(&self) -> u64 {
|
||||
self.len
|
||||
}
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> std::io::Result<()> {
|
||||
self.largest.fetch_max(buf.len(), Ordering::Relaxed);
|
||||
if buf.len() > 1 << 20 {
|
||||
return Err(std::io::Error::other("read too large for the test source"));
|
||||
}
|
||||
let start = usize::try_from(offset).unwrap_or(usize::MAX);
|
||||
for (i, b) in buf.iter_mut().enumerate() {
|
||||
*b = self.bytes.get(start.saturating_add(i)).copied().unwrap_or(0);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn h264_sample_reads_are_capped() {
|
||||
// One frame whose `stsz` entry claims 300 MiB, in a source long enough to hold it.
|
||||
let mut bytes = h264_mp4(1, 1).unwrap();
|
||||
let at = bytes.windows(4).position(|w| w == b"stsz").unwrap();
|
||||
// After the type: version and flags, the constant size (0: one size per sample), the count.
|
||||
let field = if bytes[at + 8..at + 12] == [0; 4] { at + 16 } else { at + 8 };
|
||||
bytes[field..field + 4].copy_from_slice(&(300u32 << 20).to_be_bytes());
|
||||
let src = Arc::new(PaddedSource { bytes, len: 1 << 30, largest: AtomicUsize::new(0) });
|
||||
let mut m = Movie::open_source(src.clone()).unwrap();
|
||||
let r = m.frame(0);
|
||||
assert!(matches!(&r, Err(VideoError::Decode(e)) if e.contains("sample larger than 256 MiB")), "{r:?}");
|
||||
let largest = src.largest.load(Ordering::Relaxed);
|
||||
assert!(largest <= 256 << 20, "a {largest}-byte read reached the source");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_codec_opens_but_does_not_decode() {
|
||||
let mut mw = Mp4Writer::new(Cursor::new(Vec::new()), WriterOptions::new(Brand::Mp4)).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user