Fix fifteen defects across perf, resource, panics and key hygiene
All 21 findings held up under verification; 15 fixed here, 6 deferred to files another agent held this round, 0 rejected. **A defect in my own round-2 probe fix.** CHUNK_SECTORS was 1024, and 1024 % 3 == 1 — verified — so every chunk after the first was misaligned against the 6144-byte AACS aligned unit and would be REJECTED by DecryptingSectorSource's alignment gate. On an encrypted disc the forced-subtitle probe I added last round would have read almost nothing past its first chunk. Now 1023 sectors (341 aligned units) with a const assertion that fails the build if it stops dividing, plus set_unit_base per extent so the source's gate is anchored where the extent actually starts. **The same probe skipped sectors on a short read**, advancing by the REQUESTED count rather than the bytes actually returned, so a partial read silently left a gap in the middle of the evidence. It now advances by n/SECTOR_BYTES and clamps n to the buffer. **Its cache key omitted the PGS PID set**, so a playlist declaring an extra subtitle PID got another playlist's verdict for a track that had never been probed. And the key was the whole extent list, so partial clip sharing missed entirely. Both fixed by keying (start_lba, sector_count, pid) — and per-extent keying was shown SOUND rather than assumed: ForcedTracker is two monotone booleans, so per-extent evidence composes by field-wise OR, order- and grouping-independently. Making that honest required per-extent demux state, so an extent's evidence comes only from its own bytes, and memoising only extents whose read reached a designed stop. **A reachable panic in the timeline.** mkvstream::parse_block accepts a TimestampScale up to i64::MAX, so a video frame can set high_ns = i64::MAX and the next passive frame panicked adding the backstep. In release it wrapped negative instead, firing the straggler clamp for essentially every passive frame — audio and subtitles rewritten onto the wrong point of the output timeline. All four sites saturate. **A public constructor divided by zero**: PrefetchedSectorSource::new_with_events with unit_align == 0. Now InvalidInput, matching its batch_sectors sibling. **Two Debug impls printed key material.** DiscInputs (volume_id, mkb, unit_key_ro, samples) and UnitKeyFile both derived Debug. Nothing logs them today — fixed as prevention, because the next tracing::debug! someone adds is the leak. A doc claim that DiscInputs "contains no secrets" was false and is corrected. **An env-var multiply could overflow** in file_sector_source; now bounded at 64 GiB like its writeback sibling, with the parse split out so the bound is testable without touching process env. **The mp4 demuxer allowed one sample per file byte** — ~64x RAM amplification. Now file_len/16, since only vide/soun tracks are indexed and the shortest legal AC-3 frame is 128 bytes. **Two pipeline concurrency defects**: a consumer apply() error was invisible to the producer, and abandon/finalise had a TOCTOU where a caller could report an unfinalised output. Both fixed with compare-exchange state rather than a bool. **Two per-frame copies removed**, both MEASURED rather than reasoned: the AU assembler now hands its allocation to the frame (same pointer, unchanged capacity, proven by asserting the pointer) and tsmux reuses one Annex-B buffer across frames. Both keep capacity deliberately — a naive split_off would have cost more than it saved. **A comment pointed at the wrong file** for a mirrored constant; the mirror is now compiler-enforced with a const assertion converting 90 kHz ticks to ns, so drift fails the build. Deferred to another agent's files, all confirmed: detect_rate's fractional-twin snap, the mp4 reserve's u32 truncation, round_up_grain's overflow, the quadratic base-key gap fill, and MkvStream's frame cap counting frames rather than bytes. Every fix verified red by reverting it. Also noted for later: DecodeSampleSet still derives Debug over multi-MB of on-disc ciphertext.
This commit is contained in:
+65
-3
@@ -82,9 +82,12 @@ impl DecodeSampleSet {
|
||||
}
|
||||
|
||||
/// The public AACS inputs a key source needs to look a disc up. Captured at
|
||||
/// scan; contains no secrets — only the disc identity and the on-disc AACS
|
||||
/// structures a source or key server may key on.
|
||||
#[derive(Debug, Clone)]
|
||||
/// scan; carries no DERIVED secrets (no media key, VUK or plaintext unit key) —
|
||||
/// only the disc identity and the on-disc AACS structures a source or key server
|
||||
/// may key on. The on-disc structures are nonetheless key MATERIAL (the encrypted
|
||||
/// title keys live in `unit_key_ro`), so [`Debug`] is hand-written and redacting;
|
||||
/// see the impl below.
|
||||
#[derive(Clone)]
|
||||
pub struct DiscInputs {
|
||||
/// SHA-1 of `Unit_Key_RO.inf`, `0x`-prefixed hex. The value a keydb keys
|
||||
/// its per-disc entries by, and a key server identifies the disc with.
|
||||
@@ -115,6 +118,30 @@ pub struct DiscInputs {
|
||||
pub volume_label: Option<String>,
|
||||
}
|
||||
|
||||
/// Redacting `Debug`, per the policy `aacs::types` documents (and which
|
||||
/// `aacs::types::Vid` already applies to this very Volume ID). `DiscInputs` is
|
||||
/// public and returned by [`crate::Disc::inputs`], so a consumer's
|
||||
/// `tracing::debug!("{inputs:?}")` used to print the Volume ID, the whole
|
||||
/// `Unit_Key_RO.inf` (the encrypted title keys), the entire MKB and every
|
||||
/// ciphertext sample verbatim into a log that ends up attached to a bug report.
|
||||
/// Only non-secret identity and shape (presence, lengths) is printed.
|
||||
impl std::fmt::Debug for DiscInputs {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("DiscInputs")
|
||||
.field("disc_hash", &self.disc_hash)
|
||||
.field("volume_id", &"<redacted>")
|
||||
.field("version", &self.version)
|
||||
.field("mkb", &"<redacted>")
|
||||
.field("mkb_len", &self.mkb.len())
|
||||
.field("unit_key_ro", &"<redacted>")
|
||||
.field("unit_key_ro_len", &self.unit_key_ro.len())
|
||||
.field("samples", &"<redacted>")
|
||||
.field("samples_len", &self.samples.len())
|
||||
.field("volume_label", &self.volume_label)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A lazy view of a disc's AACS material, handed to [`KeySource::get_unit_keys`] so a
|
||||
/// source can drive the derivation chain without holding the disc reader.
|
||||
///
|
||||
@@ -1334,4 +1361,39 @@ mod tests {
|
||||
assert_eq!(k10[1], [0x10; 16], "V10 reads the 2nd key at +48");
|
||||
assert_ne!(k20[1], k10[1], "the parse stride follows inputs.version");
|
||||
}
|
||||
|
||||
/// `DiscInputs` is public and returned by `Disc::inputs`, so any consumer's
|
||||
/// `tracing::debug!("{inputs:?}")` prints it. A derived `Debug` printed the
|
||||
/// Volume ID (the value `aacs::types::Vid` deliberately renders as
|
||||
/// `Vid(<redacted>)`), the whole `Unit_Key_RO.inf` (the encrypted title keys),
|
||||
/// the entire MKB and every ciphertext sample verbatim. Sentinel byte
|
||||
/// 0xD5 = decimal 213, matching `aacs::types::redaction_tests`. Mutation
|
||||
/// guard: restoring `#[derive(Debug)]` fails this.
|
||||
#[test]
|
||||
fn disc_inputs_debug_is_redacted() {
|
||||
let inputs = DiscInputs {
|
||||
disc_hash: "0xAA".into(),
|
||||
volume_id: [0xD5; 16],
|
||||
version: 2,
|
||||
mkb: vec![0xD5; 64],
|
||||
unit_key_ro: vec![0xD5; 48],
|
||||
samples: vec![vec![0xD5; 6144]],
|
||||
volume_label: Some("TITLE_2024".into()),
|
||||
};
|
||||
let dbg = format!("{inputs:?}");
|
||||
assert!(
|
||||
!dbg.contains("213"),
|
||||
"DiscInputs Debug leaked key material (decimal 213): {dbg}"
|
||||
);
|
||||
assert!(
|
||||
dbg.contains("redacted"),
|
||||
"DiscInputs Debug missing redaction marker: {dbg}"
|
||||
);
|
||||
// Non-secret identity and shape stay printable for diagnostics.
|
||||
assert!(dbg.contains("0xAA"), "{dbg}");
|
||||
assert!(dbg.contains("mkb_len: 64"), "{dbg}");
|
||||
assert!(dbg.contains("unit_key_ro_len: 48"), "{dbg}");
|
||||
assert!(dbg.contains("samples_len: 1"), "{dbg}");
|
||||
assert!(dbg.contains("TITLE_2024"), "{dbg}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user