fix(libfreemkv): rc6 hardening pass — mux timeline/colour/PCR, demux panic sentinel, parser robustness + doc accuracy

Surgical fixes (each with a regression test that fails without the change):

mux/mkv.rs, mux/demux_sink.rs: drive the clip-boundary timeline epoch
off the resolved PRIMARY VIDEO track, not the literal stream index 0.
An M2TS/PMT title can list an audio ES before video, so streams[0] may
be audio; a non-video epoch driver ratchets the frontier and inflates
the timeline. mkv cluster-opening falls back to track 0 for audio-only
titles so they still open clusters.

mux/codec/ac3.rs: correct ACMOD_CHANNELS — acmod=5 (3/1) is 4 channels,
not 3 (was undercounting a 3/1 stream); fix the A/52 Table 5.8 doc.

disc/mod.rs: HDMV coding_type 0x91 (Interactive Graphics / menus) no
longer maps to PGS subtitle — it falls through to Unknown so the PMT/STN
walker drops it instead of surfacing a bogus subtitle track.

mux/videomap.rs + mux/mkv.rs: FVI colour now mirrors the MKV muxer's CICP
precedence (measured CICP authoritative; HDR-driven PQ/HLG transfer
override) via a shared cicp_for_video helper, so the two sinks can't
disagree (HDR10 BT.2020 no longer emits SDR transfer 14).

mux/mkvstream.rs: saturating_add on cluster_ts + rel_ts so an adversarial
CLUSTER_TIMESTAMP near i64::MAX can't overflow/panic before the existing
saturating_mul.

mux/timeline.rs: tighten the tail-straggler clamp so a normal new-epoch
non-video frame leading the sparse video frontier by >3s is not demoted
into the previous clip's epoch.

mux/m2ts_mux/mod.rs: re-stamp PCR per video TS packet (mid-PES), not only
at PES boundaries, so a large UHD I-frame can't open a multi-second PCR
gap; modular 33-bit PTS rebasing so a real 90 kHz clock wrap is not
collapsed to PTS 0 (pre-base frames still floor to 0).

io/byte_prefetcher.rs, sector/prefetched.rs: wrap the producer feed loop
in catch_unwind and emit a typed error sentinel on panic, so a mid-stream
producer panic is not read as a clean EOF at the demux boundary (which
would silently truncate the mux).

mux/codec/h264.rs: extend HIGH_PROFILES to the full ISO/IEC 14496-15 set
that mandates the avcC chroma/bit-depth extension (adds 244 et al.).

Doc/comment accuracy: css/mod.rs (50000 sectors, not scrambled-sectors),
aacs/decrypt.rs (decrypt_unit already-clear path), ifo.rs (TT_SRPT at
0xC4), css/lfsr.rs (LFSR0 24-bit; TAB1-then-XOR cipher; real scramble-flag
predicate), disc/read_error.rs (for_sweep does bounded transient retries).

Skipped: keydb.rs SSRF guard (low/latent, no live caller) — a hard
loopback block breaks an existing behavioral test that exercises the
header-EOF path over a loopback server; a clean fix needs a resolver test
seam beyond this surgical pass. The sibling keydb_fetch.rs comment fix is
out of scope (freemkv crate).
This commit is contained in:
Matthew Jackson
2026-06-25 23:39:03 -07:00
parent dc1d05985b
commit 05729f5dfe
16 changed files with 876 additions and 328 deletions
+22 -1
View File
@@ -815,7 +815,11 @@ fn parse_block(
let rel_ts = i16::from_be_bytes([block[vl], block[vl + 1]]);
let keyframe = block[vl + 2] & 0x80 != 0;
let data = block[vl + 3..].to_vec();
let pts_ticks = cluster_ts_ticks + rel_ts as i64;
// saturating_add: a hostile CLUSTER_TIMESTAMP near i64::MAX plus a positive
// rel_ts would overflow this add (panic in debug/test, wrap to a large
// negative PTS in release) — one operation BEFORE the saturating_mul below.
// rel_ts as i64 is exact, so this fully bounds the sum on adversarial input.
let pts_ticks = cluster_ts_ticks.saturating_add(rel_ts as i64);
let track_idx = (track as usize) - 1; // track >= 1 checked above
// Skip blocks for non-existent tracks.
@@ -1553,6 +1557,23 @@ mod tests {
assert_eq!(f.pts, i64::MAX, "ticks→ns must saturate, not wrap/panic");
}
#[test]
fn parse_block_cluster_ts_plus_rel_ts_saturates_no_overflow() {
// Regression: a hostile CLUSTER_TIMESTAMP near i64::MAX plus a POSITIVE
// rel_ts overflows the `cluster_ts + rel_ts` ADD — one step before the
// saturating_mul. With a plain `+` this panics in debug/test (overflow
// checks on) and silently wraps to a large negative PTS in release.
// rel_ts = +0x7FFF = 32767 (max positive signed 16-bit).
let block = [0x81u8, 0x7F, 0xFF, 0x80, 0xAA];
let f = parse_block(&block, i64::MAX, 1_000_000, 1, None).unwrap();
// The add saturates at i64::MAX, then the mul saturates too.
assert_eq!(
f.pts,
i64::MAX,
"cluster_ts + rel_ts must saturate, not panic/wrap"
);
}
// ============================================================
// ts_pid_for_track — mid-range mapping (the existing test covers the
// edges; this fills in a representative middle value to lock the