Reference keyframes per track, size the DTS reserve, correct two claims

The ReferenceBlock offset was computed for ANY video track, but the keyframe tick
it measures against was recorded in a single global slot gated to the PRIMARY
video track. On a title with two video tracks — an MVC base plus secondary view,
or a multi-angle disc — a secondary track's non-keyframe therefore referenced a
keyframe on a different track, or 0 (a self-reference) when the primary had not
produced one yet. The tick is now recorded per track, so a non-keyframe can only
reference a keyframe on its own track.

The faststart moov-hole estimate modelled every audio track as (E-)AC-3 at 1536
samples per frame. 1.6.0 added DTS to the writer's carried set, and a DTS core AU
is commonly 512 samples — a third of that — so a DTS track's sample table was
under-reserved threefold and the mux fell back to moov-at-end, losing faststart
on exactly the files 1.6.0 newly supports.

mvc_frame_emits_blockgroup_additional_and_reference asserted only that the
non-keyframe's ReferenceBlock was Some(_). Its non-MVC sibling, added in the same
commit, pins the exact offset; this one now does too, so a mutant emitting a
constant or wrong-signed offset no longer passes.

The comment above the mp4 sample budget claimed file_len stops a crafted file
inflating allocations "past the file's own size". Each indexed sample costs ~52
bytes, so the real ceiling is ~52x file_len (still capped by MAX_SAMPLE_COUNT).
The bound is real; the comment overstated how tight it is.
This commit is contained in:
Matthew Jackson
2026-07-29 19:03:59 -07:00
parent d4c913e0d3
commit 99c5fd3500
3 changed files with 50 additions and 16 deletions
+6
View File
@@ -98,6 +98,12 @@ impl<R: Read + Seek> Mp4Reader<R> {
// crafted file with a fixed-size `stsz` claiming count=0xFFFFFFFF can't
// inflate the `sizes`/`Vec<SampleRef>` allocations past the file's own size
// (a genuine large title has file_len ≫ sample count, so it is unaffected).
// NOTE on the bound this actually gives: each indexed sample costs about
// 52 bytes of RAM (SampleRef 40 + u32 size 4 + u64 offset 8, plus 4 each
// for the expanded stts/ctts), so the ceiling is ~52x file_len, not 1x —
// capped by MAX_SAMPLE_COUNT. That is still a real bound (a 1 MiB crafted
// file cannot reach the 16M-sample ceiling), just not the "past the file's
// own size" the previous comment implied.
let mut sample_budget = MAX_SAMPLE_COUNT.min(file_len.min(usize::MAX as u64) as usize);
// Bound the scan at MAX_TRACKS *matches* so a crafted moov packed with tiny