docs: move drive/firmware internals notes to the private repo; drop their pointers [skip-media]

This commit is contained in:
Matthew Jackson
2026-09-25 16:48:30 -07:00
parent 35cb630af9
commit e51fda9bcd
6 changed files with 41 additions and 56 deletions
+13 -19
View File
@@ -14,7 +14,7 @@ use crate::scsi::{DataDirection, ScsiTransport};
// Issue ONE CSS bus-auth CDB, honouring the transport contract: treats a
// non-zero status as CssAuthFailed and checks bytes_transferred >= min_bytes
// rather than trusting an unfilled buffer. See docs/css-mod.md#css_scsi.
// rather than trusting an unfilled buffer.
fn css_scsi(
scsi: &mut dyn ScsiTransport,
cdb: &[u8],
@@ -202,7 +202,7 @@ impl crate::Unlocker for DvdUnlocker {
fn name(&self) -> &'static str {
// "DVD" names the medium this bus-auth unlocks, not the CSS scheme:
// the bus-auth runs on any DVD regardless of whether the content is
// actually scrambled. See docs/css-mod.md#dvdunlocker-name.
// actually scrambled.
"DVD"
}
@@ -240,7 +240,7 @@ impl crate::Unlocker for DvdUnlocker {
// Probes GET CONFIGURATION current-profile (DVD family 0x0010..=0x001F) so
// DvdUnlocker can self-verify against the drive rather than trust the
// caller's DiscKind. See docs/css-mod.md#mounted_disc_is_dvd.
// caller's DiscKind.
fn mounted_disc_is_dvd(
scsi: &mut dyn ScsiTransport,
) -> std::result::Result<bool, crate::UnlockError> {
@@ -300,7 +300,6 @@ fn unlock_css_reads_inner(scsi: &mut dyn ScsiTransport, _lba: u32) -> Result<()>
tracing::debug!(target: "freemkv::css", agid, "css unlock: bus authentication ok");
// Disc-key REPORT KEY: best-effort only, for firmware that ties part of
// its read-unlock to it; bytes are unused and failure is non-fatal.
// See docs/css-mod.md#disc-key-report-key (7014 bug history).
if let Err(e) = read_disc_key(scsi, agid) {
tracing::debug!(target: "freemkv::css", error_code = e.code(), "css unlock: disc-key REPORT KEY skipped (non-fatal)");
}
@@ -311,7 +310,7 @@ fn unlock_css_reads_inner(scsi: &mut dyn ScsiTransport, _lba: u32) -> Result<()>
// ── Step 1: Bus Authentication ────────────────────────────────────────────
// Runs the CSS bus-auth handshake to set ASF=1 (invalidate AGIDs, allocate,
// challenge/response). Returns the AGID; no bus key is derived. See docs/css-mod.md.
// challenge/response). Returns the AGID; no bus key is derived.
fn establish_authenticated_session(scsi: &mut dyn ScsiTransport) -> Result<u8> {
// Invalidate all AGIDs via REPORT KEY format 0x3F. This is also what makes
// an abandoned AGID self-heal — which is not a reason to abandon one.
@@ -438,7 +437,7 @@ fn authenticate_with_agid(scsi: &mut dyn ScsiTransport, agid: u8) -> Result<()>
// ── Step 2: Disc Key ──────────────────────────────────────────────────────
// Issues READ DVD STRUCTURE format 0x02 (opcode 0xAD) purely for its
// bus-auth side effect; returned bytes are unused. See docs/css-mod.md#read_disc_key.
// bus-auth side effect; returned bytes are unused.
fn read_disc_key(scsi: &mut dyn ScsiTransport, agid: u8) -> Result<()> {
// READ DVD STRUCTURE, format 0x02 (disc key), 2048+4 bytes
let alloc_len: u16 = 2048 + 4;
@@ -464,7 +463,7 @@ fn read_disc_key(scsi: &mut dyn ScsiTransport, agid: u8) -> Result<()> {
fn crypt_key(key_type: usize, variant: u8, challenge: &[u8; 10]) -> [u8; 5] {
// key_type indexes PERM_CHALLENGE ([_;3]); variant indexes VARIANTS/
// PERM_VARIANT ([_;32]). Real (not debug-only) asserts catch an OOB index even
// in release; cost is nil on this once-per-disc path. See docs/css-mod.md.
// in release; cost is nil on this once-per-disc path.
assert!(key_type < 3, "crypt_key: key_type out of range");
assert!((variant as usize) < 32, "crypt_key: variant out of range");
let perm = &PERM_CHALLENGE[key_type];
@@ -620,7 +619,7 @@ mod tests {
// SECURITY REGRESSION GUARD: scans source files for a `tracing` field
// binding a forbidden key name to a value expression (only a string
// literal or `_fp` field is allowed). See docs/css-mod.md#key-guard.
// literal or `_fp` field is allowed).
#[test]
fn no_key_bytes_in_instrumentation() {
use std::path::Path;
@@ -776,7 +775,7 @@ mod tests {
// ── CSS constant-table integrity ───────────────────────────────────────
// Each PERM_CHALLENGE row must be a permutation of indices 0..10; a
// non-permutation would drop/duplicate bytes. See docs/css-mod.md#perm-challenge-rows.
// non-permutation would drop/duplicate bytes.
#[test]
fn perm_challenge_rows_are_permutations() {
for (row, perm) in PERM_CHALLENGE.iter().enumerate() {
@@ -795,7 +794,6 @@ mod tests {
// Each PERM_VARIANT row must map the 32 variants to 32 distinct 5-bit
// values; a collision would make two variants indistinguishable.
// See docs/css-mod.md#perm-variant-rows.
#[test]
fn perm_variant_rows_are_permutations_of_0_31() {
for (row, perm) in PERM_VARIANT.iter().enumerate() {
@@ -816,7 +814,7 @@ mod tests {
// ── crypt_key behaviour ────────────────────────────────────────────────
// crypt_key's result must depend on every challenge byte: flipping any
// single byte must change the output. See docs/css-mod.md#crypt-key-byte-dependence.
// single byte must change the output.
#[test]
fn crypt_key_depends_on_every_challenge_byte() {
let base: [u8; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
@@ -834,7 +832,7 @@ mod tests {
// crypt_key(0, v, ..) must be distinct for each of the 32 variants:
// bus-auth brute-forces the variant by matching against key1, so a
// collision could select the wrong one. See docs/css-mod.md.
// collision could select the wrong one.
#[test]
fn crypt_key_type0_distinct_per_variant() {
let challenge: [u8; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
@@ -851,7 +849,6 @@ mod tests {
// crypt_key's `key_type < 3` debug_assert must fire for key_type 3
// (which would otherwise index PERM_CHALLENGE out of bounds).
// See docs/css-mod.md#crypt-key-preconditions.
#[test]
#[should_panic]
fn crypt_key_rejects_out_of_range_key_type() {
@@ -861,7 +858,6 @@ mod tests {
// crypt_key's `variant < 32` debug_assert must fire for variant 32
// (which would otherwise index VARIANTS/PERM_VARIANT out of bounds).
// See docs/css-mod.md#crypt-key-preconditions.
#[test]
#[should_panic]
fn crypt_key_rejects_out_of_range_variant() {
@@ -895,7 +891,6 @@ mod tests {
// The key-format field is masked to 6 bits so a format with high bits
// set (e.g. 0xFF) cannot corrupt the AGID bits of byte 10.
// See docs/css-mod.md#cdb-builders.
#[test]
fn report_key_cdb_masks_format_to_6_bits() {
let cdb = report_key_cdb(0, 0xFF, 8);
@@ -904,7 +899,7 @@ mod tests {
// send_key_cdb encodes a 12-byte MMC SEND KEY (opcode 0xA3) CDB with the
// parameter-list length at bytes 8-9 (big-endian) and AGID/format at
// byte 10. See docs/css-mod.md#cdb-builders.
// byte 10.
#[test]
fn send_key_cdb_matches_mmc_layout() {
let cdb = send_key_cdb(0b11, 0x03, 0x000C); // AGID=3, format=3, param_len=12
@@ -921,7 +916,6 @@ mod tests {
// Allocation length > 255 must split across bytes 8 (high) and 9 (low)
// as a 16-bit big-endian field, e.g. 0x0804 (the disc-key block size).
// See docs/css-mod.md#cdb-builders.
#[test]
fn report_key_cdb_alloc_len_is_16bit_big_endian() {
let cdb = report_key_cdb(0, 0x00, 0x0804);
@@ -1012,7 +1006,7 @@ mod tests {
// Defect-7 regression: a transport fault on the first probe command
// must abort as Transport, not fall through to NotApplicable (which
// let the consumer keep probing a dead bus). See docs/css-mod.md.
// let the consumer keep probing a dead bus).
#[test]
fn transport_fault_probing_for_a_dvd_aborts() {
let id = DriveId::default();
@@ -1039,7 +1033,7 @@ mod tests {
// Defect-2 regression: a DVD is mounted, then the bus dies mid
// bus-auth — must abort as Transport, not collapse to CssAuthFailed /
// NotApplicable. See docs/css-mod.md.
// NotApplicable.
#[test]
fn transport_fault_during_bus_auth_aborts() {
let id = DriveId::default();
+5 -6
View File
@@ -8,7 +8,7 @@
// `cdb` is only the bdemu emulator's wire format (real unlocking uses profile
// templates), gated behind `emulation`; also compiled under `cfg(test)` so its
// wire-format tests run in default-feature CI. See docs/ld-mod.md — cdb gating.
// wire-format tests run in default-feature CI.
#[cfg(any(feature = "emulation", test))]
mod cdb;
mod error;
@@ -21,7 +21,7 @@ use crate::{DriveId, UnlockCtx, UnlockError, Unlocked, Unlocker};
// ── Public profile catalog ──────────────────────────────────────────────────
// Only the catalog is public (supported-drive lookup, used by bdemu); the
// unlock mechanism stays private. See docs/ld-mod.md — public catalog design.
// unlock mechanism stays private.
pub use profile::{DriveProfile as Profile, Identity, Platform, ProfileMatch, Profiles};
@@ -122,7 +122,7 @@ impl LdUnlocker {
impl LdUnlocker {
// The MediaTek firmware unlock. Since it removes AACS at the drive (clear
// content), this one op satisfies both features and bus-removal, so both
// trait methods delegate here. See docs/ld-mod.md — firmware_unlock contract.
// trait methods delegate here.
fn firmware_unlock(
&self,
scsi: &mut dyn ScsiTransport,
@@ -144,7 +144,7 @@ impl LdUnlocker {
mt.init(scsi)?;
// `init` only proves the handshake completed, not that the drive reached
// extended-access state. Reporting unlocked off `init` alone shipped
// ciphertext at rc=0. See docs/ld-mod.md — half-unlock fallback.
// ciphertext at rc=0.
if !mt.is_unlocked() {
tracing::warn!(
target: "freemkv::disc",
@@ -159,7 +159,7 @@ impl LdUnlocker {
if let Err(e) = mt.probe_disc(scsi) {
// A transport fault here is a dead bus, not a calibration miss — most
// profiles never touch the bus again, so this was the only dead-bus
// signal. See docs/ld-mod.md — probe_disc dead-bus classification.
// signal.
if e.is_transport_failure() {
tracing::warn!(
target: "freemkv::disc",
@@ -432,7 +432,6 @@ mod tests {
// THE probe-disc dead-bus test: bus dies during speed calibration after a
// full unlock; must abort with Transport, not report a successful unlock.
// See docs/ld-mod.md — probe_disc dead-bus test / mutation notes.
#[test]
fn transport_fault_during_probe_disc_is_transport_not_a_successful_unlock() {
let id = known_vid_drive_id();
+13 -15
View File
@@ -127,7 +127,7 @@ impl Mt1959 {
let result = scsi.execute(&cdb, DataDirection::FromDevice, buf, 5_000)?;
// A drive sense arrives as `Ok` with a non-zero status; a merely
// SHORT response is the drive answering badly — neither is a dead
// bus. See docs/mt1959.md — short-probe-not-transport-failure.
// bus.
if result.status != 0 || result.bytes_transferred != expected {
return Err(Error::Scsi {
opcode: SCSI_READ_BUFFER,
@@ -181,7 +181,7 @@ impl Mt1959 {
// `response` is a fixed 64-byte buffer; the meaningful bound is how
// many bytes the drive actually delivered. Require enough bytes to
// validate before checking markers, up front. See docs/mt1959.md — min-unlock-response-length.
// validate before checking markers, up front.
let n = result.bytes_transferred.min(response.len());
if n < MIN_UNLOCK_RESPONSE {
tracing::debug!(
@@ -206,7 +206,6 @@ impl Mt1959 {
// Extended-access state requires the signature match AND both the
// primary marker at [12..16] AND the secondary marker at [16..20].
// See docs/mt1959.md — unlock-marker-gating.
self.unlocked =
response[FIRMWARE_MODE_OFFSET..FIRMWARE_MODE_OFFSET + 4] == FIRMWARE_MODE_SIG;
@@ -282,7 +281,7 @@ impl Mt1959 {
return Err(Error::UnlockFailed);
}
// A dead bus must abort here, not be retried three times as
// firmware reloads. See docs/mt1959.md — run-init-transport-fault.
// firmware reloads.
Err(e) if e.is_transport_failure() => {
tracing::warn!(
target: "freemkv::disc",
@@ -349,7 +348,7 @@ impl Mt1959 {
}
// Detect disc type from capacity to select probe mode (BD vs UHD
// init address). See docs/mt1959.md — probe-init-address-detection.
// init address).
let cap_cdb = [
SCSI_READ_CAPACITY,
0x00,
@@ -368,7 +367,7 @@ impl Mt1959 {
let cap = scsi.execute(&cap_cdb, DataDirection::FromDevice, &mut cap_buf, 5_000)?;
let disc_sectors = if cap.status == 0 && cap.bytes_transferred >= 4 {
// last_lba + 1 = sector count; saturate on the 32-bit-overflow
// sentinel rather than wrap to 0. See docs/mt1959.md — capacity-saturation.
// sentinel rather than wrap to 0.
u32::from_be_bytes([cap_buf[0], cap_buf[1], cap_buf[2], cap_buf[3]]).saturating_add(1)
} else {
0
@@ -413,7 +412,7 @@ impl Mt1959 {
{
// A dead bus MUST keep its transport classification; a
// short/rejected reply is the drive's own failure and stays
// `UnlockFailed`. See docs/mt1959.md — probe-failure-classification.
// `UnlockFailed`.
if e.is_transport_failure() {
return Err(e);
}
@@ -423,7 +422,6 @@ impl Mt1959 {
}
// Pass 2: continue past the coarse range (do not restart at 0).
// See docs/mt1959.md — probe-pass-overlap-fix.
let mut addr: u32 = PROBE_COARSE_END as u32;
while addr < PROBE_FINE_END {
let mut resp = [0u8; PROBE_RESPONSE_SIZE as usize];
@@ -436,7 +434,7 @@ impl Mt1959 {
) {
// Fine probing is best-effort — a short/rejected reply just ends
// the sweep early, but a transport fault must keep its
// classification and abort. See docs/mt1959.md — probe-pass2-transport-fault.
// classification and abort.
if e.is_transport_failure() {
tracing::warn!(
target: "freemkv::disc",
@@ -696,7 +694,7 @@ mod tests {
// THE defect-15 test: a zero-byte response used to skip every marker
// check (each individually `n >= ..` guarded) and report a fully
// unlocked drive. See docs/mt1959.md — defect-15.
// unlocked drive.
#[test]
fn do_unlock_rejects_a_zero_length_response() {
let sig = [0x99, 0x9E, 0xC3, 0x75];
@@ -776,7 +774,7 @@ mod tests {
// THE defect-4 test: a dead bus must abort `run_init` at once, not
// re-upload firmware on all three attempts and return the generic
// `UnlockFailed`. See docs/mt1959.md — defect-4.
// `UnlockFailed`.
#[test]
fn run_init_aborts_on_a_transport_fault_without_reloading_firmware() {
let mut t =
@@ -894,7 +892,7 @@ mod tests {
// THE probe pass-1 dead-bus test: a bus that dies during the coarse
// probe must keep its transport classification so the caller aborts
// with `Transport`. See docs/mt1959.md — pass1-dead-bus-mutation.
// with `Transport`.
#[test]
fn transport_fault_during_pass1_probe_stays_a_transport_failure() {
let mut t = ProbeFaultsTransport { fault_at: 0 };
@@ -909,7 +907,7 @@ mod tests {
// THE probe pass-2 dead-bus test: the loop used to `break` on ANY
// error and return `Ok(())`, swallowing a dead bus mid-pass-2. It
// must now propagate. See docs/mt1959.md — pass2-dead-bus-mutation.
// must now propagate.
#[test]
fn transport_fault_during_pass2_probe_stays_a_transport_failure() {
let mut t = ProbeFaultsTransport {
@@ -952,7 +950,7 @@ mod tests {
// variant_a's firmware-verify-read transport-abort branch: WRITE_BUFFER
// succeeds, the 0x45 verify read faults — a dead bus must abort there,
// not be swallowed into the unlock retries. See docs/mt1959.md — variant-a-verify-read-transport-abort.
// not be swallowed into the unlock retries.
#[test]
fn variant_a_verify_read_transport_fault_aborts() {
let mut profile = fixture_profile([0; 4]);
@@ -1013,7 +1011,7 @@ mod tests {
// variant_b's firmware-upload step (here the metadata read) hitting a
// dead bus must abort via `trace_step`, not be swallowed into the
// unlock retries. See docs/mt1959.md — variant-b-upload-step-transport-abort.
// unlock retries.
#[test]
fn variant_b_upload_step_transport_fault_aborts() {
let mut profile = fixture_profile([0; 4]);
+2 -6
View File
@@ -13,7 +13,6 @@ const FIRMWARE_EXTRA: [u8; 16] = [0; 16];
/// that drive — real profiles must supply their own (see `DriveProfile`).
const VENDOR_VERIFY: [u8; 10] = [0xF1, 0x01, 0x02, 0x00, 0x0D, 0x30, 0x01, 0xF3, 0xAD, 0x23];
// See docs/variant-b-firmware-upload.md — trace_step rationale.
// Swallows everything except a transport fault; these steps are advisory,
// the unlock retries below are the real gate.
fn trace_step(phase: &'static str, r: crate::scsi::Result<crate::scsi::ScsiResult>) -> Result<()> {
@@ -58,7 +57,7 @@ pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Re
return Err(crate::ld::error::Error::UnlockFailed);
}
// Step 1: Upload the firmware via MODE SELECT (see docs/variant-b-firmware-upload.md).
// Step 1: Upload the firmware via MODE SELECT.
// MODE SELECT(10)'s parameter-list length is 16-bit, so reject only a
// blob that can't be expressed in the CDB.
let write_len = firmware.len();
@@ -124,8 +123,6 @@ pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Re
)?;
// Step 4: Vendor verify (0xF1 — B-only, not standard SCSI), per-drive.
// See docs/variant-b-firmware-upload.md for why the outcome is traced
// rather than discarded.
let verify_cdb = mt.profile.fw_verify_cdb.unwrap_or(VENDOR_VERIFY);
let mut dummy = [0u8; 0];
trace_step(
@@ -134,8 +131,7 @@ pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Re
)?;
// Step 5: Unlock retries (up to 5, then a final fatal attempt). The
// confirmation pass after success is intentionally best-effort — see
// docs/variant-b-firmware-upload.md.
// confirmation pass after success is intentionally best-effort.
for _attempt in 0..5 {
match mt.do_unlock(scsi) {
Ok(_) => {
+7 -9
View File
@@ -24,8 +24,7 @@ const RENESAS_MARKER_OFFSET: usize = 16;
/// interface tag. A rejection (CHECK CONDITION or `Err` with a sense) is
/// `Ok(false)`: not a Renesas drive.
///
/// `Err(Transport)` on a dead bus. See docs/renesas-mod.md for why a
/// transport fault must not be folded into `Ok(false)`.
/// `Err(Transport)` on a dead bus.
pub fn is_renesas(scsi: &mut dyn ScsiTransport) -> std::result::Result<bool, UnlockError> {
let mut buf = [0u8; RB_F1_LEN];
match scsi.execute(&RB_F1_CDB, DataDirection::FromDevice, &mut buf, 5_000) {
@@ -71,8 +70,7 @@ impl Renesas {
}
// MakeMKV's vendor "open" sequence: primary read (A), and on refusal a
// knock + second read (B). See docs/renesas-mod.md for the full sequence
// and why we run A→knock→B rather than bailing after A.
// knock + second read (B).
fn vendor_open(scsi: &mut dyn ScsiTransport) -> std::result::Result<bool, UnlockError> {
const RB_B0_04_CDB: [u8; 10] = [0x3C, 0x02, 0xB0, 0x00, 0x00, 0x04, 0x00, 0x00, 0xA4, 0x00];
const KNOCK_A5AAAA_CDB: [u8; 10] =
@@ -128,7 +126,7 @@ impl Unlocker for Renesas {
/// best-effort `0xAD` reader. `Some` once the open succeeds — the drive
/// serves clear content whether or not the bonus VID read did; `None` if it
/// isn't a Renesas drive or the open is refused; `Err(Transport)` on a dead
/// bus. See docs/renesas-mod.md for the MakeMKV-parity rationale.
/// bus.
fn unlock(
&self,
scsi: &mut dyn ScsiTransport,
@@ -188,7 +186,7 @@ mod tests {
}
// Rejects like a MediaTek drive: ILLEGAL REQUEST with a sense, which is
// what distinguishes this from a dead bus. See docs/renesas-mod.md.
// what distinguishes this from a dead bus.
struct RejectingTransport;
impl ScsiTransport for RejectingTransport {
fn execute(
@@ -280,7 +278,7 @@ mod tests {
}
// Same rejection via a CONFORMING transport (`Ok` + CHECK CONDITION);
// must reach the same answer. See docs/renesas-mod.md.
// must reach the same answer.
#[test]
fn check_condition_is_not_a_renesas_drive() {
use crate::scsi::mock::{MockTransport, Reply};
@@ -308,7 +306,7 @@ mod tests {
// A recognized Renesas drive whose A read (RB 0xB0@0x04) is REFUSED but
// whose B read (RB 0xB0@0x50) SUCCEEDS after the knock must unlock — and the
// knock CDB has to be issued BETWEEN A and B. See docs/renesas-mod.md.
// knock CDB has to be issued BETWEEN A and B.
#[test]
fn knock_runs_between_a_and_b_and_b_opens() {
// The exact knock CDB the code issues (pinned wire format).
@@ -395,7 +393,7 @@ mod tests {
}
// A recognized Renesas drive that REFUSES the vendor open read must
// defer to the next unlocker, not claim the drive. See docs/renesas-mod.md.
// defer to the next unlocker, not claim the drive.
#[test]
fn open_rejection_defers_to_next_unlocker() {
struct GateOkOpenRefused;
+1 -1
View File
@@ -8,7 +8,7 @@
//! BEST-EFFORT: only a dead bus is an `Err(Transport)`. A CHECK CONDITION, a
//! short response, or an all-zero VID all yield `Ok(None)` — a VID miss must
//! never discard an unlock that already removed the bus (a key source can still
//! supply the key). See docs/freemkv-abi.md for the fixed-length reply rules.
//! supply the key).
use crate::UnlockError;
use crate::scsi::{DataDirection, ScsiTransport};