audit: cap the sparse-PTS reorder buffer, FMTS key state, zero KCD
Round-1 findings from the 10-phase release audit: - SparsePtsReorder buffered its current GOP with no bound, draining only on a keyframe — an open-GOP or crafted program stream that never signals one could hold the whole title in RAM. Force-complete the GOP at MAX_GOP_FRAMES, matching the MPEG-2 parser's backstop. - inject_unit_keys labelled a 2.1 FMTS disc as AACS 1.0 / bus-encryption off; FMTS is UHD-family, so synthesize the UHD version + bus encryption. - The compiled Key Correction Data was a non-zero 16-byte constant fed into the Media Key derivation. Per the no-compiled-keys rule it is now all-zero; the chain still cannot complete on a real disc (documented), so this is behaviour-neutral — all variant tests pass unchanged. - Fix stale doc references (broken `super::variants` intra-doc links, and `aacs::keys` comments) left by the module rename.
This commit is contained in:
@@ -36,6 +36,14 @@ use super::coding::CodingType;
|
||||
/// the timeline across GOPs.
|
||||
const FALLBACK_FRAME_DUR_NS: i64 = 1_001_000_000 / 24;
|
||||
|
||||
/// Force-complete the current GOP once it reaches this many buffered pictures
|
||||
/// even without a keyframe. A GOP is normally a few dozen frames; a stream that
|
||||
/// never signals a keyframe (open-GOP recovery-point coding, or crafted/corrupt
|
||||
/// disc bytes) would otherwise buffer every access unit — the whole title — in
|
||||
/// RAM. Mirrors the MPEG-2 parser's `MAX_PENDING_FRAMES` backstop so no
|
||||
/// reassembly buffer grows unbounded on disc-controlled input.
|
||||
const MAX_GOP_FRAMES: usize = 600;
|
||||
|
||||
/// One buffered coded picture awaiting its GOP's completion.
|
||||
struct Pending {
|
||||
/// Explicit PES PTS (ns) for this AU, or `None` when the source omitted it.
|
||||
@@ -92,9 +100,11 @@ impl SparsePtsReorder {
|
||||
.map(|c| c.coding_type())
|
||||
.unwrap_or(CodingType::P);
|
||||
// A keyframe opens a new GOP: the picture already accumulated in `cur` is
|
||||
// a complete GOP. Complete it (this frame belongs to the NEW GOP).
|
||||
// a complete GOP. Complete it (this frame belongs to the NEW GOP). Also
|
||||
// force-complete a pathologically long run that never signalled a
|
||||
// keyframe, so a crafted/corrupt stream cannot buffer without bound.
|
||||
let mut out = Vec::new();
|
||||
if frame.keyframe && !self.cur.is_empty() {
|
||||
if (frame.keyframe || self.cur.len() >= MAX_GOP_FRAMES) && !self.cur.is_empty() {
|
||||
out = self.complete_current_gop();
|
||||
}
|
||||
self.cur.push(Pending {
|
||||
@@ -296,6 +306,24 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn force_flushes_a_gop_that_never_signals_a_keyframe() {
|
||||
use CodingType::*;
|
||||
// A stream that never flags a keyframe (open-GOP recovery points, or a
|
||||
// crafted/corrupt disc) must not buffer the whole title: the cap
|
||||
// force-completes GOPs so frames are emitted well before flush().
|
||||
let mut r = SparsePtsReorder::new();
|
||||
let mut emitted = 0usize;
|
||||
for i in 0..(MAX_GOP_FRAMES * 3) {
|
||||
let pts = (i == 0).then_some(0);
|
||||
emitted += r.push(pts, frame(P, false)).len();
|
||||
}
|
||||
assert!(
|
||||
emitted >= MAX_GOP_FRAMES,
|
||||
"cap force-flushed GOPs before EOF (emitted {emitted})"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_pts_collisions_within_a_gop() {
|
||||
use CodingType::*;
|
||||
|
||||
Reference in New Issue
Block a user