unlock_bridge: run_features/run_bus dispatch; report LibreDrive vs Renesas

Adopt freemkv-unlock's split Unlocker trait: run_features drives the drive-prep capability, run_bus the content bus removal, each iterating unlockers until one doesn't decline (NotApplicable = try next; Ok or a real error stops).

unlocker_matrix now reports which drive-prep unlocker actually ran — LibreDrive removes the bus at the drive; Renesas unlocks features but leaves the bus to the cert. Wire product_id through to fu::DriveId.

Bump to 1.2.3.
This commit is contained in:
Matthew Jackson
2026-07-07 16:36:05 -07:00
parent 122a03b23d
commit bc04ee7bd2
5 changed files with 82 additions and 33 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "libfreemkv"
version = "1.2.2"
version = "1.2.3"
edition = "2024"
rust-version = "1.86"
license = "AGPL-3.0-only"
+3 -3
View File
@@ -111,13 +111,13 @@ impl AacsCertUnlocker<'_> {
// through method calls, so clone the (cheap) identity first.
let drive_id = session.drive_id.clone();
let fu_certs = crate::unlock_bridge::map_host_certs(&host_certs);
let unlocked = crate::unlock_bridge::run_unlockers(
let (_, unlock_res) = crate::unlock_bridge::run_bus(
session.scsi_mut(),
&drive_id,
freemkv_unlock::DiscKind::Aacs,
&fu_certs,
)
.map_err(CertUnlockFailure::Unlock)?;
);
let unlocked = unlock_res.map_err(CertUnlockFailure::Unlock)?;
// The cert handshake yields a VID on success; its absence is VidUnavailable.
let Some(volume_id) = unlocked.vid else {
return Err(CertUnlockFailure::Unlock(UnlockError::VidUnavailable));
+16 -10
View File
@@ -1644,12 +1644,13 @@ impl Disc {
// key). Any failure is non-fatal: continue to the crack, which
// simply finds nothing if the drive kept the sectors gated.
let drive_id = session.drive_id.clone();
if let Err(e) = crate::unlock_bridge::run_unlockers(
let (_, css_unlock_res) = crate::unlock_bridge::run_bus(
session.scsi_mut(),
&drive_id,
freemkv_unlock::DiscKind::Css,
&[],
) {
);
if let Err(e) = css_unlock_res {
tracing::warn!(
target: "freemkv::scan",
outcome = ?e,
@@ -2308,18 +2309,23 @@ impl Disc {
/// that honestly (`AACS: no`), and on a *stock* drive that fell back to the
/// cert route it reports `LibreDrive: no, AACS: yes` — the real diagnostic.
pub fn unlocker_matrix(&self, drive: &crate::Drive) -> Vec<(&'static str, bool)> {
// LibreDrive firmware-unlocked the drive iff a drive unlocker actually
// succeeded at init (name recorded only on success).
let ld_worked = drive.unlocker_name().is_some();
// The drive-prep unlocker that actually ran (recorded on init):
// "LibreDrive" (MediaTek) or "Renesas" — mutually exclusive per drive.
let prep = drive.unlocker_name();
// Only LibreDrive (MediaTek) removes AACS bus encryption AT THE DRIVE;
// Renesas unlocks features but leaves the bus to the cert.
let ld_removed_bus = prep == Some("LibreDrive");
crate::unlock_bridge::unlocker_names()
.into_iter()
.map(|name| {
let did_work = match name {
// The drive firmware unlock ran and succeeded.
"LibreDrive" => ld_worked,
// The AACS host-cert route removed the bus ONLY when LibreDrive
// didn't (stock drive) AND AACS state was actually obtained.
"AACS" => self.aacs.is_some() && !ld_worked,
// Each firmware unlocker did work iff it was the one that ran.
"LibreDrive" => ld_removed_bus,
"Renesas" => prep == Some("Renesas"),
// The AACS host-cert route removed the bus ONLY when the
// firmware didn't (stock or Renesas drive) AND AACS state was
// actually obtained.
"AACS" => self.aacs.is_some() && !ld_removed_bus,
// The CSS handshake/crack succeeded → title keys recovered.
"CSS" => self.css.is_some(),
// A newly-registered unlocker with no runtime signal wired
+7 -8
View File
@@ -433,15 +433,14 @@ impl Drive {
// that used to sit here was the v1.0.0-rc.1 regression — it skipped the
// drive-prep for DVD, leaving DVDs riplocked at stock speed.
self.init_ran = true;
let r: Result<()> = match crate::unlock_bridge::run_unlockers(
self.scsi.as_mut(),
&self.drive_id,
freemkv_unlock::DiscKind::Unknown,
&[],
) {
let (matched, unlock_res) =
crate::unlock_bridge::run_features(self.scsi.as_mut(), &self.drive_id);
let r: Result<()> = match unlock_res {
Ok(unlocked) => {
self.unlocker_name =
crate::unlock_bridge::unlocker_name(&self.drive_id).map(str::to_string);
// Record WHICH drive-prep unlocker actually ran — "LibreDrive"
// (MediaTek) or "Renesas" — not the ld-only identity lookup, so a
// Renesas drive reports itself honestly rather than as nothing.
self.unlocker_name = Some(matched.to_string());
// Stash the OEM Volume ID the unlocker returned for the AACS
// handshake phase (do_handshake reads it via `oem_vid()`). A
// drive-prep unlocker always carries a VID; guard anyway.
+55 -11
View File
@@ -9,6 +9,7 @@ use freemkv_unlock as fu;
fn to_fu_drive_id(drive_id: &crate::identity::DriveId) -> fu::DriveId {
fu::DriveId {
vendor_id: drive_id.vendor_id.clone(),
product_id: drive_id.product_id.clone(),
product_revision: drive_id.product_revision.clone(),
vendor_specific: drive_id.vendor_specific.clone(),
firmware_date: drive_id.firmware_date.clone(),
@@ -101,27 +102,70 @@ pub(crate) fn map_host_certs(certs: &[crate::aacs::types::HostCert]) -> Vec<fu::
.collect()
}
/// News up the unlockers, build the context for `kind`, and run the FIRST
/// matching one — returning its `Result` so the caller can both consume what it
/// learned (vid / bus_key / drive_unlocked) AND render the specific failure
/// (the AACS cert path maps the `UnlockError` to its outcome trace). `Err(
/// NotApplicable)` when nothing matched. `host_certs` are collected by the
/// caller — lazily, only for AACS; pass `&[]` for the drive-prep / CSS kinds.
pub(crate) fn run_unlockers(
/// Result of a capability dispatch: `(matched_name, result)`. `matched_name` is
/// the unlocker that handled it (or `""` if none did) — lets the caller record
/// WHICH unlocker ran (e.g. `LibreDrive` vs `Renesas`), distinct from the ld-only
/// identity lookup [`unlocker_name`]. Iterating stops at the first unlocker whose
/// capability method returns anything other than `NotApplicable` — i.e. an actual
/// unlock (`Ok`) OR a real failure such as a dead bus (`Err(Transport)`), which
/// the caller must surface rather than skip.
type Dispatch = (
&'static str,
std::result::Result<fu::Unlocked, fu::UnlockError>,
);
/// Try each unlocker's `capability` (`unlock_features` or `unlock_bus`) in
/// registration order, stopping at the first that doesn't decline. Shared by
/// [`run_features`] and [`run_bus`].
fn dispatch(
scsi: &mut dyn crate::scsi::ScsiTransport,
drive_id: &crate::identity::DriveId,
kind: fu::DiscKind,
host_certs: &[fu::HostCert],
) -> std::result::Result<fu::Unlocked, fu::UnlockError> {
capability: impl Fn(
&dyn fu::Unlocker,
&mut dyn fu::scsi::ScsiTransport,
&fu::UnlockCtx,
) -> std::result::Result<fu::Unlocked, fu::UnlockError>,
) -> Dispatch {
let id = to_fu_drive_id(drive_id);
let ctx = fu::UnlockCtx::new(&id, kind, host_certs);
let mut adapter = ScsiAdapter(scsi);
for u in fu::all_unlockers() {
if u.matches(&ctx) {
return u.unlock(&mut adapter, &ctx);
match capability(u.as_ref(), &mut adapter, &ctx) {
// This unlocker doesn't provide the capability for this drive/disc —
// try the next one.
Err(fu::UnlockError::NotApplicable) => continue,
// An actual unlock, or a real failure (e.g. Transport) — stop here.
other => return (u.name(), other),
}
}
Err(fu::UnlockError::NotApplicable)
("", Err(fu::UnlockError::NotApplicable))
}
/// Drive-prep: unlock DRIVE FEATURES (riplock/speed, OEM VID). `host_certs` are
/// not needed for features — pass `&[]`; `kind` is `Unknown` at drive-prep.
pub(crate) fn run_features(
scsi: &mut dyn crate::scsi::ScsiTransport,
drive_id: &crate::identity::DriveId,
) -> Dispatch {
dispatch(scsi, drive_id, fu::DiscKind::Unknown, &[], |u, s, c| {
u.unlock_features(s, c)
})
}
/// Content: remove BUS ENCRYPTION for the mounted disc. Called only when the bus
/// isn't already clear (the `oem_vid`/`bus_encryption_removed` gate). `host_certs`
/// are the caller-collected certs for the AACS route; `kind` selects Aacs vs Css.
pub(crate) fn run_bus(
scsi: &mut dyn crate::scsi::ScsiTransport,
drive_id: &crate::identity::DriveId,
kind: fu::DiscKind,
host_certs: &[fu::HostCert],
) -> Dispatch {
dispatch(scsi, drive_id, kind, host_certs, |u, s, c| {
u.unlock_bus(s, c)
})
}
/// The names of every REGISTERED unlocker, in dispatch order. Registry-driven —