libfreemkv: 10-phase release audit fixes (v1.5.2..HEAD)

Multi-round audit of the decrypt/AACS/mux-codec refactor. Fixes, in
descending severity:

- mux/mp4/read.rs: bound untrusted-input allocations. `sample_budget`
  now also capped by file_len (a fixed-size stsz claiming count=u32::MAX
  can't inflate the Vec<SampleRef> past the file's own size); trak scan
  capped at MAX_TRACKS matches; find_box() takes only the first match
  (cap=1) instead of materializing every match. Removes dead find_boxes
  wrapper.
- disc/mod.rs: merge_content_key_ranges now UNIONS same-key overlapping
  ranges (coverage-preserving) instead of dropping the non-overlapping
  tail, which silently left encrypted LBAs uncovered -> ciphertext
  passthrough in the whole-disc sweep/patch map. Different-key overlap
  (malformed) still dropped to keep the set disjoint.
- sector/decrypting.rs: remove dead unit_key_idx field + with_unit_key_idx
  setter (vestigial from the pre-keymap trial-decrypt design; AACS is
  map-only now). Fix stale docs.
- decrypt.rs / resolve.rs / error.rs / extract.rs: doc/comment drift from
  the refactor (AacsKeyMap positive-map semantics, resolve_mux_key_map doc
  reattachment, decrypt_sectors_in_content legacy-alias, E_MP4_INVALID
  meaning, multi-CPS orphan by-design note).

Test coverage (all mutation-verified real):
- DTS NeedMore force-flush buffer bound; FLAC/MPEG-audio PTS carry-forward;
  mp4 mdhd timescale=0 divide-by-zero guard, MAX_TRACKS cap, sample-count
  file_len bound, MAX_ALLOC_BYTES cap under inflated file_len.
- resolve_fmts_key_map: extracted filter_addressable_segments,
  resolve_tie_phase, fill_base_key_gaps as pure behavior-preserving
  helpers, each unit-tested (segment filter, phase-tie arms, gap-fill
  gaplessness over every extent).
This commit is contained in:
Matthew Jackson
2026-07-23 22:09:04 -07:00
parent 8ac18fa631
commit b9568242df
10 changed files with 894 additions and 100 deletions
+15 -15
View File
@@ -201,9 +201,13 @@ pub enum Phase {
/// concern, exactly as for a physically-read clear disc.
///
/// Ranges are `[start_lba, end_lba)` → index into the `Aacs { unit_keys }` pool,
/// sorted and disjoint. `default_idx` covers any LBA no range claims — the
/// single-CPS case is just an empty range list with `default_idx = 0`, so the
/// common disc pays zero lookup cost and needs no structural walk.
/// sorted and disjoint. The map is a POSITIVE list: an LBA in no range is passed
/// through untouched (no default key). How a single-CPS disc is mapped depends on
/// the caller: the whole-disc EXTRACT path uses one blanket range `(0, u32::MAX,
/// 0)` so every encrypted unit — parsed title or orphan clip — resolves to key 0;
/// the per-title MUX/sweep path (`resolve_mux_key_map` → `content_map`) maps only
/// the title's own extents, so an orphan clip outside them is left as pass-through.
/// Either way, clear nav/filesystem sectors (encrypted-flag off) pass through.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AacsKeyMap {
// (start_lba, end_lba, key_idx, phase). An LBA in NO range is passed through
@@ -514,17 +518,13 @@ pub fn decrypt_sectors(
decrypt_sectors_impl(buf, keys, unit_key_idx, None)
}
/// Like [`decrypt_sectors`], but ONLY decrypts/verifies units whose absolute LBA
/// falls inside `content_ranges` — the disc's AACS-encrypted content (the m2ts
/// stream extents). Units OUTSIDE content (UDF filesystem / nav) are left
/// untouched and never counted as decrypt loss: they are clear by definition, so
/// the content-clarity check [`is_clean`](crate::aacs::content::is_clean) must not
/// be consulted about them (a filesystem unit has no TS sync, so it would
/// otherwise be mistaken for ciphertext). `base_lba` is
/// the absolute LBA of `buf`'s first sector; aligned units are 3 sectors.
///
/// `content_ranges` is sorted, merged, disjoint `(start_lba, sector_count)`
/// tuples (each covering `[start_lba, start_lba + sector_count)`).
/// Legacy alias of [`decrypt_sectors`]. Under the keymap-only model AACS decrypts
/// EXCLUSIVELY through the resolved key map (`decrypt_sectors_mapped`), so there is
/// no per-unit content-extent gate here any more: the AACS arm fails loud and the
/// CSS / `None` arm self-gates on its per-sector scramble flag. `base_lba` and
/// `content_ranges` are therefore inert — retained only so the wrapper signature
/// stays stable for the `DecryptingSectorSource` dispatch. Prefer
/// [`decrypt_sectors`] in new code.
pub fn decrypt_sectors_in_content(
buf: &mut [u8],
keys: &mut DecryptKeys,
@@ -607,7 +607,7 @@ mod tests {
v
}
// ── Content-extent gate (`decrypt_sectors_in_content` / `lba_in_ranges`) ──
// ── `decrypt_sectors_in_content` (now a legacy alias of `decrypt_sectors`) ──
/// `DecryptKeys::None` is a no-op even with a content map + scrambled bytes.
#[test]