Make the FMTS phase gate reachable by tests
An FMTS forensic segment interleaves two variants at the unit level: the disc carries both halves and we hold the key for exactly one parity. Decrypting the alternate half with our key produces garbage; leaving it as ciphertext is correct, because the muxer drops untouched ciphertext cleanly. The decision lived inline in apply_aacs_map's per-unit closure, where no test could reach it. A mutation run flipped the subtraction to an addition and the parity comparison, and every test still passed — so the gate that decides which half of a forensic segment we decrypt was entirely unasserted. It is now unit_is_our_phase(). The index arithmetic also saturates and clamps: both inputs come from the key map, so a unit below its own range start or a zero unit size means a malformed map, and neither may panic on debug overflow or a divide by zero inside a library a long-running service depends on. Four of the five surviving mutants now fail. The fifth, dividing by the unit size versus multiplying, is equivalent rather than uncovered: aligned offsets are exact multiples of the unit size, so the two differ only by a factor of unit_sectors squared, and unit_sectors is the constant 3 — odd, so parity is preserved on every reachable input. The proof is recorded in the test instead of a test that pretends to cover it.
This commit is contained in:
+92
-5
@@ -189,6 +189,34 @@ pub enum Phase {
|
||||
Odd,
|
||||
}
|
||||
|
||||
/// Does this unit belong to the phase we hold the key for?
|
||||
///
|
||||
/// An FMTS forensic segment interleaves two variants at the unit level: the
|
||||
/// disc carries both, and we hold the key for exactly one parity. Decrypting
|
||||
/// the alternate half with our key produces garbage; leaving it as ciphertext
|
||||
/// is correct, because the muxer drops untouched ciphertext cleanly.
|
||||
///
|
||||
/// `Phase::All` means the whole range is ours (the non-forensic case).
|
||||
///
|
||||
/// This lived inline inside `apply_aacs_map`'s per-unit closure, where nothing
|
||||
/// could reach it: a mutation run flipped the `-` to `+` and the `/` to `*` in
|
||||
/// the index arithmetic and every test still passed. Getting either wrong
|
||||
/// silently decrypts the wrong half of a forensic segment.
|
||||
fn unit_is_our_phase(unit_lba: u32, range_start: u32, unit_sectors: u32, phase: Phase) -> bool {
|
||||
let want_odd = match phase {
|
||||
Phase::All => return true,
|
||||
Phase::Even => false,
|
||||
Phase::Odd => true,
|
||||
};
|
||||
// `saturating_sub` and `max(1)`: both inputs come from the key map, which is
|
||||
// built from disc structure. A unit below its own range start, or a zero
|
||||
// unit size, means the map is malformed — that must not panic (debug
|
||||
// overflow / divide-by-zero) inside a library used by a long-running
|
||||
// service. Unit 0 of the range is even, which is the safe default.
|
||||
let unit_ix = unit_lba.saturating_sub(range_start) / unit_sectors.max(1);
|
||||
(unit_ix % 2 == 1) == want_odd
|
||||
}
|
||||
|
||||
/// Proactive AACS key-selection map: which held unit key decrypts each LBA of a
|
||||
/// title's encrypted content, decided ONCE before mux from the disc's CPS-unit
|
||||
/// (and, later, FMTS segment) structure — never by trial-decrypt-and-check per
|
||||
@@ -483,13 +511,9 @@ fn apply_aacs_map(
|
||||
// PHASE GATE (FMTS forensic segment): the segment interleaves two variants
|
||||
// at the unit level. Decrypt ONLY our parity; leave the alternate half as
|
||||
// ciphertext (the muxer drops untouched ciphertext cleanly — no garble).
|
||||
if matches!(phase, Phase::Even | Phase::Odd) {
|
||||
let unit_ix = (unit_lba - range_start) / unit_sectors;
|
||||
let is_odd = unit_ix % 2 == 1;
|
||||
if is_odd != matches!(phase, Phase::Odd) {
|
||||
if !unit_is_our_phase(unit_lba, range_start, unit_sectors, phase) {
|
||||
return; // alternate half — leave as-is
|
||||
}
|
||||
}
|
||||
// Gate on the authoritative encrypted flag ONLY (CPI bits in the clear
|
||||
// seed): a clear unit is left untouched; an encrypted unit is decrypted
|
||||
// with its MAPPED key and trusted.
|
||||
@@ -1767,4 +1791,67 @@ mod tests {
|
||||
"decrypt thread count must not exceed MAX_THREADS ({MAX_THREADS}), got {n}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The FMTS phase gate picks which half of an interleaved forensic segment
|
||||
/// we decrypt. Both the `-` and the `/` in its index arithmetic survived a
|
||||
/// mutation run, and getting either wrong silently decrypts the alternate
|
||||
/// variant into garbage while reporting success.
|
||||
#[test]
|
||||
fn phase_gate_selects_only_our_parity_of_a_forensic_segment() {
|
||||
use super::{Phase, unit_is_our_phase};
|
||||
// A range starting at LBA 30, 3 sectors per aligned unit: units are at
|
||||
// 30, 33, 36, 39, ... with indices 0, 1, 2, 3, ...
|
||||
let ours = |lba, phase| unit_is_our_phase(lba, 30, 3, phase);
|
||||
|
||||
// Even phase takes indices 0, 2, 4 -> LBAs 30, 36, 42.
|
||||
assert!(ours(30, Phase::Even));
|
||||
assert!(!ours(33, Phase::Even));
|
||||
assert!(ours(36, Phase::Even));
|
||||
assert!(!ours(39, Phase::Even));
|
||||
|
||||
// Odd phase is the exact complement.
|
||||
for lba in [30, 33, 36, 39, 42, 45] {
|
||||
assert_ne!(
|
||||
ours(lba, Phase::Even),
|
||||
ours(lba, Phase::Odd),
|
||||
"LBA {lba} must belong to exactly one parity"
|
||||
);
|
||||
}
|
||||
|
||||
// Non-forensic content: the whole range is ours.
|
||||
for lba in [30, 33, 36, 39] {
|
||||
assert!(ours(lba, Phase::All));
|
||||
}
|
||||
|
||||
// The index must be RANGE-RELATIVE: `(lba - start)`, not `(lba + start)`.
|
||||
// Most value pairs give the same parity either way, so pin one where
|
||||
// they genuinely disagree: (5-1)/2 = 2 (even, ours) but
|
||||
// (5+1)/2 = 3 (odd, not ours).
|
||||
assert!(
|
||||
unit_is_our_phase(5, 1, 2, Phase::Even),
|
||||
"the index must be measured from the range start"
|
||||
);
|
||||
|
||||
// The `/ unit_sectors` -> `* unit_sectors` mutant is EQUIVALENT here,
|
||||
// and deliberately not chased. Proof: aligned offsets are exact
|
||||
// multiples of the unit size, so offset = k*u for unit index k.
|
||||
// Dividing gives k; multiplying gives k*u^2, whose parity is
|
||||
// parity(k)*parity(u^2) = parity(k) whenever u is ODD. `unit_sectors`
|
||||
// is `ALIGNED_UNIT_LEN / 2048` = 3, a compile-time constant, so u is
|
||||
// always odd and the two agree on every reachable input. Only an even
|
||||
// unit size would separate them, and none exists.
|
||||
assert!(!unit_is_our_phase(33, 30, 3, Phase::Even));
|
||||
}
|
||||
|
||||
/// A malformed key map must not take down a long-running service. A unit
|
||||
/// below its own range start, or a zero unit size, are both map bugs — they
|
||||
/// must return a defined answer rather than panicking on debug overflow or
|
||||
/// dividing by zero.
|
||||
#[test]
|
||||
fn phase_gate_does_not_panic_on_a_malformed_map() {
|
||||
use super::{Phase, unit_is_our_phase};
|
||||
assert!(unit_is_our_phase(10, 100, 3, Phase::Even));
|
||||
assert!(unit_is_our_phase(100, 30, 0, Phase::Even) || true);
|
||||
assert!(unit_is_our_phase(5, 5, 0, Phase::Even));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user