Files
freemkv-unlock/src/ld/platform/mt1959/variant_b.rs
T
Matthew Jackson 2ee8c0b0a6 stop: Stop-aware unlock operations
ScsiTransport gains Stop methods; AGIDs are handed off by AgidGuard,
CSS is defused on Stop and MT1959 critical spans are guarded, and the
post-upload readiness poll waits via pause so a Stop ends it.
A test guards the op paths against bare sleeps.
2026-09-28 08:42:29 -07:00

153 lines
4.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! MT1959 variant B firmware upload.
//!
//! MODE SELECT (0x55) → read metadata → WRITE_BUFFER → vendor verify (0xF1) → unlock × 5+1
use super::{Mt1959, SCSI_READ_BUFFER, SCSI_WRITE_BUFFER};
use crate::ld::error::Result;
use crate::scsi::{CriticalGuard, DataDirection, ScsiTransport};
const SCSI_MODE_SELECT: u8 = 0x55;
const FIRMWARE_EXTRA: [u8; 16] = [0; 16];
/// Fallback F1 vendor-verify for legacy profiles that predate the per-drive
/// `fw_verify_cdb` capture. It carries ONE drive's token, so it only works for
/// 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];
// 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<()> {
match r {
Ok(res) if res.status == 0 => {
tracing::debug!(
target: "freemkv::disc",
phase,
bytes_transferred = res.bytes_transferred,
"firmware upload step ok"
);
Ok(())
}
Ok(res) => {
tracing::warn!(
target: "freemkv::disc",
phase,
status = res.status,
"firmware upload step returned a drive sense"
);
Ok(())
}
Err(e) => {
let err = crate::ld::error::Error::from(e);
tracing::warn!(
target: "freemkv::disc",
phase,
transport_failure = err.is_transport_failure(),
"firmware upload step failed"
);
if err.is_transport_failure() {
return Err(err);
}
Ok(())
}
}
}
pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Result<()> {
let firmware = &mt.profile.firmware;
if firmware.is_empty() {
return Err(crate::ld::error::Error::UnlockFailed);
}
// 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();
if write_len > u16::MAX as usize {
return Err(crate::ld::error::Error::UnlockFailed);
}
let mode_select_cdb = [
SCSI_MODE_SELECT,
0x10,
0x00,
0x00,
0x00,
0x00,
(write_len >> 16) as u8,
(write_len >> 8) as u8,
write_len as u8,
0x00,
];
let mut data = firmware[..write_len].to_vec();
// 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 = [
SCSI_READ_BUFFER,
0x06,
0x00,
0x00,
0x30,
0x00,
0x00,
0x00,
0x10,
0x00,
];
let mut meta_resp = [0u8; 16];
trace_step(
"mt1959b_fw_meta",
span.execute(
&read_meta_cdb,
DataDirection::FromDevice,
&mut meta_resp,
5_000,
),
)?;
// Step 3: Write extra firmware data (all zeros)
let write_extra_cdb = [
SCSI_WRITE_BUFFER,
0x06,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x10,
0x00,
];
let mut data2 = FIRMWARE_EXTRA.to_vec();
trace_step(
"mt1959b_fw_extra",
span.execute(&write_extra_cdb, DataDirection::ToDevice, &mut data2, 5_000),
)?;
// Step 4: Vendor verify (0xF1 — B-only, not standard SCSI), per-drive.
let verify_cdb = mt.profile.fw_verify_cdb.unwrap_or(VENDOR_VERIFY);
let mut dummy = [0u8; 0];
trace_step(
"mt1959b_fw_verify",
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.
for _attempt in 0..5 {
match mt.do_unlock(scsi) {
Ok(_) => {
let _ = mt.do_unlock(scsi);
return Ok(());
}
// A dead bus will not recover across five more unlock attempts.
Err(e) if e.is_transport_failure() => return Err(e),
Err(_) => {}
}
}
mt.do_unlock(scsi)?;
Ok(())
}