v1.0.0-rc.1

CSS keyless decrypt (Stevenson), AACS 1.0/2.0/2.1, MPEG-2 DVD, multi-OS SCSI, multipass recovery, mux highway, audit hardening
This commit is contained in:
Matthew Jackson
2026-06-21 21:06:07 -07:00
parent e8bb6225ac
commit 5941c059c6
53 changed files with 7439 additions and 2340 deletions
+161 -6
View File
@@ -85,9 +85,20 @@ pub struct Drive {
impl Drive {
pub fn open(device: &Path) -> Result<Self> {
let t0 = std::time::Instant::now();
tracing::info!(target: "freemkv::drive", phase = "open", device = %device.display(), "begin");
let mut transport = crate::scsi::open(device)?;
let profiles = profile::load_bundled()?;
let drive_id = DriveId::from_drive(transport.as_mut())?;
tracing::info!(
target: "freemkv::drive",
phase = "open",
device = %device.display(),
vendor = %drive_id.vendor_id.trim(),
product = %drive_id.product_id.trim(),
elapsed_ms = t0.elapsed().as_millis() as u64,
"end"
);
let m = profile::find_by_drive_id(&profiles, &drive_id);
let (driver, platform, profile) = match m {
@@ -220,8 +231,14 @@ impl Drive {
pub fn wait_ready(&mut self) -> Result<()> {
let tur = [SCSI_TEST_UNIT_READY, 0x00, 0x00, 0x00, 0x00, 0x00];
let t0 = std::time::Instant::now();
tracing::info!(target: "freemkv::drive", phase = "wait_ready", "begin");
for _ in 0..60 {
// The poll can take up to 30s (60 × 500ms). Heartbeat it so a slow
// spin-up is visible as steady beats rather than a silent stall.
let mut hb = crate::progress::Heartbeat::new("wait_ready");
for attempt in 0..60u64 {
hb.tick(attempt, 60);
let mut buf = [0u8; 0];
if self
.scsi
@@ -229,10 +246,23 @@ impl Drive {
.execute(&tur, crate::scsi::DataDirection::None, &mut buf, 5_000)
.is_ok()
{
tracing::info!(
target: "freemkv::drive",
phase = "wait_ready",
attempts = attempt + 1,
elapsed_ms = t0.elapsed().as_millis() as u64,
"end"
);
return Ok(());
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
tracing::warn!(
target: "freemkv::drive",
phase = "wait_ready",
elapsed_ms = t0.elapsed().as_millis() as u64,
"device never became ready"
);
Err(Error::DeviceNotReady {
path: self.device_path.clone(),
})
@@ -311,31 +341,108 @@ impl Drive {
&self.device_path
}
/// Current mounted-disc profile from the GET CONFIGURATION header
/// (Current Profile, bytes 6-7). DVD family is `0x0010..=0x001F`, BD
/// family `0x0040..=0x0043`. This is a stock MMC command — it works
/// before (and without) any firmware unlock. `None` if unreadable.
fn current_profile(&mut self) -> Option<u16> {
let cdb = [
crate::scsi::SCSI_GET_CONFIGURATION,
0x00, // RT=0: header carries the Current Profile
0x00,
0x00, // starting feature 0
0x00,
0x00,
0x00,
0x00,
0x08, // allocation length = 8 (header only)
0x00,
];
let mut buf = [0u8; 8];
let r = self
.scsi
.as_mut()
.execute(
&cdb,
crate::scsi::DataDirection::FromDevice,
&mut buf,
5_000,
)
.ok()?;
if r.bytes_transferred >= 8 {
Some(((buf[6] as u16) << 8) | buf[7] as u16)
} else {
None
}
}
/// True when the mounted disc is a DVD (profile family `0x0010..=0x001F`).
fn disc_is_dvd(&mut self) -> bool {
matches!(self.current_profile(), Some(p) if (0x0010..=0x001F).contains(&p))
}
/// Initialize drive — unlock + firmware upload.
/// Optional. Adds features: removes riplock, enables UHD reads, speed control.
///
/// The LibreDrive/OEM firmware unlock is required for BD/UHD (AACS) reads,
/// but it puts the drive in an extended-access state where stock CSS
/// authentication no longer works — so a CSS-protected DVD can't be read.
/// For a DVD we therefore SKIP the unlock and run the drive in its normal
/// stock mode; the DVD path then issues standard CSS commands, which a stock
/// drive honors. BD/UHD and any non-DVD/unknown media keep today's behavior.
pub fn init(&mut self) -> Result<()> {
match self.driver {
let t0 = std::time::Instant::now();
tracing::info!(target: "freemkv::drive", phase = "init", "begin");
if self.disc_is_dvd() {
tracing::info!(target: "freemkv::drive", phase = "init", dvd = true, elapsed_ms = t0.elapsed().as_millis() as u64, "end (stock-mode DVD, no unlock)");
return Ok(());
}
let r = match self.driver {
Some(ref mut d) => d.init(self.scsi.as_mut()),
None => Err(Error::UnsupportedDrive {
vendor_id: self.drive_id.vendor_id.trim().to_string(),
product_id: self.drive_id.product_id.trim().to_string(),
product_revision: self.drive_id.product_revision.trim().to_string(),
}),
}
};
tracing::info!(
target: "freemkv::drive",
phase = "init",
ok = r.is_ok(),
elapsed_ms = t0.elapsed().as_millis() as u64,
"end"
);
r
}
/// Probe disc surface so the drive firmware learns optimal read speeds
/// per region. After this the host reads at max speed and the drive
/// manages zones internally.
pub fn probe_disc(&mut self) -> Result<()> {
match self.driver {
let t0 = std::time::Instant::now();
tracing::info!(target: "freemkv::drive", phase = "probe_disc", "begin");
// A DVD runs in stock mode (see `init`); skip the OEM/firmware-path
// disc calibration, which only applies to the unlocked BD/UHD drive.
if self.disc_is_dvd() {
tracing::info!(target: "freemkv::drive", phase = "probe_disc", dvd = true, elapsed_ms = t0.elapsed().as_millis() as u64, "end (stock-mode DVD, no calibration)");
return Ok(());
}
let r = match self.driver {
Some(ref mut d) => d.probe_disc(self.scsi.as_mut()),
None => Err(Error::UnsupportedDrive {
vendor_id: self.drive_id.vendor_id.trim().to_string(),
product_id: self.drive_id.product_id.trim().to_string(),
product_revision: self.drive_id.product_revision.trim().to_string(),
}),
}
};
tracing::info!(
target: "freemkv::drive",
phase = "probe_disc",
ok = r.is_ok(),
elapsed_ms = t0.elapsed().as_millis() as u64,
"end"
);
r
}
/// Query a specific GET CONFIGURATION feature by code.
@@ -544,7 +651,22 @@ impl Drive {
buf,
timeout_ms,
) {
Ok(result) => Ok(result.bytes_transferred),
Ok(result) if result.bytes_transferred == count as usize * 2048 => {
Ok(result.bytes_transferred)
}
// A READ(10) that completes with GOOD status but a residual
// underrun (bytes_transferred < requested) is a SHORT transfer:
// the tail of `buf` still holds stale bytes from a prior read.
// Committing those as recovered/Good is silent data corruption, so
// treat a short transfer as a failed read — the caller marks the
// range NonTrimmed and retries (a loud miss, never a silent commit).
// The sector/file path enforces the same invariant in
// sector/prefetched.rs; this is the live-drive counterpart.
Ok(_) => Err(Error::DiscRead {
sector: lba as u64,
status: None,
sense: None,
}),
Err(Error::Halted) => Err(Error::Halted),
Err(e) => {
let (status, sense) = extract_scsi_context(&e);
@@ -1026,6 +1148,39 @@ mod command_tests {
));
}
/// `disc_is_dvd()` must match the DVD profile family (0x0010..=0x001F)
/// and ONLY that family. A false positive on a BD/UHD profile (0x0040+)
/// would skip the LibreDrive firmware unlock that UHD reads require; a
/// false negative on a DVD would re-introduce the CSS read failure. The
/// Current Profile is bytes 6-7 of the GET CONFIGURATION header.
/// Mutation: widening the range to `..=0x0040` makes the BD-ROM assert
/// fire; a failed/short GET CONFIGURATION must default to NOT-DVD so the
/// unlock still runs.
#[test]
fn disc_is_dvd_matches_only_dvd_profile_family() {
let probe = |profile: u16| {
let mut hdr = vec![0u8; 8];
hdr[6] = (profile >> 8) as u8;
hdr[7] = profile as u8;
drive_with(hdr).disc_is_dvd()
};
// DVD family → DVD (skip firmware unlock, run stock for CSS).
assert!(probe(0x0010), "DVD-ROM");
assert!(probe(0x0011), "DVD-R");
assert!(probe(0x001B), "DVD+R DL");
// BD/UHD family → NOT DVD (must keep today's unlock path).
assert!(!probe(0x0040), "BD-ROM (UHD) must NOT be classed as DVD");
assert!(!probe(0x0041), "BD-R");
assert!(!probe(0x0008), "CD-ROM");
assert!(!probe(0x0000), "no/unknown profile");
// Short / failed GET CONFIGURATION → no Current Profile → NOT DVD,
// so the firmware unlock still runs (safe default).
assert!(
!drive_with(vec![0u8; 4]).disc_is_dvd(),
"short GET CONFIGURATION must default to not-DVD (unlock still runs)"
);
}
#[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