Files
craft-libs/crates
Arshiya Rahgozar 1e4271e369 media: bound Matroska resync work and header reads (#457)
* media: bound Matroska resync work and header reads

What was wrong: the Matroska demuxer did no bounded work when recovering from
damage. `resync` tested every plausible-looking element ID, and each test could
refill the 64 KiB read window at a far offset, so a damaged region cost about
13,000x read amplification (a 1 MB crafted file read 4.3 GB). The EBML header
was read with no size cap, so a header as large as the file read the whole file
into memory. The warnings list grew without limit, and block and lace sizes
were narrowed to u32 with `as`, so a block over 4 GiB was indexed with a
truncated size.

What changed: damage recovery now has a 64 MiB per-file budget, charged by the
bytes actually read to refill the window; the scan stops when it is spent and
a warning says so. The EBML header is limited to 4 KiB (real ones are a few
dozen bytes). At most 256 warnings are kept, plus one saying the rest were
dropped. Block and lace sizes use checked conversion; a block that does not fit
is skipped like other damaged blocks.

How it was tested: four new tests in synthetic_tests.rs failed before the fix
and pass after it: a counting ByteSource over dense fake Clusters (reads stay
within the budget), an EBML header as large as the file (quick NotMatroska
error, small reads), a SeekHead with thousands of bad entries (warnings
capped), and a sparse source with a block over 4 GiB (not indexed). The rest of
the matroska tests still pass, and clippy is clean with -D warnings.

This carries over the intent of storytold/filmcraft#2951 (a recovery-work
budget for Matroska) now that the demuxer lives in craft-libs, and adds the
header, warning and size limits from the audit.

Completes the Matroska part of storytold/filmcraft#2794

* media: cap laced Matroska block reads and give each index pass its own recovery budget

Review follow-ups to the Matroska recovery budget.

A laced block's whole body was read into memory before any size check, so a
block declaring 4 GiB and carrying a lacing flag allocated that much. Laced
blocks hold small frames, so a body over 16 MiB is now skipped like other
damaged blocks, without reading it.

The recovery budget lived in the read cache, which open and build_index share,
so open(index: false) followed by build_index() charged the same damaged
region twice and could drop later clusters. Each pass over the file (open_impl)
now starts with the whole budget, and packet reading starts with its own.

The warnings test now pins the exact count (256 kept plus the note) and the
note's text.

Tested: a sparse file with each lacing flag fails to open before and opens with
no samples after; a file whose pre-cluster damage costs about 50 MiB of recovery
reads loses its cluster in the second pass before and keeps it after. The
matroska unit tests pass, clippy -D warnings and fmt are clean.

* media: restore the packet-reading recovery budget after build_index

build_index runs a full pass over the file on the shared read cache, so it
spent the recovery budget that next_packet then relied on. After
open(index: false) and build_index(), reading stopped at a damaged region the
index had recovered across, ending playback before the next cluster.

build_index now gives packet reading a whole budget again once its own pass is
done.

Tested: a file with a cluster, about 40 MiB of recovery reads of damage, then a
second cluster; after open(index: false) and build_index() the index held both
clusters but reading ended after the first before the change, and returns both
after it.
2026-11-11 07:11:34 -05:00
..
2026-11-05 10:29:52 +01:00