mirror of
https://github.com/freemkv/freemkv-unlock.git
synced 2026-10-07 18:13:11 +00:00
feat(stop): AgidGuard hand-off, CSS defuse, MT1959 critical spans, pause (ST-U1)
stop-design-v5 §2.3, second commit of the two-commit shape (UT1-UT9 green): - AACS: aacs1_ake / aacs2_authenticate_p256_with_anchor return the AGID in its AgidGuard; finish_auth takes it and releases on every exit, so the 1.0 AGID is freed before the P-256 fall-through. release_agid is gone: exactly one release per allocation, through execute_cleanup (SS-7). - CSS: the AGID is guarded during the challenge and defused on success, still held for read_disc_key (D9); the pre-allocation invalidate sweep stays on execute, so a Stop refuses it. - MT1959: A's WRITE BUFFER + verify (D8, C_max 35 s) and B's MODE SELECT, READ BUFFER, WRITE BUFFER, F1 (C_max 45 s) run in a CriticalGuard; do_unlock and the retries stay cancellable. - The per-cert backoff uses pause; an interrupted wait aborts as Transport. - The test-only FwFront forwards all four new methods. UnlockError, the Unlocker trait and every public signature are unchanged.
This commit is contained in:
+73
-58
@@ -7,7 +7,7 @@
|
||||
//!
|
||||
//! Supports AACS 1.0 and AACS 2.0 cert chains.
|
||||
use crate::aacs::error::{Error, Result};
|
||||
use crate::scsi::{DataDirection, ScsiTransport};
|
||||
use crate::scsi::{AgidGuard, DataDirection, ScsiTransport};
|
||||
use num_bigint::BigUint;
|
||||
use num_traits::{One, Zero};
|
||||
use sha1::{Digest, Sha1};
|
||||
@@ -69,12 +69,11 @@ fn cert_host_id_hex(cert: &[u8]) -> String {
|
||||
cert[4..10].iter().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
|
||||
// Release an AGID (REPORT KEY format 0x3F). A drive has only four, so avoid
|
||||
// leaving one held between attempts. Best-effort: a release failure isn't a
|
||||
// failure of the operation that's already failing.
|
||||
fn release_agid(session: &mut dyn ScsiTransport, agid: u8) {
|
||||
let cdb = cdb_report_key(agid, 0x3F, 2);
|
||||
let _ = scsi_read(session, &cdb, 2);
|
||||
// Hold an allocated AGID. A drive has only four, so its drop releases it (REPORT KEY
|
||||
// format 0x3F) via `execute_cleanup`, so even a Stop frees it: one release per
|
||||
// allocation (SS-7, evidence: libaacs mmc.c AGID invalidation; stop-design-v5 §2.3).
|
||||
fn agid_guard<'a>(session: &'a mut (dyn ScsiTransport + 'a), agid: u8) -> AgidGuard<'a> {
|
||||
AgidGuard::new(session, agid, cdb_report_key(agid, 0x3F, 2))
|
||||
}
|
||||
|
||||
// ── AACS 1.0 elliptic curve parameters (160-bit) ───────────────────────────
|
||||
@@ -982,7 +981,14 @@ fn aacs_authenticate_with_anchor(
|
||||
la_x: &[u8; 20],
|
||||
la_y: &[u8; 20],
|
||||
) -> Result<AacsAuth> {
|
||||
aacs1_ake(session, host_priv_key, host_cert, la_x, la_y).map_err(V1Fail::into_error)
|
||||
// AKE-only callers read the VID themselves under `auth.agid`, so the AGID is
|
||||
// handed over still held, exactly as before the guard existed.
|
||||
aacs1_ake(session, host_priv_key, host_cert, la_x, la_y)
|
||||
.map(|(auth, guard)| {
|
||||
guard.defuse();
|
||||
auth
|
||||
})
|
||||
.map_err(V1Fail::into_error)
|
||||
}
|
||||
|
||||
/// Why the 1.0 AKE failed. `DriveCert`: the drive presented a recognised cert
|
||||
@@ -1008,14 +1014,16 @@ impl V1Fail {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`aacs_authenticate_with_anchor`] keeping the [`V1Fail`] distinction.
|
||||
fn aacs1_ake(
|
||||
session: &mut dyn ScsiTransport,
|
||||
/// [`aacs_authenticate_with_anchor`] keeping the [`V1Fail`] distinction. On
|
||||
/// success the AGID comes back still held in its [`AgidGuard`], which the caller
|
||||
/// hands to [`finish_auth`]; on any failure the guard has already released it.
|
||||
fn aacs1_ake<'a>(
|
||||
session: &'a mut (dyn ScsiTransport + 'a),
|
||||
host_priv_key: &[u8; 20],
|
||||
host_cert: &[u8],
|
||||
la_x: &[u8; 20],
|
||||
la_y: &[u8; 20],
|
||||
) -> std::result::Result<AacsAuth, V1Fail> {
|
||||
) -> std::result::Result<(AacsAuth, AgidGuard<'a>), V1Fail> {
|
||||
if host_cert.len() < 92 {
|
||||
return Err(Error::AacsCertShort.into());
|
||||
}
|
||||
@@ -1032,18 +1040,15 @@ fn aacs1_ake(
|
||||
scsi_read(session, &cdb, 8).map_err(|e| handshake_err(e, Error::AacsAgidAlloc))?;
|
||||
let agid = (response[7] >> 6) & 0x03;
|
||||
|
||||
// From here on we HOLD the AGID. Every failure below used to abandon it
|
||||
// (see [`release_agid`]); release it on the way out instead.
|
||||
let r = aacs_authenticate_with_agid(session, agid, host_priv_key, host_cert, la_x, la_y);
|
||||
if r.is_err() {
|
||||
release_agid(session, agid);
|
||||
}
|
||||
r
|
||||
// From here on we HOLD the AGID: an error below drops the guard, releasing it.
|
||||
let mut guard = agid_guard(session, agid);
|
||||
let auth =
|
||||
aacs_authenticate_with_agid(&mut *guard, agid, host_priv_key, host_cert, la_x, la_y)?;
|
||||
Ok((auth, guard))
|
||||
}
|
||||
|
||||
/// Steps 3-9 of [`aacs_authenticate`], with the AGID already allocated. Split
|
||||
/// out so the single caller can release the AGID on ANY failure without a Drop
|
||||
/// guard or a release call at each of the seven early returns.
|
||||
/// Steps 3-9 of [`aacs_authenticate`], with the AGID already allocated and held
|
||||
/// by the caller's [`AgidGuard`], which releases it on any of the early returns.
|
||||
fn aacs_authenticate_with_agid(
|
||||
session: &mut dyn ScsiTransport,
|
||||
agid: u8,
|
||||
@@ -1183,16 +1188,16 @@ fn aacs_authenticate_with_agid(
|
||||
})
|
||||
}
|
||||
|
||||
// Native AACS 2.0 handshake (P-256/SHA-256); LA anchor is a parameter so tests drive the full
|
||||
// AKE under a test keypair. Its 2.0 cert offsets are PROVISIONAL, so run_cert_handshake gates
|
||||
// it off (see AACS2_P256_EXPERIMENTAL).
|
||||
fn aacs2_authenticate_p256_with_anchor(
|
||||
session: &mut dyn ScsiTransport,
|
||||
// Native AACS 2.0 handshake (P-256/SHA-256), returning the AGID still held in its guard like
|
||||
// `aacs1_ake`. LA anchor is a parameter so tests drive the full AKE; its 2.0 cert offsets are
|
||||
// PROVISIONAL, so run_cert_handshake gates it off (see AACS2_P256_EXPERIMENTAL).
|
||||
fn aacs2_authenticate_p256_with_anchor<'a>(
|
||||
session: &'a mut (dyn ScsiTransport + 'a),
|
||||
host_priv_key: &[u8; 32],
|
||||
host_cert: &[u8],
|
||||
la_x: &[u8; 32],
|
||||
la_y: &[u8; 32],
|
||||
) -> Result<AacsAuth> {
|
||||
) -> Result<(AacsAuth, AgidGuard<'a>)> {
|
||||
if host_cert.len() < 132 {
|
||||
return Err(Error::AacsCertShort);
|
||||
}
|
||||
@@ -1209,18 +1214,15 @@ fn aacs2_authenticate_p256_with_anchor(
|
||||
scsi_read(session, &cdb, 8).map_err(|e| handshake_err(e, Error::AacsAgidAlloc))?;
|
||||
let agid = (response[7] >> 6) & 0x03;
|
||||
|
||||
// From here we HOLD the AGID; release on ANY failure below, mirroring
|
||||
// the leak fix (`975315d`) applied to the AACS 1.0 twin.
|
||||
let r = aacs2_authenticate_p256_with_agid(session, agid, host_priv_key, host_cert, la_x, la_y);
|
||||
if r.is_err() {
|
||||
release_agid(session, agid);
|
||||
}
|
||||
r
|
||||
// From here we HOLD the AGID: an error below drops the guard, releasing it.
|
||||
let mut guard = agid_guard(session, agid);
|
||||
let auth =
|
||||
aacs2_authenticate_p256_with_agid(&mut *guard, agid, host_priv_key, host_cert, la_x, la_y)?;
|
||||
Ok((auth, guard))
|
||||
}
|
||||
|
||||
/// Steps 3-9 of [`aacs2_authenticate_p256_with_anchor`] with the AGID already allocated,
|
||||
/// split out so the single caller can release the AGID on any of the seven
|
||||
/// fallible exits without a per-return release call or a Drop guard.
|
||||
/// Steps 3-9 of [`aacs2_authenticate_p256_with_anchor`] with the AGID already
|
||||
/// allocated and held by the caller's [`AgidGuard`].
|
||||
fn aacs2_authenticate_p256_with_agid(
|
||||
session: &mut dyn ScsiTransport,
|
||||
agid: u8,
|
||||
@@ -1483,15 +1485,17 @@ enum FinishErr {
|
||||
}
|
||||
|
||||
/// Read the Volume ID + data keys for a completed auth and assemble the finished
|
||||
/// [`CertHandshake`]. Releases the AGID on EVERY exit — including the
|
||||
/// fully-successful one (previously leaked, slowly draining the drive's 4-AGID
|
||||
/// pool across discs), since nothing downstream needs the AGID once the VID and
|
||||
/// data keys are read. `idx` is for log correlation only.
|
||||
/// [`CertHandshake`]. Takes the AGID's guard and so releases it on EVERY exit,
|
||||
/// including success (nothing downstream needs the AGID once the VID and data
|
||||
/// keys are read) and before the caller's P-256 fall-through. `idx` is for log
|
||||
/// correlation only.
|
||||
fn finish_auth(
|
||||
scsi: &mut dyn ScsiTransport,
|
||||
mut guard: AgidGuard<'_>,
|
||||
mut auth: AacsAuth,
|
||||
idx: usize,
|
||||
) -> std::result::Result<CertHandshake, FinishErr> {
|
||||
debug_assert_eq!(guard.agid(), auth.agid, "the guard holds this auth's AGID");
|
||||
let scsi: &mut dyn ScsiTransport = &mut *guard;
|
||||
let volume_id = match read_volume_id(scsi, &mut auth) {
|
||||
Ok(vid) => vid,
|
||||
Err(e) => {
|
||||
@@ -1505,10 +1509,7 @@ fn finish_auth(
|
||||
transport_failure = transport,
|
||||
"auth ok but volume ID read failed"
|
||||
);
|
||||
// We authenticated, so we hold an AGID; release it before giving up
|
||||
// rather than leaving the drive one short until the next attempt
|
||||
// invalidates all four.
|
||||
release_agid(scsi, auth.agid);
|
||||
// The guard releases the AGID on this return.
|
||||
// A dead bus is NOT "the drive has no Volume ID" — that told the
|
||||
// consumer to fall through and keep working a transport that is gone.
|
||||
return Err(if transport {
|
||||
@@ -1542,9 +1543,8 @@ fn finish_auth(
|
||||
);
|
||||
// A dead bus here is NOT "no data key served" — left unclassified it
|
||||
// returned a successful-looking unlock with read_data_key: None.
|
||||
// Release AGID, abort like VID.
|
||||
// Abort like VID (the guard releases the AGID).
|
||||
if transport {
|
||||
release_agid(scsi, auth.agid);
|
||||
return Err(FinishErr::Transport);
|
||||
}
|
||||
(None, Some(e.code()))
|
||||
@@ -1559,10 +1559,9 @@ fn finish_auth(
|
||||
has_read_data_key = read_data_key.is_some(),
|
||||
"AACS bus-auth handshake complete"
|
||||
);
|
||||
// Release the AGID on the fully-successful path too: the VID and data keys
|
||||
// are already read, so nothing downstream needs it, and holding it slowly
|
||||
// leaks the drive's 4-AGID pool across discs.
|
||||
release_agid(scsi, auth.agid);
|
||||
// Release the AGID on the fully-successful path too: nothing downstream needs
|
||||
// it, and holding it slowly leaks the drive's 4-AGID pool across discs.
|
||||
drop(guard);
|
||||
Ok(CertHandshake {
|
||||
volume_id,
|
||||
read_data_key,
|
||||
@@ -1613,7 +1612,9 @@ fn attempt_one_cert(
|
||||
let mut v1_drive_dead = false;
|
||||
if try_v1 {
|
||||
match aacs1_ake(scsi, &hc.private_key, &hc.certificate, la.0, la.1) {
|
||||
Ok(auth) => match finish_auth(scsi, auth, idx) {
|
||||
// `finish_auth` drops the guard (releasing the 1.0 AGID) on every
|
||||
// return, so it is free before the P-256 fall-through below.
|
||||
Ok((auth, guard)) => match finish_auth(guard, auth, idx) {
|
||||
Ok(ch) => return CertOutcome::Ok(ch),
|
||||
Err(FinishErr::Transport) => return CertOutcome::Transport,
|
||||
// Post-auth VID/MAC failure with v2 creds available: fall through to
|
||||
@@ -1659,7 +1660,7 @@ fn attempt_one_cert(
|
||||
return CertOutcome::VidUnavailable;
|
||||
};
|
||||
match aacs2_authenticate_p256_with_anchor(scsi, k, c, la.2, la.3) {
|
||||
Ok(auth) => match finish_auth(scsi, auth, idx) {
|
||||
Ok((auth, guard)) => match finish_auth(guard, auth, idx) {
|
||||
Ok(ch) => CertOutcome::Ok(ch),
|
||||
Err(FinishErr::Transport) => CertOutcome::Transport,
|
||||
// A VID failure on the P-256 path is terminal for this cert.
|
||||
@@ -1742,8 +1743,20 @@ pub(crate) fn run_cert_handshake_with_anchors(
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if drive_attempts > 0 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(PER_CERT_BACKOFF_MS));
|
||||
// Halt-aware (stop-design-v5 §2.3 "The per-cert backoff … uses `pause`"): a
|
||||
// Stop ends the wait at once, surfacing as the transport abort it is.
|
||||
if drive_attempts > 0
|
||||
&& scsi
|
||||
.pause(std::time::Duration::from_millis(PER_CERT_BACKOFF_MS))
|
||||
.is_err()
|
||||
{
|
||||
tracing::debug!(
|
||||
target: "freemkv::disc",
|
||||
phase = "cert_backoff_interrupted",
|
||||
cert_index = idx,
|
||||
"per-cert backoff interrupted (operation stopped); aborting"
|
||||
);
|
||||
return Err(UnlockError::Transport);
|
||||
}
|
||||
drive_attempts += 1;
|
||||
match attempt_one_cert(scsi, hc, idx, la, v1_ok && !v1_drive_dead, has_v2) {
|
||||
@@ -3447,9 +3460,11 @@ pub(crate) mod tests {
|
||||
let host_cert = p256_synth_cert(0x11, &host_x, &host_y, &la_priv);
|
||||
|
||||
let mut emu = DriveEmuP256::new(drive_lt_priv, cert);
|
||||
let auth =
|
||||
let (auth, guard) =
|
||||
aacs2_authenticate_p256_with_anchor(&mut emu, &host_priv, &host_cert, &la_x, &la_y)
|
||||
.expect("a genuine LA-signed 2.0 cert must complete the AKE");
|
||||
assert_eq!(guard.agid(), auth.agid, "the AGID comes back still held");
|
||||
drop(guard);
|
||||
assert_ne!(auth.bus_key, [0u8; 16], "a bus key must be derived");
|
||||
assert_eq!(
|
||||
Some(auth.bus_key),
|
||||
|
||||
@@ -442,6 +442,27 @@ mod tests {
|
||||
}
|
||||
status(0, data.len())
|
||||
}
|
||||
// Forward the Stop methods (stop-design-v5 §2.3): the defaults would
|
||||
// swallow them, so wrapped tests would stop exercising cancellation.
|
||||
fn pause(&mut self, d: std::time::Duration) -> crate::scsi::Result<()> {
|
||||
self.inner.pause(d)
|
||||
}
|
||||
fn begin_critical(&mut self) -> crate::scsi::Result<()> {
|
||||
self.inner.begin_critical()
|
||||
}
|
||||
fn end_critical(&mut self) {
|
||||
self.inner.end_critical()
|
||||
}
|
||||
fn execute_cleanup(
|
||||
&mut self,
|
||||
cdb: &[u8],
|
||||
dir: crate::scsi::DataDirection,
|
||||
data: &mut [u8],
|
||||
timeout_ms: u32,
|
||||
) -> crate::scsi::Result<crate::scsi::ScsiResult> {
|
||||
self.cdbs.push(cdb.to_vec());
|
||||
self.inner.execute_cleanup(cdb, dir, data, timeout_ms)
|
||||
}
|
||||
}
|
||||
|
||||
// UT6 (stop-design-v5 §5.2; §2.3 "The test-only `FwFront<T>` … forwards all
|
||||
|
||||
+27
-17
@@ -10,7 +10,7 @@
|
||||
|
||||
mod error;
|
||||
use crate::css::error::{Error, Result, step_err};
|
||||
use crate::scsi::{DataDirection, ScsiTransport};
|
||||
use crate::scsi::{AgidGuard, 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
|
||||
@@ -310,12 +310,13 @@ 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.
|
||||
// challenge/response). Returns the AGID, still held (D9); 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.
|
||||
// an abandoned AGID self-heal — which is not a reason to abandon one. Plain
|
||||
// `execute`: nothing is held yet, so a Stop refuses these (stop-design-v5 §2.3).
|
||||
for agid in 0..4u8 {
|
||||
release_agid(scsi, agid);
|
||||
invalidate_agid(scsi, agid);
|
||||
}
|
||||
|
||||
// Allocate AGID
|
||||
@@ -329,28 +330,37 @@ fn establish_authenticated_session(scsi: &mut dyn ScsiTransport) -> Result<u8> {
|
||||
)?;
|
||||
let agid = (buf[7] >> 6) & 0x03;
|
||||
|
||||
// From here on we hold the AGID; release it on any failure (see
|
||||
// [`release_agid`]) instead of abandoning it.
|
||||
let r = authenticate_with_agid(scsi, agid);
|
||||
if r.is_err() {
|
||||
release_agid(scsi, agid);
|
||||
}
|
||||
r.map(|()| agid)
|
||||
// From here on we hold the AGID: a failure drops the guard, which releases
|
||||
// it even after a Stop (SS-7, evidence: libaacs mmc.c AGID invalidation).
|
||||
let mut guard = AgidGuard::new(scsi, agid, agid_release_cdb(agid));
|
||||
authenticate_with_agid(&mut *guard, agid)?;
|
||||
// D9 (stop-design-v5 §2.3 "defused on success, so it is still held for
|
||||
// `read_disc_key`"): hand the AGID over unreleased.
|
||||
Ok(guard.defuse())
|
||||
}
|
||||
|
||||
/// Release an AGID (REPORT KEY format 0x3F). Best-effort: a failure to release
|
||||
/// is not a failure of the operation that is already failing.
|
||||
fn release_agid(scsi: &mut dyn ScsiTransport, agid: u8) {
|
||||
/// REPORT KEY key format 0x3F for `agid` (invalidate / release it).
|
||||
fn agid_release_cdb(agid: u8) -> [u8; 12] {
|
||||
let mut cdb = [0u8; 12];
|
||||
cdb[0] = crate::scsi::SCSI_REPORT_KEY;
|
||||
cdb[10] = (agid << 6) | 0x3F;
|
||||
cdb
|
||||
}
|
||||
|
||||
/// Best-effort invalidate of an AGID this session does not hold (the
|
||||
/// pre-allocation sweep).
|
||||
fn invalidate_agid(scsi: &mut dyn ScsiTransport, agid: u8) {
|
||||
let mut buf = [0u8; 8];
|
||||
let _ = scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 5_000);
|
||||
let _ = scsi.execute(
|
||||
&agid_release_cdb(agid),
|
||||
DataDirection::FromDevice,
|
||||
&mut buf,
|
||||
5_000,
|
||||
);
|
||||
}
|
||||
|
||||
/// The challenge-response half of [`establish_authenticated_session`], with the
|
||||
/// AGID already allocated. Split out so its caller can release the AGID on any
|
||||
/// failure without a Drop guard or a release at each early return.
|
||||
/// AGID already allocated and held by the caller's [`AgidGuard`].
|
||||
fn authenticate_with_agid(scsi: &mut dyn ScsiTransport, agid: u8) -> Result<()> {
|
||||
// Host sends challenge. The spec wants a fresh per-session random nonce,
|
||||
// not a fixed constant — a predictable challenge weakens the bus-auth
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
use super::Mt1959;
|
||||
use crate::ld::error::Result;
|
||||
use crate::scsi::{DataDirection, ScsiTransport};
|
||||
use crate::scsi::{CriticalGuard, DataDirection, ScsiTransport};
|
||||
|
||||
use super::SCSI_WRITE_BUFFER;
|
||||
|
||||
@@ -40,7 +40,10 @@ pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Re
|
||||
0x00,
|
||||
];
|
||||
let mut data = firmware.clone();
|
||||
scsi.execute(&cdb, DataDirection::ToDevice, &mut data, 30_000)?;
|
||||
// D8 (stop-design-v5 §2.3): "the WRITE BUFFER at 30 s … and its verify run inside
|
||||
// a `CriticalGuard`. C_max = 35 s" — a Stop mid-upload waits for both CDBs.
|
||||
let mut span = CriticalGuard::enter(&mut *scsi)?;
|
||||
span.execute(&cdb, DataDirection::ToDevice, &mut data, 30_000)?;
|
||||
|
||||
// Verify the firmware loaded (non-fatal — different buffer_id 0x45). No
|
||||
// documented expected payload exists, so the outcome is traced rather
|
||||
@@ -58,7 +61,7 @@ pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Re
|
||||
0x00,
|
||||
];
|
||||
let mut verify_resp = [0u8; super::VALIDATE_RESPONSE_SIZE as usize];
|
||||
match scsi.execute(
|
||||
match span.execute(
|
||||
&verify_cdb,
|
||||
DataDirection::FromDevice,
|
||||
&mut verify_resp,
|
||||
@@ -96,6 +99,8 @@ pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Re
|
||||
}
|
||||
}
|
||||
|
||||
drop(span); // the span ends here; the unlock below stays cancellable
|
||||
|
||||
// Double unlock after firmware upload: first is fatal on failure, second
|
||||
// is a best-effort confirmation pass (matching variant B) so a benign
|
||||
// hiccup there doesn't fail an already-successful unlock.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
use super::{Mt1959, SCSI_READ_BUFFER, SCSI_WRITE_BUFFER};
|
||||
use crate::ld::error::Result;
|
||||
use crate::scsi::{DataDirection, ScsiTransport};
|
||||
use crate::scsi::{CriticalGuard, DataDirection, ScsiTransport};
|
||||
|
||||
const SCSI_MODE_SELECT: u8 = 0x55;
|
||||
const FIRMWARE_EXTRA: [u8; 16] = [0; 16];
|
||||
@@ -77,7 +77,10 @@ pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Re
|
||||
0x00,
|
||||
];
|
||||
let mut data = firmware[..write_len].to_vec();
|
||||
scsi.execute(&mode_select_cdb, DataDirection::ToDevice, &mut data, 30_000)?;
|
||||
// stop-design-v5 §2.3: "MODE SELECT at 30 s …, READ BUFFER, WRITE BUFFER and F1 run
|
||||
// inside a `CriticalGuard`. C_max = 45 s" — a Stop mid-upload waits for all four.
|
||||
let mut span = CriticalGuard::enter(&mut *scsi)?;
|
||||
span.execute(&mode_select_cdb, DataDirection::ToDevice, &mut data, 30_000)?;
|
||||
|
||||
// Step 2: Read firmware metadata (READ_BUFFER mode 6, offset 0x3000)
|
||||
let read_meta_cdb = [
|
||||
@@ -95,7 +98,7 @@ pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Re
|
||||
let mut meta_resp = [0u8; 16];
|
||||
trace_step(
|
||||
"mt1959b_fw_meta",
|
||||
scsi.execute(
|
||||
span.execute(
|
||||
&read_meta_cdb,
|
||||
DataDirection::FromDevice,
|
||||
&mut meta_resp,
|
||||
@@ -119,7 +122,7 @@ pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Re
|
||||
let mut data2 = FIRMWARE_EXTRA.to_vec();
|
||||
trace_step(
|
||||
"mt1959b_fw_extra",
|
||||
scsi.execute(&write_extra_cdb, DataDirection::ToDevice, &mut data2, 5_000),
|
||||
span.execute(&write_extra_cdb, DataDirection::ToDevice, &mut data2, 5_000),
|
||||
)?;
|
||||
|
||||
// Step 4: Vendor verify (0xF1 — B-only, not standard SCSI), per-drive.
|
||||
@@ -127,8 +130,9 @@ pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Re
|
||||
let mut dummy = [0u8; 0];
|
||||
trace_step(
|
||||
"mt1959b_fw_verify",
|
||||
scsi.execute(&verify_cdb, DataDirection::None, &mut dummy, 5_000),
|
||||
span.execute(&verify_cdb, DataDirection::None, &mut dummy, 5_000),
|
||||
)?;
|
||||
drop(span); // the span ends here; the unlock retries below stay cancellable
|
||||
|
||||
// Step 5: Unlock retries (up to 5, then a final fatal attempt). The
|
||||
// confirmation pass after success is intentionally best-effort.
|
||||
|
||||
+11
@@ -158,6 +158,17 @@ impl<'a> AgidGuard<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// Manual Debug: the transport is not `Debug`; the AGID and whether it is still
|
||||
// armed (will be released on drop) are what a failure message needs.
|
||||
impl std::fmt::Debug for AgidGuard<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("AgidGuard")
|
||||
.field("agid", &self.agid)
|
||||
.field("armed", &self.armed)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AgidGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
if !self.armed {
|
||||
|
||||
Reference in New Issue
Block a user