Compare commits

..
Author SHA1 Message Date
Matthew Jackson 93d9517964 merge: record v1.7.2 release into dev (-s ours; keep path deps + dev's next-cycle work) 2026-09-18 21:39:15 -07:00
Matthew Jackson 64a53b8349 firmware: mirror fw 0.8.3 RESET ABI — RESET_TO_DEFAULTS (0x01) + reset_to_defaults(), true-OEM/marker-gated docs 2026-09-18 21:23:07 -07:00
Matthew Jackson f434298e0d v1.7.2: bump version (unified release) 2026-09-18 18:37:18 +00:00
Matthew Jackson a18bc90164 freemkv: source the SET data-in buffer from MIN_ALLOC_LEN (single source for the transfer length) 2026-09-18 09:52:27 -07:00
Matthew Jackson 79f3324488 changelog: 1.7.2 — record the SET data-phase unlock fix + READ CAPACITY/empty-ISO hardening 2026-09-18 06:54:58 -07:00
Matthew Jackson 0842babe2d freemkv: SET must issue the data-in phase the firmware requires
FreemkvUnlocker::set sent the vendor SET with DataDirection::None + an
empty buffer, but the SET CDB advertises a MIN_ALLOC_LEN (64B) data-in
allocation and the drive ABORTS a no-data transfer (CHECK CONDITION /
Aborted Command — HW-confirmed on BU40N fw 0.8.1). Every Set(feature)
failed, so the load-bearing Set(Ake,off) failed and the whole freemkv
unlock aborted, silently falling back to the AACS/online key route
(autorip showed 'Unlockers — freemkv: no'). Mirror the working
FirmwareControl::set (exec_in): read the state table back over a
FromDevice phase and ignore it. Adds a regression test modelling the
drive's abort-without-data-phase contract.
2026-09-18 06:19:49 -07:00
4 changed files with 102 additions and 16 deletions
+10
View File
@@ -1,7 +1,17 @@
# Changelog
## [1.7.3] — UNRELEASED
### Changed
- firmware: mirror the fw 0.8.3 RESET ABI — added `RESET_TO_DEFAULTS` (`0x01`, restore the baked create-time defaults into RAM) and `FirmwareControl::reset_to_defaults()`; `RESET_TO_OEM` (`0xFF`) is now documented as TRUE OEM (RAM passthrough **and** the NV block blanked to all-`0xFF`, so the drive is traceless), and `RESET_TO_FLASH` as marker-gated (a never-saved drive loads the defaults). Wire values unchanged.
## [1.7.2] — UNRELEASED
### Fixed
- `FreemkvUnlocker::set` now issues the vendor SET over the required 64-byte data-in phase (mirroring `FirmwareControl::set`). freemkv firmware 0.8.x aborts a SET sent with no data phase (CHECK CONDITION / Aborted Command), so every drive-feature SET failed — including the load-bearing `Set(Ake, off)` — and the freemkv firmware unlock silently fell back to the AACS/online-key route. Drives on freemkv firmware now unlock via the freemkv route again (hardware-confirmed on BU40N fw 0.8.1).
### Changed
- firmware: mirror ABI v2 — `Save` + `RESET`-to-flash/OEM modes, the region/speed value encodings, and the HRL/AKE/BUS `off = unlock` (`0x00`) polarity; `full_unlock`'s default profile flipped to match. Aligns `FirmwareControl` with freemkv firmware 0.8.0.
+36 -11
View File
@@ -31,7 +31,9 @@
//!
//! Every feature defaults to [`STATE_PASSTHROUGH`] (firmware does not touch the
//! subsystem → byte-identical to OEM); [`Verb::Reset`] with [`RESET_TO_OEM`]
//! returns every feature to passthrough, while [`RESET_TO_FLASH`] reloads the last
//! returns every feature to passthrough (and, firmware ≥0.8.3, blanks the NV block
//! so the drive is traceless), [`RESET_TO_DEFAULTS`] restores the baked create-time
//! defaults, while [`RESET_TO_FLASH`] reloads the last
//! [`Verb::Save`]d config block.
use crate::scsi::{DataDirection, ScsiTransport};
@@ -114,11 +116,21 @@ 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".
/// — the non-destructive "revert to last SAVE". Marker-gated in firmware ≥0.8.3: a
/// never-saved drive loads the baked create-time defaults instead.
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.
/// [`Verb::Reset`] mode (rides in the state slot `cdb[6]`): restore the baked
/// per-create **defaults** (the create-time flags, e.g. UHD/BD on) into the RAM
/// feature-state table. RAM-only — does not touch flash. This is what a never-saved
/// drive boots to. Firmware ≥0.8.3.
pub const RESET_TO_DEFAULTS: u8 = 0x01;
/// [`Verb::Reset`] mode (rides in the state slot `cdb[6]`): TRUE OEM. Forces every
/// feature to [`STATE_PASSTHROUGH`] in RAM **and** (firmware ≥0.8.3) writes the NV
/// config block all-`0xFF` — marker included — so the drive is byte-for-byte like a
/// never-saved/never-configured drive (no trace it was ever set). Because the marker
/// is then `0xFF`, the NEXT boot loads the baked defaults, exactly like a fresh drive.
pub const RESET_TO_OEM: u8 = 0xFF;
/// The verb selector in `cdb[4]`. These numeric values ARE the wire protocol.
@@ -133,8 +145,10 @@ pub enum Verb {
Get = 0x03,
/// 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.
/// into RAM (discarding un-saved changes; marker-gated in fw ≥0.8.3),
/// [`RESET_TO_DEFAULTS`] (`0x01`) restores the baked create-time defaults, and
/// [`RESET_TO_OEM`] (`0xFF`) forces every feature to [`STATE_PASSTHROUGH`] AND
/// blanks the NV block (traceless, fw ≥0.8.3). Ignores feature.
Reset = 0x04,
/// Diagnostic RAM peek: [`MEMREAD_LEN`] bytes at the 32-bit address in
/// `cdb[5..9]` (big-endian).
@@ -267,8 +281,9 @@ pub fn build_get_cdb(feature: Feature) -> [u8; CDB_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
/// [`RESET_TO_DEFAULTS`] (`0x01`) restores the baked create-time defaults, and
/// [`RESET_TO_OEM`] (`0xFF`) forces every feature to passthrough (and blanks NV).
/// 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)
@@ -528,14 +543,24 @@ impl<'a> FirmwareControl<'a> {
self.exec_in(&cdb).map(|_| ())
}
/// 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.
/// Restore every feature to [`STATE_PASSTHROUGH`] (RESET with [`RESET_TO_OEM`])
/// — TRUE OEM: firmware ≥0.8.3 also blanks the NV block so the drive is
/// traceless. 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(RESET_TO_OEM);
self.exec_in(&cdb).map(|_| ())
}
/// Restore the baked create-time defaults into the RAM feature-state table
/// (RESET with [`RESET_TO_DEFAULTS`]) — what a never-saved drive boots to; does
/// not touch flash. Firmware ≥0.8.3. Data-in phase for the same reason as
/// [`reset`](Self::reset).
pub fn reset_to_defaults(&mut self) -> Result<()> {
let cdb = build_reset_cdb(RESET_TO_DEFAULTS);
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
+11
View File
@@ -44,6 +44,7 @@ fn state_and_frame_constants_match_abi() {
assert_eq!(REGION_DVD_BASE, 0x00);
assert_eq!(REGION_FREE, 0x0F);
assert_eq!(RESET_TO_FLASH, 0x00);
assert_eq!(RESET_TO_DEFAULTS, 0x01);
assert_eq!(RESET_TO_OEM, 0xFF);
}
@@ -332,6 +333,16 @@ fn reset_sends_reset_cdb_and_clears_state() {
assert_eq!(m.states, FeatureStates::all_passthrough());
}
#[test]
fn reset_to_defaults_sends_the_defaults_mode_cdb() {
let mut m = FwMock::new();
{
let mut fw = FirmwareControl::new(&mut m);
fw.reset_to_defaults().expect("reset to defaults");
}
assert_eq!(m.cdbs[0], build_reset_cdb(RESET_TO_DEFAULTS));
}
#[test]
fn save_then_reset_to_flash_restores_the_saved_value() {
// SET a feature, SAVE it, force all-passthrough with RESET(to-OEM), then
+45 -5
View File
@@ -18,8 +18,8 @@
//! the migrated spec the HRL/AKE/BUS unlock direction is `STATE_OFF`, not `0x01`.
use crate::firmware::{
Feature, MEMREAD_LEN, REGION_FREE, RESP_MAGIC, SPEED_MAX, STATE_OFF, build_identity_cdb,
build_memread_cdb, build_set_cdb,
Feature, MEMREAD_LEN, MIN_ALLOC_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};
@@ -90,7 +90,7 @@ impl FreemkvUnlocker {
}
}
// Issue a `Set(feature, state)` (no data-in). Ok(()) on GOOD status;
// Issue a `Set(feature, state)` over the required data-in phase. Ok(()) on GOOD status;
// NotApplicable if rejected; Transport only on a dead bus.
fn set(
&self,
@@ -99,8 +99,11 @@ impl FreemkvUnlocker {
state: u8,
) -> std::result::Result<(), UnlockError> {
let cdb = build_set_cdb(feature, state);
let mut empty: [u8; 0] = [];
match scsi.execute(&cdb, DataDirection::None, &mut empty, 5_000) {
// SET needs the data-in phase its CDB advertises — a no-data SET is aborted
// (CHECK CONDITION, HW-confirmed on BU40N fw 0.8.1). Buffer sized from the SAME
// const the CDB advertises (MIN_ALLOC_LEN) so the transfer length has one source.
let mut buf = [0u8; MIN_ALLOC_LEN as usize];
match scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 5_000) {
Ok(r) if r.status == 0 => Ok(()),
Ok(r) => {
tracing::debug!(
@@ -315,6 +318,43 @@ mod tests {
);
}
/// Regression (BU40N fw 0.8.1): a SET issued WITHOUT the data-in phase its
/// CDB advertises is aborted by the drive (CHECK CONDITION / Aborted Command),
/// so the unlocker's `set()` MUST read the MIN_ALLOC_LEN table back like
/// `FirmwareControl::set` — not send `DataDirection::None`. Before the fix
/// the whole freemkv unlock failed at `Set(Ake)` and fell back to AACS.
#[test]
fn set_issues_the_required_data_in_phase() {
struct DataInContract;
impl ScsiTransport for DataInContract {
fn execute(
&mut self,
cdb: &[u8],
dir: DataDirection,
data: &mut [u8],
_timeout_ms: u32,
) -> Result<ScsiResult> {
assert_eq!(cdb[4], 0x02, "test exercises only the SET verb");
// Model the drive: a SET with no from-device phase is aborted.
if !matches!(dir, DataDirection::FromDevice) || data.is_empty() {
return Ok(ScsiResult {
status: 2,
bytes_transferred: 0,
sense: [0u8; 32],
});
}
Ok(ScsiResult {
status: 0,
bytes_transferred: data.len(),
sense: [0u8; 32],
})
}
}
FreemkvUnlocker::new()
.set(&mut DataInContract, Feature::Region, REGION_FREE)
.expect("SET must succeed by issuing the data-in phase the firmware requires");
}
/// Each unlock SET is `Verb::Set` (02) on the right feature id (cdb[5]) with
/// the state at cdb[6]; SET floors its data-in allocation at MIN_ALLOC_LEN
/// (0x0040 at cdb[7..9]) — the drive aborts a sub-16-byte transfer (HW-confirmed).