Constrain five behaviours that mutation testing showed nothing constrained
Fifteen surviving mutants killed, from the highest-risk class: functions a mutant could replace wholesale with a constant while all 2,555 tests passed. None of the code was wrong. In every case a test was absent, which is why eight rounds of reading never found any of them. The one that generalises is in sector/mod.rs. Its existing test READS as covering `read_sectors` on the `&mut dyn SectorSource` forwarding impl — it takes a `&mut dyn`, calls the method, checks the spy. But the receiver auto-derefs and dispatches through the vtable straight to the spy, so the forwarding body is never entered. An earlier round hit this exact trap on `set_unit_base` and fixed it with a generic helper; the read path kept the test that looked right. Verified by stubbing the forwarding impl to Ok(0): the new test fails, the old one passes. That makes a tenth distinct shape of bad test in this audit, and the mutation list is how to find the rest — any forwarding-impl method in it has the same problem. decrypt.rs's two existing gate tests assert only `dropped == 0`, which is precisely what the `Ok(0)` mutant returns; one asserts nothing else at all. A wrapper that decrypts nothing therefore looked correct while the caller muxed scrambled MPEG. Now pinned by descrambling a real CSS sector and comparing against the plaintext it was built from — not against a re-derived descramble, which would only assert the code agrees with itself. css/mod.rs's `is_scrambled_uncracked` turns out to have no production callers at all; the enum is matched directly. Its three tests all assert only the true direction, which is exactly why the `-> true` mutant survived. It is public API, so a consumer routing on it would, under that mutant, refuse to rip every clear DVD. aacs/inf.rs's MKB drive read had no test whatsoever. Now pinned byte-for-byte across multi-pack concatenation, the single-pack case, a genuinely empty response, and error propagation — an unreadable MKB must surface as an error, not as an empty one. aacs/derive.rs's nine mutants are killed with planted MKBs built by inverting the AACS relations, so no real key material is involved. The assertions land on the derived Media Key rather than the intermediate positions: a recovered position that does not actually walk to the planted key is no better than None. A fixture-guard test asserts the planted MKB parses, since an unparseable one would make every `-> None` body look right. 2570 lib tests, debug and release.
This commit is contained in:
+160
@@ -549,3 +549,163 @@ mod vtkf_tests {
|
||||
assert!(dbg.contains("encrypted_keys_len: 2"), "{dbg}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod read_mkb_tests {
|
||||
use super::*;
|
||||
use crate::scsi::{DataDirection, SCSI_READ_DISC_STRUCTURE, ScsiResult, ScsiTransport};
|
||||
|
||||
/// A drive that answers READ DISC STRUCTURE format 0x83 from a scripted set
|
||||
/// of packs and records every CDB it was handed.
|
||||
struct MkbDrive {
|
||||
/// One entry per pack: the pack's MKB payload bytes.
|
||||
packs: Vec<Vec<u8>>,
|
||||
cdbs: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl ScsiTransport for MkbDrive {
|
||||
fn execute(
|
||||
&mut self,
|
||||
cdb: &[u8],
|
||||
_direction: DataDirection,
|
||||
data: &mut [u8],
|
||||
_timeout_ms: u32,
|
||||
) -> crate::error::Result<ScsiResult> {
|
||||
self.cdbs.push(cdb.to_vec());
|
||||
// Pack number is carried in the CDB address field (bytes 2..6),
|
||||
// MMC-6 READ DISC STRUCTURE.
|
||||
let pack = u32::from_be_bytes([cdb[2], cdb[3], cdb[4], cdb[5]]) as usize;
|
||||
let body = self.packs.get(pack).cloned().unwrap_or_default();
|
||||
// Header: BE16 data length (counts the 2 header bytes that follow
|
||||
// it plus the payload), reserved byte, pack count, then payload.
|
||||
let data_len = body.len() + 2;
|
||||
data[0..2].copy_from_slice(&(data_len as u16).to_be_bytes());
|
||||
data[2] = 0x00;
|
||||
data[3] = self.packs.len() as u8;
|
||||
data[4..4 + body.len()].copy_from_slice(&body);
|
||||
Ok(ScsiResult {
|
||||
status: 0,
|
||||
bytes_transferred: 4 + body.len(),
|
||||
sense: [0u8; 32],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// `read_mkb_from_drive` is the in-drive MKB source: every AACS derivation
|
||||
/// downstream (`mkb_find_mk_dv`, the subset-difference walk, the whole
|
||||
/// Media Key ladder) consumes exactly what it returns. An empty return is
|
||||
/// not a benign "no MKB" — it is a total read failure reported as success,
|
||||
/// and every derivation then fails with a key-not-found code that points
|
||||
/// the operator at their keydb rather than at the drive.
|
||||
///
|
||||
/// This pins the CONTENT: the concatenated payload of all packs, in pack
|
||||
/// order, byte for byte.
|
||||
#[test]
|
||||
fn read_mkb_from_drive_returns_the_concatenated_pack_payload() {
|
||||
let pack0: Vec<u8> = (0..600u32).map(|i| (i % 251) as u8).collect();
|
||||
let pack1: Vec<u8> = (0..300u32).map(|i| (i % 253) as u8 ^ 0xA5).collect();
|
||||
let mut drive = MkbDrive {
|
||||
packs: vec![pack0.clone(), pack1.clone()],
|
||||
cdbs: Vec::new(),
|
||||
};
|
||||
|
||||
let mkb = read_mkb_from_drive(&mut drive).expect("scripted drive answers");
|
||||
|
||||
let mut expected = pack0.clone();
|
||||
expected.extend_from_slice(&pack1);
|
||||
assert_eq!(
|
||||
mkb.len(),
|
||||
expected.len(),
|
||||
"every pack's payload must be concatenated, none dropped"
|
||||
);
|
||||
assert!(
|
||||
mkb == expected,
|
||||
"MKB bytes must be the drive's payload in pack order; first \
|
||||
mismatch at {:?}",
|
||||
(0..expected.len()).find(|&i| mkb[i] != expected[i])
|
||||
);
|
||||
|
||||
// MMC-6 READ DISC STRUCTURE with the AACS MKB format code, one command
|
||||
// per pack, pack number in the address field.
|
||||
assert_eq!(drive.cdbs.len(), 2, "one command per declared pack");
|
||||
for (i, cdb) in drive.cdbs.iter().enumerate() {
|
||||
assert_eq!(cdb[0], SCSI_READ_DISC_STRUCTURE, "opcode");
|
||||
assert_eq!(cdb[7], 0x83, "AACS MKB disc-structure format code");
|
||||
assert_eq!(
|
||||
u32::from_be_bytes([cdb[2], cdb[3], cdb[4], cdb[5]]),
|
||||
i as u32,
|
||||
"pack {i} must be requested by number"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A single-pack disc still yields that pack's bytes — the common case, and
|
||||
/// the one where a body returning an empty vector looks most plausible.
|
||||
#[test]
|
||||
fn read_mkb_from_drive_returns_a_single_packs_payload() {
|
||||
let pack: Vec<u8> = (0..1024u32).map(|i| (i * 7 % 256) as u8).collect();
|
||||
let mut drive = MkbDrive {
|
||||
packs: vec![pack.clone()],
|
||||
cdbs: Vec::new(),
|
||||
};
|
||||
let mkb = read_mkb_from_drive(&mut drive).expect("scripted drive answers");
|
||||
assert_eq!(mkb.len(), pack.len(), "single pack payload length");
|
||||
assert!(mkb == pack, "single pack payload bytes");
|
||||
}
|
||||
|
||||
/// A drive that reports a header-only response (`data_len < 2`) has no MKB
|
||||
/// to give. That must be an EMPTY vec, not a partial one — the distinction
|
||||
/// matters because the AACS paths treat a non-empty MKB as parseable.
|
||||
#[test]
|
||||
fn read_mkb_from_drive_empty_response_is_empty() {
|
||||
struct NoMkb;
|
||||
impl ScsiTransport for NoMkb {
|
||||
fn execute(
|
||||
&mut self,
|
||||
_cdb: &[u8],
|
||||
_direction: DataDirection,
|
||||
data: &mut [u8],
|
||||
_timeout_ms: u32,
|
||||
) -> crate::error::Result<ScsiResult> {
|
||||
data[0..2].copy_from_slice(&0u16.to_be_bytes());
|
||||
Ok(ScsiResult {
|
||||
status: 0,
|
||||
bytes_transferred: 4,
|
||||
sense: [0u8; 32],
|
||||
})
|
||||
}
|
||||
}
|
||||
let mkb = read_mkb_from_drive(&mut NoMkb).expect("no-MKB drive still returns Ok");
|
||||
assert!(
|
||||
mkb.is_empty(),
|
||||
"a header-only response carries no MKB bytes"
|
||||
);
|
||||
}
|
||||
|
||||
/// A transport failure on the FIRST pack must propagate as an error — the
|
||||
/// MKB is the root of the whole AACS ladder, so an unreadable one cannot be
|
||||
/// downgraded to "an MKB with no records".
|
||||
#[test]
|
||||
fn read_mkb_from_drive_propagates_the_first_pack_failure() {
|
||||
struct DeadDrive;
|
||||
impl ScsiTransport for DeadDrive {
|
||||
fn execute(
|
||||
&mut self,
|
||||
_cdb: &[u8],
|
||||
_direction: DataDirection,
|
||||
_data: &mut [u8],
|
||||
_timeout_ms: u32,
|
||||
) -> crate::error::Result<ScsiResult> {
|
||||
Err(crate::error::Error::ScsiError {
|
||||
opcode: SCSI_READ_DISC_STRUCTURE,
|
||||
status: 0x02,
|
||||
sense: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
read_mkb_from_drive(&mut DeadDrive).is_err(),
|
||||
"an unreadable MKB must surface as an error, not an empty MKB"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user