Files
libfreemkv/docs/mp4-read.md
T
Matthew Jackson 6e14fc0c0c libfreemkv: 1.7.0 groundwork — sector-recycle soundness + CI hardening
Security
- Remove the unsafe Vec::set_len over recycled sector buffers — UB reachable
  from safe code (GHSA-j8ww-f5fg-9pmh, low). Uses Vec::resize; pooling unchanged.

CI / tooling
- clippy on stable with the MSRV single-sourced from Cargo.toml; cargo-deny
  dependency-audit gate with first-party git sources allowed (#15); audience-based
  comment-guard (#17).

Docs
- README (keydb ownership, BD-J parser count), drive-access transport/reset
  notes, mux timeline module-doc links.
2026-09-02 12:40:55 -07:00

500 lines
25 KiB
Markdown

# mp4 read (`src/mux/mp4/read.rs`) — internal notes
## `MAX_TRACKS`
Track count is otherwise unbounded (a crafted `moov` can pack tens of
thousands of `trak` boxes), and the per-track PID is `0x1011 + track_idx`,
which overflows `u16` past ~61k tracks. Real titles have well under a
hundred tracks.
## `MAX_SAMPLE_COUNT`
MP4 sample-table fields (`stsz` sample_count, `stts`/`ctts` run-lengths) are
untrusted 32-bit values; a crafted box can declare billions of entries in a
few bytes. Real titles stay far under this (a 10 h/60 fps track is ~2M
samples), so clamping to it caps a hostile file's allocation without
truncating any legitimate track.
## `MIN_FILE_BYTES_PER_SAMPLE`
Only `vide` and `soun` tracks are indexed here, and no real coded video or
audio access unit is anywhere near this small: the shortest legal AC-3
frame is 128 bytes, an AAC frame is hundreds, and a video sample carries at
least a NAL header plus slice data. A 2-hour title runs to thousands of
file bytes per sample, so this cannot truncate a genuine track — it only
stops a crafted sample table from claiming more samples than the file
could possibly hold.
## Test rationale (`mod tests`)
### `FakeBigReader`
A `Read + Seek` backed by a small crafted prefix followed by an endless run
of zeros, reporting `len` bytes total on `seek(End)`. Lets a test exercise
a hundred-MiB-to-multi-GiB boundary (`MAX_ALLOC_BYTES`, a sparse-file-inflated
`file_len`) without a real backing allocation of that size for the SOURCE
side — only the destination buffer the code under test allocates is real,
which is the point of the test.
### `parse_stss_reads_two_distinct_entries_from_their_own_offsets`
`parse_stss` had no direct positive-path test anywhere in this file — only
its short-buffer safety was pinned. Two real, distinct entries in a buffer
sized to exactly fit them (`b.len() == 16`, so the second entry's
`o + 4 == b.len()` exactly) separates every arithmetic mutant at once: an
`o = 8 + i*4` → `8 - i*4` slip reads the `count` field instead of the
second entry; an `i*4` → `i/4` slip re-reads the FIRST entry for the
second (integer division collapses `i=1` back to the same offset as
`i=0`); and an `o + 4 > b.len()` → `o + 4 < b.len()` slip breaks out on
the very first entry, because there IS more room after it (`12 < 16`) —
leaving the set empty instead of holding both.
### `parse_stss_count_lie_is_bounded_by_the_box_not_trusted`
A declared `count` larger than the box can hold (a lie, or a truncated
box) must be bounded by the per-entry guard, not by trusting the count —
the SAME contract `parse_stco`/`parse_stsc`/`parse_stts` pin with their
own "count lie" tests. Bounded HERE means the loop must stop, not read
past the end: an `o + 4 > b.len()` → `o - 4 > b.len()` mutant almost
never fires (`o - 4` stays small for every realistic `o`), so it lets
the loop walk straight past a truncated box into an out-of-bounds `be32`
read.
### `sample_offsets_advances_through_distinct_sizes_within_one_chunk`
`sidx` must advance by exactly one PER SAMPLE PLACED, so the third (and
every later) sample in a chunk picks up its own size, not the first
sample's reused over and over. With only two samples in a chunk this bug
is invisible — the second sample's pushed offset was already computed
from `sizes[0]` before `sidx` had a chance to matter — so this test needs
a chunk with THREE distinctly-sized samples to expose it.
### `stream_read_allows_a_sample_of_exactly_max_alloc_bytes`
`Stream::read`'s own `s.size as u64 > MAX_ALLOC_BYTES` cap — a SEPARATE
call site from `read_moov`'s (see
`read_moov_allows_a_payload_of_exactly_max_alloc_bytes` / the module doc's
note on the same policy hardened at only one of two sites elsewhere in
this crate). A `>`↔`>=` mutant here would reject a sample of exactly the
cap, which must be allowed. Builds an `Mp4Reader` directly (its fields are
private but visible to this module's tests) over a `FakeBigReader` so the
256 MiB sample doesn't need a real backing file — only the destination
buffer `read()` allocates is real.
### `stream_read_rejects_a_sample_one_byte_over_max_alloc_bytes`
The same cap's other edge: one byte OVER it must still be rejected. A
`>`↔`==` mutant would only catch a sample of EXACTLY the cap and let
anything larger through — the worse direction to get wrong, since it
turns the cap into a no-op for every real oversized/hostile sample.
### `stts_expands_runs_to_per_sample_deltas_in_order`
`stts` run-length expansion — ISO/IEC 14496-12 §8.6.1.2. The box stores
`(sample_count, sample_delta)` runs; the reader must expand them back to
one delta PER SAMPLE, in order, or every sample after the first run lands
on the wrong decode time.
(This test used to be called `stts_and_ctts_expand` while touching no
`ctts` box at all. The composition-offset half now lives in
`ctts_build_and_parse_are_exact_inverses_over_signed_offsets` and
`b_frame_presentation_order_survives_the_mp4_round_trip`.)
### `declared_track_duration_equals_frame_count_times_frame_duration`
A track's declared duration must be its real length. `mdhd.duration` is in
the MEDIA timescale and `mvhd.duration` in the movie timescale
(ISO/IEC 14496-12 §8.2.2, §8.4.2); a player uses them to draw the seek bar
and to decide when the title ends, so a zeroed or constant duration makes
a correct file unseekable and apparently empty.
### `moov_tree_carries_the_mandatory_track_header_and_media_boxes`
The `moov` tree must carry the boxes ISO/IEC 14496-12 makes mandatory for
a playable track, with the field values the spec fixes. These live in
this module rather than the writer's because the box-walking helpers
(`find_box`) are here — asserting through them means the test reads the
file the way the demuxer does, instead of re-deriving the layout.
Nothing in the demux path needs `tkhd`/`vmhd`/`smhd`/`dinf`, so an empty
one of any of them round-trips through this crate unnoticed while making
the file unplayable elsewhere.
### `table_parsers_reject_every_length_up_to_the_header_size`
Every one of these five table parsers opens with `if b.len() < 8 { return
<empty> }` before reading `count = be32(b, 4)`, which needs `b.len() >= 8`.
No existing fixture ever called any of them with a buffer shorter than 8
bytes — every test builds a complete box — so a `<`→`==` mutant of that
guard (which only early-returns at EXACTLY `b.len() == 8`, letting every
length below it fall through to the out-of-bounds `be32` read) had
nothing to fail against. Pins the entire boundary at once, every length
from 0 through 8, for all five.
### `parse_stco_co64_reads_each_byte_from_its_own_offset`
`co64`'s 8-byte offsets are decoded byte-by-byte
(`u64::from_be_bytes([b[o], b[o+1], ..., b[o+7]])`) — a SEPARATE code path
from the 32-bit `stco` case, which goes through `be32` instead and was
never itself built with a co64 fixture anywhere in this file. Every byte
here is distinct, so an index slip (off by one, negated, or scaled) that
reads the wrong byte cannot agree by coincidence.
### `parse_stsd_rejects_every_length_below_the_header_size`
Every buffer shorter than the 8-byte version+flags+entry_count header
must return `None` — never fall through to `&b[8..]`, which would slice
past the end and panic. `b.len() == 8` is the boundary itself (see the
equivalence note on `parse_stsd`'s guard); everything strictly below it
must be rejected by THIS check, not rely on a later one that isn't
reached yet.
### `parse_esds_asc_boundary_checks_are_independent`
The final guard is `asc_len == 0 || end > b.len()` — two independent
rejection reasons, not one condition that needs both. Pins each half:
* `asc_len == 0` must be rejected even though `end == pos` is trivially
in bounds (an `||`→`&&` mutant would let a useless zero-length ASC
through as `Some(vec![])`);
* `end > b.len()` means TRUNCATED, not "anything before the buffer's
end" — trailing bytes after a complete ASC (`end < b.len()`) must
still succeed (a `>`→`<` mutant rejects exactly this, the common case
of an esds embedded inside a larger box with more child boxes or
padding after it).
### `read_moov_exactly_header_sized_is_a_valid_empty_box`
`box_size < header_len` rejects a box that cannot even hold its own
header. A box whose size is EXACTLY `header_len` (8, no 64-bit largesize)
is the boundary itself: legal (an empty box), so an empty `moov` — size
8, zero payload bytes — must parse to `Ok(vec![])`, not be rejected by a
`<`→`<=`/`==` mutant that also rejects the boundary.
### `read_moov_rejects_a_box_that_overruns_the_file_via_the_or_not_and_guard`
The forward-progress / EOF guard is `box_size < header_len ||
pos.checked_add(box_size).is_none_or(|end| end > file_end)` — an `||`↔`&&`
mutant only shows up on an input where EXACTLY ONE side is true. A box
that claims to run 992 bytes past a file that ends right after its header
satisfies only the second clause (`box_size >= header_len`, so the first
is false); under `&&` the guard would not fire, and `read_moov` would
instead fail later inside `read_exact` with a plain `UnexpectedEof` — a
different, uncoded `io::Error` rather than this crate's `Mp4Invalid`
(`ErrorKind::InvalidData`), which the guard exists to produce uniformly
for every malformed box.
### `read_moov_allows_a_payload_of_exactly_max_alloc_bytes`
`payload_len > MAX_ALLOC_BYTES` is a SEPARATE, sparse-file-proof cap from
the EOF check above (see
`read_moov_over_cap_rejected_despite_inflated_file_len`). A `>`→`>=`
mutant would reject a `moov` whose payload is EXACTLY `MAX_ALLOC_BYTES`,
which must be allowed.
### `edit_list_media_time_shifts_the_presentation_timeline`
Regression (silent A/V desync): the sample timeline was built purely from
stts/ctts starting at tick 0 and no `edts`/`elst` was ever parsed, so the
presentation timeline an edit list defines was discarded. A non-empty
edit with `media_time = 1024` — the standard way encoder delay is
expressed — must move the track's presentation, not be ignored.
### `parse_elst_v1_entry_bytes_come_from_their_own_offsets`
Every byte of a version-1 entry's `segment_duration` (8 bytes) and
`media_time` (8 bytes) at its own offset — not a neighbour's. The
existing fixtures elsewhere use `5_000u64` and `-1i64`, which are almost
all zero/`0xFF` bytes, so an index slip that reads an adjacent byte (or a
header byte outside the entry) often reads the SAME value and the test
cannot tell. Every byte here is distinct and nonzero, and the header
bytes are zero, so any wrong index — off by one, negated, or scaled —
pulls in a value that cannot match by coincidence.
### `elst_offset_ticks_stays_safe_at_zero_empty_ticks_even_with_a_zero_movie_timescale`
`Some(mts) if empty_movie_ticks > 0` and the `None` branch's `if
empty_movie_ticks > 0` are both guards around a `tracing::warn!`/delay
computation that, at `empty_movie_ticks == 0`, must produce the exact
same answer as skipping it (0 ticks of delay — there is no empty edit to
convert). The one case where the two are NOT interchangeable is
`movie_timescale == Some(0)`: skipping the arithmetic returns 0, but
entering it divides by that zero timescale and panics. Real callers
never pass this — `from_reader` filters `mvhd`'s timescale through
`.filter(|&t| t != 0)` before it ever reaches this function (see
`mdhd_timescale_zero_does_not_divide_by_zero` for the sibling guard on
the media timescale) — but the function itself has to stay panic-free at
its own boundary, independent of what its one caller happens to do
today, because `libfreemkv` runs inside the long-lived `autorip` service
and a panic there is downtime.
### `audio_trak_hostile_count`
A crafted file with a *fixed-size* `stsz` (sample_size != 0) claiming
count = 0xFFFFFFFF must not inflate the sample index past the file's own
byte length. `from_reader` sets `sample_budget =
MAX_SAMPLE_COUNT.min(file_len)`, so a few-hundred-byte file bounds the
`Vec<SampleRef>` to a few hundred — NOT the 16M `MAX_SAMPLE_COUNT`
ceiling. Mutation check: revert the budget to a bare `MAX_SAMPLE_COUNT`
and this file yields ~16M samples, failing the `<= file_len` (and `<
MAX_SAMPLE_COUNT`) assertions below. Built by a minimal-but-complete
audio `trak` whose fixed-size `stsz` LIES about its sample count
(`u32::MAX`), with an stsc/stts wide enough to place whatever count
survives the budget.
### `sample_index_ram_is_bounded_by_a_multiple_of_the_file`
The RAM amplification the byte-per-sample budget actually bounds: a
small crafted file whose `stsz` claims `u32::MAX` samples must not force
an eager multi-hundred-MB index. At ~60 bytes of RAM per indexed sample,
a budget of one sample per file byte gave ~60x the input size; the
assertion pins the amplification factor rather than a raw count, so it
fails if the budget ever goes back to counting samples per byte.
### `audio_trak_missing`
Build an audio `trak` identical to `audio_trak(48_000)` but with the
named stbl child box omitted. Used to reach the untrusted-input guards
that drop a track whose `stsz` says samples exist yet whose `stco`/`co64`
(chunk offsets) or `stsc` (sample-to-chunk map) is missing — without such
a table every sample offset would resolve near file byte 0.
### `missing_stco_drops_track_all_dropped_is_invalid`
A track with samples (`stsz`) but no chunk-offset table (`stco`/`co64`)
must be DROPPED, not indexed with offsets that resolve near file byte 0.
With it the only track, the whole file fails `Mp4Invalid`. Mutation
check: delete the `if chunk_offsets.is_empty() { continue; }` guard and
`from_reader` returns `Ok` (garbage samples), flipping this to FAIL.
### `missing_stsc_drops_track_all_dropped_is_invalid`
A track with samples (`stsz`) and chunk offsets (`stco`) but no
sample-to-chunk map (`stsc`) must be DROPPED — without `stsc` the
samples cannot be placed against the chunk offsets and would pack from
byte 0. Mutation check: delete the `if stsc.is_empty() { continue; }`
guard and `from_reader` returns `Ok`, flipping this to FAIL.
### `missing_stts_drops_track_all_dropped_is_invalid`
A track with samples (`stsz`) but no time-to-sample table (`stts`,
mandatory per ISO/IEC 14496-12 §8.6.1) must be DROPPED — without it every
sample takes dur=0, collapsing the whole track onto one instant
(all-zero timestamps). With it the only track, the file fails
`Mp4Invalid`. Mutation check: delete the `if durations.is_empty() {
continue; }` guard and `from_reader` returns `Ok` (all-zero-timestamp
samples), flipping this to FAIL.
### `stsc_placing_fewer_samples_than_stsz_drops_the_track`
An `stsc` that PASSES the non-empty guard but places fewer samples than
`stsz` declares must drop the track. `sample_offsets` used to pack the
unplaced tail after the last known offset, inventing a position, so
those frames were read from arbitrary file bytes — the same "emit
garbage" outcome the stco/stsc presence guards exist to refuse.
Mutation check: restore the trailing `while offsets.len() < sizes.len()`
pack-after-last loop and this file indexes 3 samples instead of erroring.
### `short_stts_drops_the_track_like_an_absent_one`
A SHORT `stts` — present and non-empty, but covering fewer samples than
`stsz` declares — must drop the track just like an absent one. The tail
samples took `dur = 0`, collapsing the whole tail onto a single
timestamp, which is the exact degenerate timing the absent-stts guard
refuses.
Mutation check: weaken the guard back to `durations.is_empty()` and this
file indexes 3 samples whose last two share one timestamp.
### `mdhd_language_boundary_rejects_short_not_merely_non_exact`
The length guard is `b.len() < off + 2` — reject too SHORT, not "not
exactly `off + 2`". Pins both edges of that boundary for the version-0
offset (`off == 20`):
* `b.len() == off` (no room for the packed field at all) must return
`None`, not read two bytes past the end;
* `b.len() == off + 3` (one byte MORE than the minimum) must still
succeed — a `<`→`>` mutant of the guard would reject this case, since
`off + 3 > off + 2`.
### `parse_stsd_takes_height_from_its_own_field_not_the_width_beside_it`
`height` is the SECOND of the two 16-bit dimensions in a
VisualSampleEntry, at byte 26 — width sits at 24. Reading the wrong one
is silent: the value is still a plausible dimension, so the track's
seeded resolution simply comes out wrong (1920 read as a height would
classify a 1080p title as UHD). The two are deliberately different here;
a fixture with width == height would pass under either offset.
### `parse_stsd_reads_channelcount_from_its_own_field_and_defaults_a_short_entry`
`channelcount` is at byte 16 of an AudioSampleEntry, after the 8 reserved
bytes that follow `data_reference_index`. An offset slip reads a reserved
field, and reserved fields are conventionally zero — which would make
every audio track come out as 0 channels rather than fail. The
short-entry fallback of 2 is pinned in the same test: an entry with no
room for the fixed part still has to name SOME channel count, and 0 is
not a usable one.
### `parse_stsd_extracts_aac_codec_private_only_when_the_body_is_long_enough`
`mp4a`'s codec_private (the AudioSpecificConfig inside a child `esds`
box) is only read when the entry is `Codec::Aac` AND its body is at
least the 28-byte fixed AudioSampleEntry part — `find_box(&body[28..],
...)` would slice past the end otherwise. No existing test built an
actual `mp4a` entry, so neither half of that guard (`&&`, or the `>= 28`
on its right) was constrained: an `&&`→`||` mutant reaches the same
`&body[28..]` on a codec-only match, and a `>=`→`<` mutant loses a real
title's AAC CodecPrivate outright by only running on entries too short
to have one.
### `parse_stsd_recognises_every_audio_fourcc`
Every recognised audio fourcc must map to its own `Codec`, checked one by
one. `ec-3`/`dtsc`/`dtse`/`dtsh`/`dtsl` had no coverage at all before
this test — a deleted match arm for any of them falls through to the
catch-all `_ => return None`, silently dropping every E-AC-3 or DTS
variant track from the title instead of remuxing it.
### `read_descriptor_len_is_a_four_byte_base_128_varint`
A descriptor length is a base-128 varint: 7 bits per byte, continued
while the top bit is set, to a maximum of FOUR bytes. Every existing
esds fixture uses a single-byte length, so the continuation path is
unconstrained by them — yet a multi-byte length is exactly what an
`esds` carrying a long AudioSpecificConfig (or one written by a tool
that always pads to 4 bytes, which is common) uses.
### `parse_esds_asc_steps_over_every_optional_es_descriptor_field`
The optional `ES_Descriptor` fields (ISO/IEC 14496-1 §7.2.6.5) are
selected by three flag bits, and each one that is set inserts bytes
before the `DecoderConfigDescriptor`. Skipping them wrongly does not
corrupt anything — the tag check fails and `parse_esds_asc` returns
`None`, so the AAC track simply loses its CodecPrivate and the remux
emits AAC no decoder can initialise.
The existing fixture has flags = 0, so all three skips are
unconstrained. This one sets all three at once and still has to reach
the same ASC.
### `find_boxes_capped_refuses_a_box_smaller_than_its_own_header`
A box header is 8 bytes, so a declared `size` below 8 cannot describe a
box — and taking it at face value slices `payload[pos + 8 .. pos +
size]` with the start past the end, which panics. A crafted `moov` is
untrusted input read straight off a user's file.
### `find_boxes_capped_decodes_the_size_field_from_its_own_bytes`
The declared box size is decoded from four specific bytes
(`payload[pos..pos+4]`, big-endian). A size deliberately chosen so byte
1 differs from byte 0 catches an index slip that rereads byte 0 (or any
other wrong offset) instead of the byte the field actually occupies —
with a small size every byte but the last is zero, so such a slip would
go unnoticed.
### `sample_offsets_clamps_an_stsc_run_that_outruns_the_chunk_table`
An `stsc` entry names a `first_chunk` that may exceed the chunk count
the `stco` actually declares (a truncated or crafted table). The run it
would fill has to be clamped to the chunks that exist — indexing `spc`
past its length is a panic on a file the user merely opened.
The clamp is only reachable through a NON-final entry: the final entry's
end is `n_chunks` by construction, so a fixture whose only over-range
entry is last never reaches the line.
### `elst_offset_ticks_honours_the_first_media_edit_not_the_last`
ISO/IEC 14496-12 §8.6.6: an edit list may hold several media edits. This
frame model can only express a constant shift, so it honours the
LEADING one and logs the rest — taking the last instead would shift the
whole track by a trim that belongs to a later segment, i.e. silent A/V
desync of exactly the size of the difference.
### `mvhd_timescale_version_1_reads_past_the_64_bit_times`
A version-1 `mvhd` carries 64-bit creation/modification times, so its
`timescale` sits at byte 20 rather than 12 (ISO/IEC 14496-12 §8.2.2).
Every existing fixture is version 0, so the version-1 offset is
unconstrained by them — and it is not a hypothetical: writers emit
version 1 whenever the movie duration does not fit 32 bits.
A wrong offset reads part of the 64-bit modification time, which is a
large arbitrary number — and the movie timescale is the denominator
that converts an empty edit's delay into media ticks, so the A/V offset
it produces is arbitrary too.
### `parse_elst_entry_count_is_capped`
Only the leading empty edits and the FIRST media edit shape the offset,
so `MAX_ELST_ENTRIES` bounds what a crafted `elst` can allocate. Without
it a box declaring millions of entries — and carrying the bytes for
them, inside a `moov` already capped at 256 MiB — expands to a Vec of
20-byte tuples for no benefit at all.
### `non_av_handler_track_is_dropped_not_folded_into_audio`
A `hdlr` of anything other than `vide`/`soun` (e.g. a hint or subtitle
track) must be dropped, not folded into the audio branch. Before this
guard existed as a literal `&h == b"soun"` comparison, weakening it to
"anything that isn't vide" would silently turn a hint track's `stsd`
entry into a fabricated `AudioStream` — with whatever codec its sample
entry happened to name.
### `per_track_pid_arithmetic_is_exact_past_the_first_track`
The per-track PID is `0x1011 + track_idx` for video and `0x1100 +
track_idx` for audio. At `track_idx == 0` a `+`↔`-`/`*` mutant of either
is invisible (`0x1011 + 0 == 0x1011 - 0 == 0x1011 * 0`… no — `* 0` is 0,
already different — but `+`↔`-` specifically needs `track_idx != 0` to
separate). Four tracks (two video, two audio) push `track_idx` to 1 and
3 respectively, so every one of `+`→`*`, `+`→`-` is forced to disagree
with the correct PID on the second track of its kind.
### `track_idx_advances_past_a_sample_less_track`
A track whose `stsz` is absent (`n == 0`) still occupies a `track_idx`
slot and must still advance the counter for the NEXT track — otherwise
two tracks collide on the same PID, or (worse, if the increment
direction is reversed) `track_idx` underflows a `usize` and panics on
the very first sample-less track.
### `sample_budget_is_shared_and_exhausted_across_tracks`
The global `sample_budget` is DECREMENTED by each track's real sample
count, and it is SHARED across tracks — a track late in the file gets
only what earlier tracks left behind. Build two tracks that both LIE
about their sample count (`stsz` fixed-size, count = `u32::MAX`, as in
`audio_trak_hostile_count`): the file is tiny, so the first track's
clamped count exactly exhausts the whole initial budget, and the second
track — clamped against what's left — must come back with ZERO samples.
An `+=`/`/=` mutant of the decrement either grows the budget or leaves
it nonzero, letting the second track claim samples it must not have.
## `elst_offset_ticks`
Presentation-time offset an edit list imposes on a track's samples, in the
track's MEDIA timescale ticks (ISO/IEC 14496-12 §8.6.5-§8.6.6).
Two constructs cover essentially every real edit list, and both reduce to a
constant shift of the whole track:
* an EMPTY edit (`media_time == -1`) before the media edit, whose
`segment_duration` — in MOVIE timescale ticks — delays presentation;
* a non-empty edit whose `media_time` trims that much media off the front.
So the offset is `(sum of leading empty segment_durations) - media_time`.
A list with several non-empty edits, or a non-empty edit at a rate other
than 1, describes a timeline this frame model cannot express (it would
need samples dropped, reordered or repeated); the leading edit is still
honoured, and the part that is not is LOGGED rather than passed off as a
faithful copy.
## `MAX_ALLOC_BYTES`
The EOF check alone is not enough: `file_len` is cheaply inflatable with a
sparse file (`truncate -s 8G`), so a crafted stsz size or moov box size
just under an 8 GiB apparent length would otherwise force a multi-GiB
allocation. No real sample or moov approaches this.