firmware: mirror ABI v2 — Save + RESET modes, region/speed encodings, HRL/AKE/BUS off=unlock; flip full_unlock default profile

Mirrors freemkv-fw/src/abi.rs; drift-guard pins updated. 310 tests green.
This commit is contained in:
Matthew Jackson
2026-09-16 11:18:44 -07:00
parent eeb36bf389
commit 47bb6a7ac3
4 changed files with 280 additions and 135 deletions
+4 -4
View File
@@ -355,7 +355,7 @@ mod tests {
#[test]
fn arm_on_freemkv_drive_applies_the_recipe() {
use crate::firmware::{
ALL_FEATURES, CDB_FEATURE, CDB_STATE, CDB_VERB, Feature, RESP_MAGIC, STATE_ON,
ALL_FEATURES, CDB_FEATURE, CDB_STATE, CDB_VERB, Feature, RESP_MAGIC, STATE_OFF,
STATE_PASSTHROUGH, Verb, build_set_cdb,
};
// A minimal freemkv drive: IDENTITY→magic, SET updates state, GET reads it.
@@ -402,10 +402,10 @@ mod tests {
};
let u = AacsUnlocker::new(vec![host_cert()]).arm_before_unlock(ArmRecipe::BypassBd);
u.maybe_arm(&mut t).expect("armed");
assert_eq!(t.ake, STATE_ON, "the recipe nulled the AKE");
// IDENTITY, then Set(Ake,on), then a verifying Get(Ake).
assert_eq!(t.ake, STATE_OFF, "the recipe nulled the AKE (Ake=off)");
// IDENTITY, then Set(Ake,off), then a verifying Get(Ake).
assert_eq!(t.cdbs[0][CDB_VERB], Verb::Identity as u8);
assert_eq!(t.cdbs[1], build_set_cdb(Feature::Ake, STATE_ON));
assert_eq!(t.cdbs[1], build_set_cdb(Feature::Ake, STATE_OFF));
assert_eq!(t.cdbs[2][CDB_VERB], Verb::Get as u8);
}
+136 -59
View File
@@ -30,8 +30,9 @@
//! into `cdb[5..9]` and always returns a fixed [`MEMREAD_LEN`]-byte window.
//!
//! Every feature defaults to [`STATE_PASSTHROUGH`] (firmware does not touch the
//! subsystem → byte-identical to OEM); [`Verb::Reset`] returns every feature to
//! passthrough.
//! subsystem → byte-identical to OEM); [`Verb::Reset`] with [`RESET_TO_OEM`]
//! returns every feature to passthrough, while [`RESET_TO_FLASH`] reloads the last
//! [`Verb::Save`]d config block.
use crate::scsi::{DataDirection, ScsiTransport};
@@ -87,25 +88,39 @@ pub const STATE_OFF: u8 = 0x00;
/// Feature state: explicit **on / enabled** (`0x01`).
pub const STATE_ON: u8 = 0x01;
/// [`Feature::Speed`] state: uncapped / maximum read speed (full riplock lift).
/// Any other non-passthrough value is treated as an explicit speed-cap byte.
pub const SPEED_MAX: u8 = 0x01;
/// [`Feature::Speed`] state: speed control OFF — the read-speed cap is lifted and
/// the drive runs uncapped at maximum throughput. This is the [`STATE_OFF`]
/// (`0x00`) leg of Speed: "off" is the *limiter* being off, i.e. maximum speed.
/// `0x01..=0xFE` are explicit caps and `0xFF` is the OEM ramp.
pub const SPEED_MAX: u8 = 0x00;
/// [`Feature::Bd`] state: force-refuse BD (AACS 1.0) discs (`0x02`). A distinct
/// sentinel — NOT [`STATE_OFF`] (`0x00`, the SRAM boot value) — so an unarmed image
/// leaves BD engaged (OEM). Mirrors the firmware ABI `STATE_BD_DISABLE`.
pub const STATE_BD_DISABLE: u8 = 0x02;
/// [`Feature::Region`] state: force BD region A (`0x0A`). See [`BdRegion`]. BD
/// regions A/B/C share the low-nibble block with the DVD `0x0N` scheme
/// ([`REGION_DVD_BASE`]) and [`REGION_FREE`] (`0x0F`).
pub const REGION_BD_A: u8 = 0x0A;
/// [`Feature::Region`] state: force BD region B (`0x0B`).
pub const REGION_BD_B: u8 = 0x0B;
/// [`Feature::Region`] state: force BD region C (`0x0C`).
pub const REGION_BD_C: u8 = 0x0C;
/// [`Feature::Region`] state: force BD region A (`0x2A`). See [`BdRegion`].
pub const REGION_BD_A: u8 = 0x2A;
/// [`Feature::Region`] state: force BD region B (`0x2B`).
pub const REGION_BD_B: u8 = 0x2B;
/// [`Feature::Region`] state: force BD region C (`0x2C`).
pub const REGION_BD_C: u8 = 0x2C;
/// [`Feature::Region`] state base for "force DVD region N": `REGION_DVD_BASE + N`,
/// so regions 1..=8 are `0x01..=0x08`. `0x00` itself is region-locked (nothing
/// plays).
pub const REGION_DVD_BASE: u8 = 0x00;
/// [`Feature::Region`] state base for "force DVD region N": `0x10 + N`, so
/// regions 1..=8 are `0x11..=0x18`.
pub const REGION_DVD_BASE: u8 = 0x10;
/// [`Feature::Region`] state: region-free — any disc plays regardless of its
/// region code. Sits in the region low-nibble block above the DVD (`0x01..=0x08`)
/// and BD (`0x0A..=0x0C`) values.
pub const REGION_FREE: u8 = 0x0F;
/// [`Verb::Reset`] mode (rides in the state slot `cdb[6]`): reload the saved flash
/// config block into the RAM feature-state table, discarding un-saved RAM changes
/// — the non-destructive "revert to last SAVE".
pub const RESET_TO_FLASH: u8 = 0x00;
/// [`Verb::Reset`] mode (rides in the state slot `cdb[6]`): force every feature to
/// [`STATE_PASSTHROUGH`] (OEM behaviour everywhere), regardless of the saved config.
pub const RESET_TO_OEM: u8 = 0xFF;
/// The verb selector in `cdb[4]`. These numeric values ARE the wire protocol.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -117,11 +132,20 @@ pub enum Verb {
Set = 0x02,
/// Read one feature's (`cdb[5]`) current state back in the data-in payload.
Get = 0x03,
/// Restore every feature to [`STATE_PASSTHROUGH`].
/// Restore the RAM feature-state table. The mode rides in `cdb[6]` (the state
/// slot): [`RESET_TO_FLASH`] (`0x00`) reloads the saved flash config block back
/// into RAM (discarding un-saved changes), [`RESET_TO_OEM`] (`0xFF`) forces every
/// feature to [`STATE_PASSTHROUGH`] (out-of-the-box behaviour). Ignores feature.
Reset = 0x04,
/// Diagnostic RAM peek: [`MEMREAD_LEN`] bytes at the 32-bit address in
/// `cdb[5..9]` (big-endian).
DumpAll = 0x09,
/// Persist the whole RAM feature-state table to the flash config block. This is
/// the ONLY verb that writes the config to flash: [`Verb::Set`] and
/// [`Verb::Reset`] touch RAM only, so a host's changes stay volatile until a
/// `Save` commits them (and survive a power cycle only once saved). Ignores
/// feature/state.
Save = 0x0B,
}
/// The feature selector in `cdb[5]` for [`Verb::Set`] / [`Verb::Get`]. These
@@ -129,24 +153,43 @@ pub enum Verb {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Feature {
/// Read-speed / riplock ceiling. `0x01` = max; other = explicit cap byte.
/// Read-speed / riplock ceiling. [`STATE_PASSTHROUGH`] (`0xFF`) = OEM ramp;
/// [`STATE_OFF`] (`0x00`) = speed control off, i.e. the cap is lifted and the
/// drive runs uncapped at maximum throughput (see [`SPEED_MAX`]); `0x01..=0xFE`
/// = an explicit speed cap (the byte IS the cap, lower is slower). Note the OFF
/// direction: "off" means the *limiter* is off, so OFF is the fastest state.
Speed = 0x01,
/// Region (RPC) control. [`STATE_ON`] = region-free; `0x11..=0x18` = force
/// DVD region 1..8; [`REGION_BD_A`]/`_B`/`_C` = force BD region A/B/C.
/// Region (RPC) control. [`STATE_PASSTHROUGH`] (`0xFF`) = drive's own region
/// logic; [`STATE_OFF`] (`0x00`) = region-locked (nothing plays);
/// [`REGION_DVD_BASE`]` + N` (`0x01..=0x08`) = force DVD region 1..8;
/// [`REGION_BD_A`]/`_B`/`_C` (`0x0A`/`0x0B`/`0x0C`) = force BD region A/B/C;
/// [`REGION_FREE`] (`0x0F`) = region-free (any disc plays).
Region = 0x02,
/// UHD (AACS 2.0) capability gate. [`STATE_ON`] = force enabled; [`STATE_OFF`]
/// is a reserved/OEM no-op (only the enable direction is emitted).
/// UHD (AACS 2.0) capability gate. [`STATE_PASSTHROUGH`] (`0xFF`) = OEM (as
/// shipped); [`STATE_OFF`] (`0x00`) = No (refuse UHD discs); [`STATE_ON`]
/// (`0x01`) = Yes (accept UHD — the mode gate is neutralized so the drive
/// engages UHD discs). On the byte-extraction classifier (e.g. BU40N) the No
/// (OFF) leg currently reads back but behaves == OEM (boot-safe no-op, pending
/// deeper RE) — do NOT treat OFF as a proven UHD disable there.
Uhd = 0x03,
/// Blu-ray (AACS 1.0) capability gate. [`STATE_BD_DISABLE`] (`0x02`) =
/// force-refuse BD discs; passthrough/`0x00` boot/[`STATE_ON`] = OEM (engaged —
/// the enable direction is a no-op, OEM already engages BD).
/// Blu-ray (AACS 1.0) capability gate. [`STATE_PASSTHROUGH`] (`0xFF`) = OEM (BD
/// engaged as shipped); [`STATE_OFF`] (`0x00`) = No (force-refuse BD discs — the
/// drive raises its own `6F` refusal sense); [`STATE_ON`] (`0x01`) = Yes (accept
/// BD — an enable direction, not a no-op).
Bd = 0x04,
/// Host Revocation List handling. [`STATE_ON`] = skip lookup (revoked certs
/// accepted, non-destructive).
/// Host Revocation List handling on the cert path. [`STATE_PASSTHROUGH`]
/// (`0xFF`) = OEM enforce; [`STATE_OFF`] (`0x00`) = off (skip the HRL lookup —
/// revoked certs accepted, non-destructive, the unlock direction); [`STATE_ON`]
/// (`0x01`) = on (enforce the HRL).
Hrl = 0x05,
/// Drive-host AKE. [`STATE_ON`] = null (drive acts pre-authenticated).
/// Drive-host AKE. [`STATE_PASSTHROUGH`] (`0xFF`) = OEM real handshake;
/// [`STATE_OFF`] (`0x00`) = off (null/bypass — the drive acts pre-authenticated,
/// the unlock direction); [`STATE_ON`] (`0x01`) = on (require the real handshake).
Ake = 0x06,
/// In-transit AACS bus encryption. [`STATE_ON`] = off (content de-bussed).
/// In-transit AACS bus encryption. [`STATE_PASSTHROUGH`] (`0xFF`) = OEM (bus
/// encryption on); [`STATE_OFF`] (`0x00`) = off (de-bussed — content returned
/// with no bus encryption, the unlock direction); [`STATE_ON`] (`0x01`) = on
/// (bus encryption).
Bus = 0x07,
}
@@ -165,11 +208,11 @@ pub const ALL_FEATURES: [Feature; 7] = [
/// A Blu-ray region for [`FirmwareControl::force_region_bd`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BdRegion {
/// Region A (`0x2A`).
/// Region A (`0x0A`).
A,
/// Region B (`0x2B`).
/// Region B (`0x0B`).
B,
/// Region C (`0x2C`).
/// Region C (`0x0C`).
C,
}
@@ -223,10 +266,20 @@ pub fn build_get_cdb(feature: Feature) -> [u8; CDB_LEN] {
build_cdb(Verb::Get, Some(feature), None, MIN_ALLOC_LEN)
}
/// Build a `RESET` CDB (all features → passthrough). Floors its data-in at
/// [`MIN_ALLOC_LEN`] for the same HW min-transfer reason as [`build_set_cdb`].
pub fn build_reset_cdb() -> [u8; CDB_LEN] {
build_cdb(Verb::Reset, None, None, MIN_ALLOC_LEN)
/// Build a `RESET` CDB. `mode` rides in the state slot (`cdb[6]`):
/// [`RESET_TO_FLASH`] (`0x00`) reloads the saved flash config into RAM,
/// [`RESET_TO_OEM`] (`0xFF`) forces every feature to passthrough. Floors its
/// data-in at [`MIN_ALLOC_LEN`] for the same HW min-transfer reason as
/// [`build_set_cdb`].
pub fn build_reset_cdb(mode: u8) -> [u8; CDB_LEN] {
build_cdb(Verb::Reset, None, Some(mode), MIN_ALLOC_LEN)
}
/// Build a `SAVE` CDB (persist the RAM feature-state table to the flash config
/// block — the only verb that writes config to flash). Floors its data-in at
/// [`MIN_ALLOC_LEN`] for the same HW min-transfer reason as [`build_reset_cdb`].
pub fn build_save_cdb() -> [u8; CDB_LEN] {
build_cdb(Verb::Save, None, None, MIN_ALLOC_LEN)
}
/// Build an `IDENTITY` CDB. `alloc_len` sizes the magic+version+state reply.
@@ -476,10 +529,29 @@ impl<'a> FirmwareControl<'a> {
self.exec_in(&cdb).map(|_| ())
}
/// Restore every feature to [`STATE_PASSTHROUGH`] (RESET). Data-in phase for
/// the same reason as [`set`](Self::set) — a no-data RESET is aborted.
/// Restore every feature to [`STATE_PASSTHROUGH`] (RESET with
/// [`RESET_TO_OEM`]). Data-in phase for the same reason as [`set`](Self::set) —
/// a no-data RESET is aborted.
pub fn reset(&mut self) -> Result<()> {
let cdb = build_reset_cdb();
let cdb = build_reset_cdb(RESET_TO_OEM);
self.exec_in(&cdb).map(|_| ())
}
/// Reload the saved flash config block back into the RAM feature-state table
/// (RESET with [`RESET_TO_FLASH`]), discarding any un-saved RAM changes — the
/// non-destructive "revert to last SAVE". Data-in phase for the same reason as
/// [`reset`](Self::reset).
pub fn reset_to_flash(&mut self) -> Result<()> {
let cdb = build_reset_cdb(RESET_TO_FLASH);
self.exec_in(&cdb).map(|_| ())
}
/// Persist the whole RAM feature-state table to the flash config block (SAVE)
/// — the only verb that writes config to flash, so a host's SET changes survive
/// a power cycle only once saved. Data-in phase for the same reason as
/// [`reset`](Self::reset).
pub fn save(&mut self) -> Result<()> {
let cdb = build_save_cdb();
self.exec_in(&cdb).map(|_| ())
}
@@ -531,27 +603,32 @@ impl<'a> FirmwareControl<'a> {
pub fn enable_bd(&mut self) -> Result<()> {
self.set(Feature::Bd, STATE_ON)
}
/// Force the Blu-ray capability gate off (drive refuses BD discs). Sends
/// [`STATE_BD_DISABLE`] (`0x02`) — a distinct sentinel, NOT `0x00` (the SRAM boot
/// value) — so an unarmed image stays OEM-behaviour-identical (BD engaged).
/// Force the Blu-ray capability gate off (drive refuses BD discs). Sends the
/// uniform [`STATE_OFF`] (`0x00`) — an unarmed image boots to [`STATE_PASSTHROUGH`]
/// (`0xFF`, OEM) via the firmware power-on hook, so `0x00` no longer collides
/// with the boot value (the retired `0x02` sentinel is gone).
pub fn disable_bd(&mut self) -> Result<()> {
self.set(Feature::Bd, STATE_BD_DISABLE)
self.set(Feature::Bd, STATE_OFF)
}
/// Skip the HRL lookup (revoked certs accepted; non-destructive).
/// Skip the HRL lookup (revoked certs accepted; non-destructive). The unlock
/// direction is now [`STATE_OFF`] (`0x00` = HRL enforcement off).
pub fn skip_hrl(&mut self) -> Result<()> {
self.set(Feature::Hrl, STATE_ON)
self.set(Feature::Hrl, STATE_OFF)
}
/// Null the drive-host AKE (drive acts pre-authenticated).
/// Null the drive-host AKE (drive acts pre-authenticated). The unlock direction
/// is now [`STATE_OFF`] (`0x00` = handshake off/bypassed).
pub fn null_ake(&mut self) -> Result<()> {
self.set(Feature::Ake, STATE_ON)
self.set(Feature::Ake, STATE_OFF)
}
/// Turn AACS in-transit bus encryption off (content returned de-bussed).
/// Turn AACS in-transit bus encryption off (content returned de-bussed). The
/// unlock direction is now [`STATE_OFF`] (`0x00` = bus encryption off).
pub fn bus_off(&mut self) -> Result<()> {
self.set(Feature::Bus, STATE_ON)
self.set(Feature::Bus, STATE_OFF)
}
/// Region-free (RPC-1).
/// Region-free (RPC-1). Sends [`REGION_FREE`] (`0x0F`) — under the migrated
/// region scheme `0x01` now means "force DVD region 1", not region-free.
pub fn region_free(&mut self) -> Result<()> {
self.set(Feature::Region, STATE_ON)
self.set(Feature::Region, REGION_FREE)
}
/// Force a specific Blu-ray region.
pub fn force_region_bd(&mut self, region: BdRegion) -> Result<()> {
@@ -580,7 +657,7 @@ impl<'a> FirmwareControl<'a> {
/// host-cert handshake and returns bus-encrypted content, so the host
/// performs the AKE and de-busses. Rips: BD with an otherwise-revoked cert.
pub fn arm_oem_bd(&mut self) -> Result<()> {
self.set_verify(Feature::Hrl, STATE_ON)
self.set_verify(Feature::Hrl, STATE_OFF)
}
/// **arm_oem_uhd** — OEM-style UHD (AACS 2.0) rip with a REAL AKE.
@@ -590,8 +667,8 @@ impl<'a> FirmwareControl<'a> {
/// The host still runs the real AKE. Rips: UHD via the OEM cert path.
pub fn arm_oem_uhd(&mut self) -> Result<()> {
self.set_verify(Feature::Uhd, STATE_ON)?;
self.set_verify(Feature::Hrl, STATE_ON)?;
self.set_verify(Feature::Bus, STATE_ON)
self.set_verify(Feature::Hrl, STATE_OFF)?;
self.set_verify(Feature::Bus, STATE_OFF)
}
/// **arm_bypass_bd** — full-bypass Blu-ray rip (NO host cert needed).
@@ -599,7 +676,7 @@ impl<'a> FirmwareControl<'a> {
/// Sets: `Ake = null` (drive acts pre-authenticated, so a bare VID read
/// returns the volume ID with no cert and no AKE). Rips: BD with no cert.
pub fn arm_bypass_bd(&mut self) -> Result<()> {
self.set_verify(Feature::Ake, STATE_ON)
self.set_verify(Feature::Ake, STATE_OFF)
}
/// **arm_bypass_uhd** — full-bypass UHD rip (NO host cert needed).
@@ -608,8 +685,8 @@ impl<'a> FirmwareControl<'a> {
/// handshake), `Bus = off` (content de-bussed). Rips: UHD with no cert.
pub fn arm_bypass_uhd(&mut self) -> Result<()> {
self.set_verify(Feature::Uhd, STATE_ON)?;
self.set_verify(Feature::Ake, STATE_ON)?;
self.set_verify(Feature::Bus, STATE_ON)
self.set_verify(Feature::Ake, STATE_OFF)?;
self.set_verify(Feature::Bus, STATE_OFF)
}
/// **arm_stealth_oem** — return the drive to byte-for-byte OEM behaviour.
+95 -34
View File
@@ -13,6 +13,7 @@ fn verb_values_match_abi() {
assert_eq!(Verb::Get as u8, 0x03);
assert_eq!(Verb::Reset as u8, 0x04);
assert_eq!(Verb::DumpAll as u8, 0x09);
assert_eq!(Verb::Save as u8, 0x0B);
}
#[test]
@@ -38,10 +39,12 @@ fn state_and_frame_constants_match_abi() {
assert_eq!(STATE_PASSTHROUGH, 0xFF);
assert_eq!(STATE_OFF, 0x00);
assert_eq!(STATE_ON, 0x01);
assert_eq!(SPEED_MAX, 0x01);
assert_eq!(STATE_BD_DISABLE, 0x02);
assert_eq!([REGION_BD_A, REGION_BD_B, REGION_BD_C], [0x2A, 0x2B, 0x2C]);
assert_eq!(REGION_DVD_BASE, 0x10);
assert_eq!(SPEED_MAX, 0x00);
assert_eq!([REGION_BD_A, REGION_BD_B, REGION_BD_C], [0x0A, 0x0B, 0x0C]);
assert_eq!(REGION_DVD_BASE, 0x00);
assert_eq!(REGION_FREE, 0x0F);
assert_eq!(RESET_TO_FLASH, 0x00);
assert_eq!(RESET_TO_OEM, 0xFF);
}
// ── CDB builders ─────────────────────────────────────────────────────────────
@@ -76,16 +79,27 @@ fn vendor_commands_floor_alloc_len_at_min_for_hw() {
|cdb: &[u8; CDB_LEN]| u16::from_be_bytes([cdb[CDB_ALLOC_LEN], cdb[CDB_ALLOC_LEN + 1]]);
assert_eq!(alloc(&build_set_cdb(Feature::Ake, STATE_ON)), MIN_ALLOC_LEN);
assert_eq!(alloc(&build_get_cdb(Feature::Ake)), MIN_ALLOC_LEN);
assert_eq!(alloc(&build_reset_cdb()), MIN_ALLOC_LEN);
assert_eq!(alloc(&build_reset_cdb(RESET_TO_OEM)), MIN_ALLOC_LEN);
assert_eq!(alloc(&build_save_cdb()), MIN_ALLOC_LEN);
}
#[test]
fn build_reset_and_identity_cdb_exact_bytes() {
// RESET floors its data-in at MIN_ALLOC_LEN (0x0040) for the same HW reason.
// RESET carries its mode in the state slot (cdb[6]) and floors its data-in at
// MIN_ALLOC_LEN (0x0040) for the same HW reason: to-OEM = 0xFF, to-flash = 0x00.
assert_eq!(
build_reset_cdb(),
build_reset_cdb(RESET_TO_OEM),
[0x3C, 0x0E, 0xC0, 0xDE, 0x04, 0x00, 0xFF, 0x00, 0x40, 0x00]
);
assert_eq!(
build_reset_cdb(RESET_TO_FLASH),
[0x3C, 0x0E, 0xC0, 0xDE, 0x04, 0x00, 0x00, 0x00, 0x40, 0x00]
);
// SAVE takes no feature/state and floors its data-in at MIN_ALLOC_LEN.
assert_eq!(
build_save_cdb(),
[0x3C, 0x0E, 0xC0, 0xDE, 0x0B, 0x00, 0x00, 0x00, 0x40, 0x00]
);
// IDENTITY with a 64-byte allocation (0x0040 big-endian at cdb[7..9]).
assert_eq!(
build_identity_cdb(MEMREAD_LEN as u16),
@@ -109,9 +123,9 @@ fn build_memread_cdb_packs_address_big_endian_at_5_to_9() {
#[test]
fn region_state_bytes() {
assert_eq!(BdRegion::A.state(), 0x2A);
assert_eq!(BdRegion::B.state(), 0x2B);
assert_eq!(BdRegion::C.state(), 0x2C);
assert_eq!(BdRegion::A.state(), 0x0A);
assert_eq!(BdRegion::B.state(), 0x0B);
assert_eq!(BdRegion::C.state(), 0x0C);
}
// ── Identity / states parsing ────────────────────────────────────────────────
@@ -145,6 +159,9 @@ fn identity_parse_none_without_magic() {
struct FwMock {
version: &'static str,
states: FeatureStates,
/// The saved flash config block: SAVE snapshots `states` into it, RESET
/// with RESET_TO_FLASH reloads it, RESET_TO_OEM forces all-passthrough.
saved: FeatureStates,
is_freemkv: bool,
cdbs: Vec<Vec<u8>>,
}
@@ -154,6 +171,7 @@ impl FwMock {
FwMock {
version: "0.6.6",
states: FeatureStates::all_passthrough(),
saved: FeatureStates::all_passthrough(),
is_freemkv: true,
cdbs: Vec::new(),
}
@@ -220,7 +238,16 @@ impl ScsiTransport for FwMock {
self.states.set(f, cdb[CDB_STATE]);
}
v if v == Verb::Reset as u8 => {
self.states = FeatureStates::all_passthrough();
// The mode rides in the state slot (cdb[6]): to-OEM forces
// all-passthrough, to-flash reloads the saved config block.
self.states = if cdb[CDB_STATE] == RESET_TO_FLASH {
self.saved
} else {
FeatureStates::all_passthrough()
};
}
v if v == Verb::Save as u8 => {
self.saved = self.states;
}
v if v == Verb::DumpAll as u8 => {
for (i, b) in data.iter_mut().enumerate() {
@@ -301,10 +328,41 @@ fn reset_sends_reset_cdb_and_clears_state() {
let mut fw = FirmwareControl::new(&mut m);
fw.reset().expect("reset");
}
assert_eq!(m.cdbs[0], build_reset_cdb());
assert_eq!(m.cdbs[0], build_reset_cdb(RESET_TO_OEM));
assert_eq!(m.states, FeatureStates::all_passthrough());
}
#[test]
fn save_then_reset_to_flash_restores_the_saved_value() {
// SET a feature, SAVE it, force all-passthrough with RESET(to-OEM), then
// RESET(to-flash) must reload the saved value (not passthrough).
let mut m = FwMock::new();
{
let mut fw = FirmwareControl::new(&mut m);
fw.set(Feature::Bus, STATE_OFF).expect("set");
fw.save().expect("save");
fw.reset().expect("reset to OEM");
assert_eq!(fw.get(Feature::Bus).expect("get"), STATE_PASSTHROUGH);
fw.reset_to_flash().expect("reset to flash");
assert_eq!(fw.get(Feature::Bus).expect("get"), STATE_OFF);
}
// The verbs issued, in order: SET, SAVE, RESET, GET, RESET, GET.
assert_eq!(
m.verbs(),
vec![
Verb::Set as u8,
Verb::Save as u8,
Verb::Reset as u8,
Verb::Get as u8,
Verb::Reset as u8,
Verb::Get as u8,
]
);
assert_eq!(m.cdbs[1], build_save_cdb());
assert_eq!(m.cdbs[2], build_reset_cdb(RESET_TO_OEM));
assert_eq!(m.cdbs[4], build_reset_cdb(RESET_TO_FLASH));
}
#[test]
fn states_probes_every_feature_in_order() {
let mut m = FwMock::new();
@@ -355,13 +413,16 @@ fn typed_setters_issue_expected_set_cdbs() {
fw.unlock_speed().unwrap();
}
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Uhd, STATE_ON));
assert_eq!(m.cdbs[1], build_set_cdb(Feature::Bd, STATE_BD_DISABLE));
assert_eq!(m.cdbs[2], build_set_cdb(Feature::Hrl, STATE_ON));
assert_eq!(m.cdbs[3], build_set_cdb(Feature::Ake, STATE_ON));
assert_eq!(m.cdbs[4], build_set_cdb(Feature::Bus, STATE_ON));
assert_eq!(m.cdbs[5], build_set_cdb(Feature::Region, STATE_ON));
assert_eq!(m.cdbs[1], build_set_cdb(Feature::Bd, STATE_OFF));
// The HRL/AKE/BUS unlock direction is now STATE_OFF (0x00), not STATE_ON.
assert_eq!(m.cdbs[2], build_set_cdb(Feature::Hrl, STATE_OFF));
assert_eq!(m.cdbs[3], build_set_cdb(Feature::Ake, STATE_OFF));
assert_eq!(m.cdbs[4], build_set_cdb(Feature::Bus, STATE_OFF));
// Region-free is REGION_FREE (0x0F) — 0x01 now forces DVD region 1.
assert_eq!(m.cdbs[5], build_set_cdb(Feature::Region, REGION_FREE));
assert_eq!(m.cdbs[6], build_set_cdb(Feature::Region, REGION_BD_B));
assert_eq!(m.cdbs[7], build_set_cdb(Feature::Region, 0x12));
// force_region_dvd(2) = REGION_DVD_BASE + 2 = 0x02 under the new base.
assert_eq!(m.cdbs[7], build_set_cdb(Feature::Region, 0x02));
assert_eq!(m.cdbs[8], build_set_cdb(Feature::Speed, SPEED_MAX));
}
@@ -382,10 +443,10 @@ fn arm_oem_bd_sets_and_verifies_hrl_skip() {
let mut fw = FirmwareControl::new(&mut m);
fw.arm_oem_bd().expect("armed");
}
// SET Hrl=ON, then GET Hrl to verify.
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Hrl, STATE_ON));
// SET Hrl=OFF (the migrated skip direction), then GET Hrl to verify.
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Hrl, STATE_OFF));
assert_eq!(m.cdbs[1], build_get_cdb(Feature::Hrl));
assert_eq!(m.states.hrl, STATE_ON);
assert_eq!(m.states.hrl, STATE_OFF);
}
#[test]
@@ -403,11 +464,11 @@ fn arm_oem_uhd_sets_uhd_hrl_bus_each_verified() {
"each set is verified by a get"
);
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Uhd, STATE_ON));
assert_eq!(m.cdbs[2], build_set_cdb(Feature::Hrl, STATE_ON));
assert_eq!(m.cdbs[4], build_set_cdb(Feature::Bus, STATE_ON));
assert_eq!(m.cdbs[2], build_set_cdb(Feature::Hrl, STATE_OFF));
assert_eq!(m.cdbs[4], build_set_cdb(Feature::Bus, STATE_OFF));
assert_eq!(m.states.uhd, STATE_ON);
assert_eq!(m.states.hrl, STATE_ON);
assert_eq!(m.states.bus, STATE_ON);
assert_eq!(m.states.hrl, STATE_OFF);
assert_eq!(m.states.bus, STATE_OFF);
}
#[test]
@@ -417,9 +478,9 @@ fn arm_bypass_bd_nulls_ake() {
let mut fw = FirmwareControl::new(&mut m);
fw.arm_bypass_bd().expect("armed");
}
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Ake, STATE_ON));
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Ake, STATE_OFF));
assert_eq!(m.cdbs[1], build_get_cdb(Feature::Ake));
assert_eq!(m.states.ake, STATE_ON);
assert_eq!(m.states.ake, STATE_OFF);
}
#[test]
@@ -430,11 +491,11 @@ fn arm_bypass_uhd_sets_uhd_ake_bus() {
fw.arm_bypass_uhd().expect("armed");
}
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Uhd, STATE_ON));
assert_eq!(m.cdbs[2], build_set_cdb(Feature::Ake, STATE_ON));
assert_eq!(m.cdbs[4], build_set_cdb(Feature::Bus, STATE_ON));
assert_eq!(m.cdbs[2], build_set_cdb(Feature::Ake, STATE_OFF));
assert_eq!(m.cdbs[4], build_set_cdb(Feature::Bus, STATE_OFF));
assert_eq!(m.states.uhd, STATE_ON);
assert_eq!(m.states.ake, STATE_ON);
assert_eq!(m.states.bus, STATE_ON);
assert_eq!(m.states.ake, STATE_OFF);
assert_eq!(m.states.bus, STATE_OFF);
}
#[test]
@@ -446,7 +507,7 @@ fn arm_stealth_oem_resets_then_verifies_all_passthrough() {
let mut fw = FirmwareControl::new(&mut m);
fw.arm_stealth_oem().expect("disarmed");
}
assert_eq!(m.cdbs[0], build_reset_cdb());
assert_eq!(m.cdbs[0], build_reset_cdb(RESET_TO_OEM));
// then a GET of every feature
assert_eq!(m.cdbs.len(), 1 + ALL_FEATURES.len());
assert!(m.cdbs[1..].iter().all(|c| c[CDB_VERB] == Verb::Get as u8));
@@ -460,7 +521,7 @@ fn arm_by_recipe_enum_dispatches() {
let mut fw = FirmwareControl::new(&mut m);
fw.arm(ArmRecipe::BypassBd).expect("armed");
}
assert_eq!(m.states.ake, STATE_ON);
assert_eq!(m.states.ake, STATE_OFF);
}
#[test]
@@ -495,7 +556,7 @@ fn set_verify_reports_mismatch() {
err,
FirmwareError::VerifyFailed {
feature: Feature::Ake,
wanted: STATE_ON,
wanted: STATE_OFF,
got: STATE_PASSTHROUGH,
}
);
+45 -38
View File
@@ -10,14 +10,16 @@
//! only sequences the vendor commands into an unlock; it reuses the firmware
//! module's CDB builders so the wire framing never drifts.
//!
//! Unlock mapping onto the grammar: region-free = `Set(Region, on)`, riplock
//! lift = `Set(Speed, max)`, the load-bearing transport unlock = `Set(Ake, null)`
//! (drive acts pre-authenticated → a bare `0xAD` returns the VID with no cert
//! and no AKE), and the trailing bus-off = `Set(Bus, off)` (content de-bussed).
//! Unlock mapping onto the grammar: region-free = `Set(Region, free)`
//! (`REGION_FREE`), riplock lift = `Set(Speed, max)` (`SPEED_MAX`), the
//! load-bearing transport unlock = `Set(Ake, off)` (`STATE_OFF` — drive acts
//! pre-authenticated → a bare `0xAD` returns the VID with no cert and no AKE), and
//! the trailing bus-off = `Set(Bus, off)` (`STATE_OFF`, content de-bussed). Under
//! the migrated spec the HRL/AKE/BUS unlock direction is `STATE_OFF`, not `0x01`.
use crate::firmware::{
Feature, MEMREAD_LEN, RESP_MAGIC, SPEED_MAX, STATE_ON, build_identity_cdb, build_memread_cdb,
build_set_cdb,
Feature, MEMREAD_LEN, REGION_FREE, RESP_MAGIC, SPEED_MAX, STATE_OFF, build_identity_cdb,
build_memread_cdb, build_set_cdb,
};
use crate::scsi::{DataDirection, ScsiTransport};
use crate::{UnlockCtx, UnlockError, Unlocked, Unlocker};
@@ -156,9 +158,9 @@ impl FreemkvUnlocker {
}
}
// Full freemkv unlock: IDENTITY (hard gate) → Set(Region,on) → Set(Speed,max)
// (best-effort) → Set(Ake,null) (LOAD-BEARING) → bare 0xAD VID (best-effort)
// → Set(Bus,off) (trailing, best-effort).
// Full freemkv unlock: IDENTITY (hard gate) → Set(Region,free) →
// Set(Speed,max) (best-effort) → Set(Ake,off) (LOAD-BEARING) → bare 0xAD VID
// (best-effort) → Set(Bus,off) (trailing, best-effort).
fn full_unlock(
&self,
scsi: &mut dyn ScsiTransport,
@@ -186,12 +188,13 @@ impl FreemkvUnlocker {
}
};
// Region-free + riplock lift (best-effort features).
best_effort(self.set(scsi, Feature::Region, STATE_ON), "region")?;
best_effort(self.set(scsi, Feature::Region, REGION_FREE), "region")?;
best_effort(self.set(scsi, Feature::Speed, SPEED_MAX), "speed")?;
// Ake = null (LOAD-BEARING): drive acts pre-authenticated, so a bare
// Ake = off/null (LOAD-BEARING): drive acts pre-authenticated, so a bare
// 0xAD returns the VID with no cert and no AKE. No fallback — a firmware
// that rejects it can't do the one-command unlock.
match self.set(scsi, Feature::Ake, STATE_ON) {
// that rejects it can't do the one-command unlock. The migrated spec puts
// the null/bypass direction on STATE_OFF (0x00), not 0x01.
match self.set(scsi, Feature::Ake, STATE_OFF) {
Ok(()) => {}
Err(UnlockError::Transport) => return Err(UnlockError::Transport),
Err(_) => {
@@ -207,10 +210,10 @@ impl FreemkvUnlocker {
// LD/Renesas routes). The null AKE already unlocked the drive, so a VID
// miss must not discard it: only a dead bus propagates (`?`), else `None`.
let vid = crate::vid::read_aacs_vid(scsi)?;
// Trailing Set(Bus, off): remove in-transit bus encryption. FULLY
// best-effort — inert without the lever, and a failure here never
// discards an already-obtained unlock.
if let Err(e) = self.set(scsi, Feature::Bus, STATE_ON) {
// Trailing Set(Bus, off): remove in-transit bus encryption (STATE_OFF is
// the de-bussed direction). FULLY best-effort — inert without the lever,
// and a failure here never discards an already-obtained unlock.
if let Err(e) = self.set(scsi, Feature::Bus, STATE_OFF) {
tracing::debug!(
target: "freemkv::disc",
phase = "freemkv_bus_off_unavailable",
@@ -222,7 +225,7 @@ impl FreemkvUnlocker {
target: "freemkv::disc",
phase = "freemkv_unlocked",
has_vid = vid.is_some(),
"freemkv drive unlocked (null AKE on)"
"freemkv drive unlocked (null AKE — Ake=off)"
);
Ok(Unlocked { vid, bus_key: None })
}
@@ -253,7 +256,8 @@ mod tests {
use super::*;
use crate::DiscKind;
use crate::firmware::{
STATE_ON, build_identity_cdb, build_memread_cdb, build_reset_cdb, build_set_cdb,
REGION_FREE, RESET_TO_OEM, STATE_OFF, build_identity_cdb, build_memread_cdb,
build_reset_cdb, build_set_cdb,
};
use crate::scsi::mock::{MockTransport, Reply};
use crate::scsi::{DataDirection, Result, ScsiResult, ScsiTransport};
@@ -317,21 +321,24 @@ mod tests {
/// (0x0040 at cdb[7..9]) — the drive aborts a sub-16-byte transfer (HW-confirmed).
#[test]
fn unlock_set_cdbs_have_the_new_grammar_shape() {
// Region-free is REGION_FREE (0x0F) at cdb[6] — 0x01 now forces DVD region 1.
assert_eq!(
build_set_cdb(Feature::Region, STATE_ON),
[0x3C, 0x0E, 0xC0, 0xDE, 0x02, 0x02, 0x01, 0x00, 0x40, 0x00]
build_set_cdb(Feature::Region, REGION_FREE),
[0x3C, 0x0E, 0xC0, 0xDE, 0x02, 0x02, 0x0F, 0x00, 0x40, 0x00]
);
// Speed max is SPEED_MAX (0x00) — the limiter-off leg of Speed.
assert_eq!(
build_set_cdb(Feature::Speed, SPEED_MAX),
[0x3C, 0x0E, 0xC0, 0xDE, 0x02, 0x01, 0x01, 0x00, 0x40, 0x00]
[0x3C, 0x0E, 0xC0, 0xDE, 0x02, 0x01, 0x00, 0x00, 0x40, 0x00]
);
// Ake/Bus unlock direction is now STATE_OFF (0x00) at cdb[6], not 0x01.
assert_eq!(
build_set_cdb(Feature::Ake, STATE_OFF),
[0x3C, 0x0E, 0xC0, 0xDE, 0x02, 0x06, 0x00, 0x00, 0x40, 0x00]
);
assert_eq!(
build_set_cdb(Feature::Ake, STATE_ON),
[0x3C, 0x0E, 0xC0, 0xDE, 0x02, 0x06, 0x01, 0x00, 0x40, 0x00]
);
assert_eq!(
build_set_cdb(Feature::Bus, STATE_ON),
[0x3C, 0x0E, 0xC0, 0xDE, 0x02, 0x07, 0x01, 0x00, 0x40, 0x00]
build_set_cdb(Feature::Bus, STATE_OFF),
[0x3C, 0x0E, 0xC0, 0xDE, 0x02, 0x07, 0x00, 0x00, 0x40, 0x00]
);
}
@@ -385,25 +392,25 @@ mod tests {
fn set_region_free_issues_the_set_region_on_cdb() {
let mut t = MockTransport::always(Reply::good(vec![]));
FreemkvUnlocker::new()
.set(&mut t, Feature::Region, STATE_ON)
.set(&mut t, Feature::Region, REGION_FREE)
.expect("ok");
assert_eq!(t.cdbs[0], build_set_cdb(Feature::Region, STATE_ON));
assert_eq!(t.cdbs[0], build_set_cdb(Feature::Region, REGION_FREE));
}
#[test]
fn set_ake_null_issues_the_set_ake_on_cdb() {
let mut t = MockTransport::always(Reply::good(vec![]));
FreemkvUnlocker::new()
.set(&mut t, Feature::Ake, STATE_ON)
.set(&mut t, Feature::Ake, STATE_OFF)
.expect("ok");
assert_eq!(t.cdbs[0], build_set_cdb(Feature::Ake, STATE_ON));
assert_eq!(t.cdbs[0], build_set_cdb(Feature::Ake, STATE_OFF));
}
#[test]
fn set_drive_rejection_is_not_applicable() {
let mut t = MockTransport::always(Reply::illegal_request());
let err = FreemkvUnlocker::new()
.set(&mut t, Feature::Ake, STATE_ON)
.set(&mut t, Feature::Ake, STATE_OFF)
.unwrap_err();
assert_eq!(err, UnlockError::NotApplicable);
}
@@ -444,11 +451,11 @@ mod tests {
assert_eq!(t.cdbs.len(), 6);
// The four vendor SETs land on the right features, in order.
assert_eq!(t.cdbs[0], build_identity_cdb(RESP_LEN as u16));
assert_eq!(t.cdbs[1], build_set_cdb(Feature::Region, STATE_ON));
assert_eq!(t.cdbs[1], build_set_cdb(Feature::Region, REGION_FREE));
assert_eq!(t.cdbs[2], build_set_cdb(Feature::Speed, SPEED_MAX));
assert_eq!(t.cdbs[3], build_set_cdb(Feature::Ake, STATE_ON));
assert_eq!(t.cdbs[3], build_set_cdb(Feature::Ake, STATE_OFF));
assert_eq!(t.cdbs[4][0], crate::scsi::SCSI_READ_DISC_STRUCTURE);
assert_eq!(t.cdbs[5], build_set_cdb(Feature::Bus, STATE_ON));
assert_eq!(t.cdbs[5], build_set_cdb(Feature::Bus, STATE_OFF));
}
/// A missing region/speed feature does not fail the unlock (best-effort).
@@ -572,8 +579,8 @@ mod tests {
#[test]
fn reset_cdb_shape() {
assert_eq!(
build_reset_cdb(),
[0x3C, 0x0E, 0xC0, 0xDE, 0x04, 0x00, 0x00, 0x00, 0x40, 0x00]
build_reset_cdb(RESET_TO_OEM),
[0x3C, 0x0E, 0xC0, 0xDE, 0x04, 0x00, 0xFF, 0x00, 0x40, 0x00]
);
}