Key each FMTS extent from its own CPS unit, and stop a bad ICB tag reading
as an empty directory On an AACS 2.1 disc the non-forensic gap fill hardcoded pool slot 0 as "the" base Unit Key, so every content LBA outside a forensic segment was keyed with CPS unit 1's key even on a disc carrying several CPS units. It does not fail loudly — it produces garbage plaintext. The gap fill now resolves each extent's own base key from its ciphertext, sharing the sampling and slot-picking the multi-CPS path already had rather than adding a second copy, and memoised in the existing per-disc cache. A disc with one base key still short-circuits with zero extra reads, which the existing probe-cost test pins. Forcing the slot back to a constant fails four tests, so the choice is load-bearing rather than incidental. A directory ICB whose descriptor tag is neither File Entry nor Extended File Entry (ECMA-167 4/14.9, 4/14.17) was turned into a successfully-read EMPTY directory, indistinguishable from a genuinely empty one, while the same tag on a file ICB was already a hard error. Fifth instance in this audit of a failure converted into a plausible success value, and the second in this very function — round 5 fixed a read error becoming a file size of zero here. An unrecorded extent (ECMA-167 4/14.14.1.1: allocated but not recorded, logically zeros that still occupy file space) was dropped entirely rather than contributing its length, so every later extent landed at the wrong file offset. Silent corruption, not an error. Extents now carry a recorded flag and the hole emits zeros without touching the media. The Volume Descriptor Sequence was swept at hardcoded sectors 32..64 while the anchor's own Main VDS Extent pointer was parsed into a comment and ignored; ECMA-167 3/10.2.1 defines that extent by the field, not by position, so a conformant volume placing it elsewhere failed to mount. drive_status decoded byte 5 as Media Status without checking the event header's NEA bit or notification class (MMC-6 §6.7), so a reply carrying no media event descriptor decoded as "no disc". The drive is untrusted input here, and this is the works-on-my-drive class. Two more tests were found asserting the defects they sit next to — one requiring unrecorded extents to be dropped, one that four drive fixtures built non-conformant replies the corrected decoder rightly rejects. Both rewritten. Combined with the DTS one in the previous commit that makes three tests this round that locked a bug in as intended behaviour, which is a different and worse failure than the tautological tests found so far: a tautology fails to catch a regression, these actively defend the defect. Not fixed, adjacent: extract_one_file streams extents sequentially and will now READ an unrecorded extent's sectors rather than writing guaranteed zeros. Offsets are right, and pressed media reads as zeros there, but it is not zero-guaranteed the way read_file now is; that needs a recorded flag through PlannedFile.
This commit is contained in:
+125
-28
@@ -324,14 +324,61 @@ impl Drive {
|
||||
0x00,
|
||||
];
|
||||
let mut buf = [0u8; 8];
|
||||
match self.scsi.as_mut().execute(
|
||||
let reply = self.scsi.as_mut().execute(
|
||||
&cdb,
|
||||
crate::scsi::DataDirection::FromDevice,
|
||||
&mut buf,
|
||||
5_000,
|
||||
) {
|
||||
);
|
||||
|
||||
// MMC-6 §6.7: byte 5 is a Media Status only when the Event Header
|
||||
// actually announces a Media Event Descriptor behind it. The header is
|
||||
// Event Descriptor Length (big-endian, bytes 0-1), then byte 2 = NEA
|
||||
// (bit 7) + Notification Class (bits 2-0), then byte 3 = Supported
|
||||
// Event Classes. With NEA set the drive is telling us there is NO event
|
||||
// to report and returns the header alone; with a different Notification
|
||||
// Class the descriptor that follows is not a media one and its second
|
||||
// byte means something else entirely.
|
||||
//
|
||||
// Decoding byte 5 unconditionally turns both of those into Media Status
|
||||
// 0 == "tray closed, no disc" — a drive with a disc loaded reported as
|
||||
// empty, from a reply that said nothing about the media at all. The
|
||||
// drive is untrusted input here, so an event-less or foreign-class reply
|
||||
// yields no media state and the TEST UNIT READY fallback answers
|
||||
// instead.
|
||||
const NEA: u8 = 0x80;
|
||||
const NOTIFICATION_CLASS_MASK: u8 = 0x07;
|
||||
const NOTIFICATION_CLASS_MEDIA: u8 = 0x04;
|
||||
// Bytes 2..7: the 2 remaining header bytes plus the 4-byte Media Event
|
||||
// Descriptor — the shortest reply in which byte 5 exists and is a Media
|
||||
// Status.
|
||||
const MIN_DESCRIPTOR_LENGTH: u16 = 6;
|
||||
|
||||
let media_status = match reply {
|
||||
Ok(r) if r.bytes_transferred >= 6 => {
|
||||
let media_status = buf[5];
|
||||
let descriptor_len = u16::from_be_bytes([buf[0], buf[1]]);
|
||||
let class = buf[2] & NOTIFICATION_CLASS_MASK;
|
||||
if buf[2] & NEA == 0
|
||||
&& class == NOTIFICATION_CLASS_MEDIA
|
||||
&& descriptor_len >= MIN_DESCRIPTOR_LENGTH
|
||||
{
|
||||
Some(buf[5])
|
||||
} else {
|
||||
tracing::debug!(
|
||||
target: "freemkv::drive",
|
||||
nea = buf[2] & NEA != 0,
|
||||
class,
|
||||
descriptor_len,
|
||||
"get event status carried no media event descriptor"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
match media_status {
|
||||
Some(media_status) => {
|
||||
// Bits 1-0: door/tray state
|
||||
// Bit 1: media present, Bit 0: tray open
|
||||
match media_status & 0x03 {
|
||||
@@ -346,7 +393,7 @@ impl Drive {
|
||||
_ => DriveStatus::Unknown,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
None => {
|
||||
// Fallback: try TUR
|
||||
let tur = [SCSI_TEST_UNIT_READY, 0x00, 0x00, 0x00, 0x00, 0x00];
|
||||
let mut empty = [0u8; 0];
|
||||
@@ -1569,25 +1616,83 @@ mod command_tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// A conformant GET EVENT STATUS NOTIFICATION reply carrying one Media
|
||||
/// Event Descriptor (MMC-6 §6.7): Event Header — Event Descriptor Length
|
||||
/// (big-endian, bytes 0-1), then NEA (bit 7) + Notification Class (bits
|
||||
/// 2-0) in byte 2 and the Supported Event Class bitmap in byte 3 — followed
|
||||
/// by the 4-byte Media Event Descriptor whose byte 1 (reply byte 5) is the
|
||||
/// Media Status.
|
||||
fn media_event_reply(media_status: u8) -> Vec<u8> {
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[0..2].copy_from_slice(&6u16.to_be_bytes()); // 6 bytes follow
|
||||
buf[2] = 0x04; // NEA = 0, Notification Class 4 = Media
|
||||
buf[3] = 0x10; // Supported Event Classes: media
|
||||
buf[4] = 0x00; // Event Code: NoChg
|
||||
buf[5] = media_status;
|
||||
buf
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drive_status_tray_open_and_media_present_is_not_ready_to_rip() {
|
||||
// GET EVENT STATUS reply: byte 5 (media_status) low bits = 0b11
|
||||
// (tray-open AND media-present, contradictory). Must NOT report
|
||||
// DiscPresent. Buffer is 8 bytes; bytes_transferred >= 6.
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[5] = 0x03;
|
||||
let mut d = drive_with(buf);
|
||||
// Media Status low bits = 0b11 (tray-open AND media-present,
|
||||
// contradictory). Must NOT report DiscPresent.
|
||||
let mut d = drive_with(media_event_reply(0x03));
|
||||
assert_eq!(d.drive_status(), DriveStatus::TrayOpen);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drive_status_disc_present_maps_correctly() {
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[5] = 0x02; // media present, tray closed
|
||||
let mut d = drive_with(buf);
|
||||
// Media Status 0x02 = media present, tray closed.
|
||||
let mut d = drive_with(media_event_reply(0x02));
|
||||
assert_eq!(d.drive_status(), DriveStatus::DiscPresent);
|
||||
}
|
||||
|
||||
/// MMC-6 §6.7: byte 5 of the reply is a Media Status ONLY when the Event
|
||||
/// Header says a media event descriptor follows — NEA (byte 2 bit 7) clear
|
||||
/// AND Notification Class (byte 2 bits 2-0) == 4 (Media). A drive that
|
||||
/// answers with NEA set, or with a different class it chose to report, still
|
||||
/// returns 8 bytes; decoding byte 5 regardless reads a reserved/zero byte as
|
||||
/// Media Status 0 and reports NoDisc on a drive that has a disc loaded —
|
||||
/// the classic "works on my drive, not theirs" firmware split. The drive is
|
||||
/// untrusted input: an event-less reply carries no media state at all, so
|
||||
/// the status must come from the TEST UNIT READY fallback instead.
|
||||
///
|
||||
/// This mock answers every command (including the fallback TUR) with
|
||||
/// success, so the fallback's verdict is `DiscPresent` — the point is that
|
||||
/// it is NOT the fabricated `NoDisc`.
|
||||
#[test]
|
||||
fn drive_status_rejects_a_reply_carrying_no_media_event_descriptor() {
|
||||
// NEA = 1: "No Event Available" — no descriptor was returned, so the
|
||||
// bytes after the header are not a Media Event Descriptor.
|
||||
let mut nea = media_event_reply(0x00);
|
||||
nea[2] = 0x80 | 0x04;
|
||||
let mut d = drive_with(nea);
|
||||
assert_ne!(
|
||||
d.drive_status(),
|
||||
DriveStatus::NoDisc,
|
||||
"NEA=1 means no event descriptor — byte 5 is not a Media Status"
|
||||
);
|
||||
assert_eq!(d.drive_status(), DriveStatus::DiscPresent);
|
||||
|
||||
// Notification Class 1 (Operational Change), not 4 (Media): a real
|
||||
// descriptor, but of a class whose byte 5 means something else.
|
||||
let mut other_class = media_event_reply(0x00);
|
||||
other_class[2] = 0x01;
|
||||
let mut d = drive_with(other_class);
|
||||
assert_ne!(
|
||||
d.drive_status(),
|
||||
DriveStatus::NoDisc,
|
||||
"a non-Media notification class carries no media status"
|
||||
);
|
||||
assert_eq!(d.drive_status(), DriveStatus::DiscPresent);
|
||||
|
||||
// Control: the same 8 bytes WITH a valid media event header really do
|
||||
// decode Media Status 0 as NoDisc, so the two asserts above are about
|
||||
// the header and not about byte 5.
|
||||
let mut d = drive_with(media_event_reply(0x00));
|
||||
assert_eq!(d.drive_status(), DriveStatus::NoDisc);
|
||||
}
|
||||
|
||||
// ── Mocks for Drive::read single-shot semantics + CDB encoding ──
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
@@ -2009,9 +2114,7 @@ mod command_tests {
|
||||
/// 0x00 = NoDisc, etc. Stands in for a real opened drive so the
|
||||
/// selection policy is testable without hardware.
|
||||
fn drive_with_media_byte(media_status: u8) -> Drive {
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[5] = media_status;
|
||||
drive_with(buf)
|
||||
drive_with(media_event_reply(media_status))
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2056,29 +2159,23 @@ mod command_tests {
|
||||
#[test]
|
||||
fn drive_status_no_disc_maps_correctly() {
|
||||
// media_status low bits 0b00 = tray closed, no disc.
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[5] = 0x00;
|
||||
let mut d = drive_with(buf);
|
||||
let mut d = drive_with(media_event_reply(0x00));
|
||||
assert_eq!(d.drive_status(), DriveStatus::NoDisc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drive_status_tray_open_maps_correctly() {
|
||||
// media_status low bits 0b01 = tray open, no media.
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[5] = 0x01;
|
||||
let mut d = drive_with(buf);
|
||||
let mut d = drive_with(media_event_reply(0x01));
|
||||
assert_eq!(d.drive_status(), DriveStatus::TrayOpen);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drive_status_high_bits_in_media_status_ignored() {
|
||||
// Only the low 2 bits of byte 5 are the door/media state; upper
|
||||
// bits (NEA, etc.) must be masked. 0xFE has low bits 0b10 =
|
||||
// DiscPresent.
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[5] = 0xFE;
|
||||
let mut d = drive_with(buf);
|
||||
// MMC-6 §6.7: only the low 2 bits of the Media Event Descriptor's
|
||||
// Media Status are the door/media state; the reserved upper bits must
|
||||
// be masked. 0xFE has low bits 0b10 = DiscPresent.
|
||||
let mut d = drive_with(media_event_reply(0xFE));
|
||||
assert_eq!(d.drive_status(), DriveStatus::DiscPresent);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user