unlock/patch: preserve SCSI sense across the bridge; init aborts on dead bus; slow-retry first patch failure

Audit fixes (v1.1.0..HEAD regressions in the unlock migration + adaptive patch
speed):

- unlock_bridge ScsiAdapter: libfreemkv's transport returns Err on ANY non-zero
  SCSI status (a normal CHECK CONDITION), not only transport faults. The adapter
  was collapsing every such Err to { status: 0xFF, sense: None }, which discarded
  the parsed sense and defeated the AACS handshake's ILLEGAL_REQUEST wedge guard
  (so it kept hammering the drive — hard-rule #2) and inverted its
  transport-vs-rejection diagnosis. Now reconstruct status + the 32-byte sense
  buffer (sense_key@2, asc@12, ascq@13) and only emit 0xFF/None for a genuine
  transport fault.

- Drive::init: a genuine transport fault during the drive-prep unlock means the
  bus is dead — propagate it (the v1.1.0 invariant) instead of silently
  swallowing it via `if let Ok`. Other errors (no matching unlocker) still fall
  through to stock mode. SET CD SPEED max now runs only when the bus is alive.

- disc::patch: on the first read failure in a range, drop to slow recovery speed
  and RE-ATTEMPT the same position at slow speed before marking it. A
  single-sector range's first failing sector was being marked from a MAX-speed
  read it never got to recover.

- docs: lib.rs architecture diagram (handshake → host_certs) and README (stale
  pluggable-unlock-seam / register-unlocker / crates.io / docs.rs references).
This commit is contained in:
Matthew Jackson
2026-06-29 22:16:28 -07:00
parent b36896564f
commit 8c38cb0918
5 changed files with 83 additions and 37 deletions
+17 -5
View File
@@ -2085,20 +2085,32 @@ impl Disc {
}
Err(err) => {
// First failure in this range: the fast-batched pass over
// the clean overshoot is done; drop to the slow recovery
// speed for the rest of the range and arm the cooldown.
// Idempotent — only the first failure issues SET CD SPEED.
if !range_slowed {
// the clean overshoot is done. A genuine transport fault
// (bridge crash) is NOT a recoverable bad sector — let
// handle_read_failure abort the pass immediately rather
// than burn a slow re-read on a wedged bus.
if !range_slowed && !err.is_scsi_transport_failure() {
// Drop to the slow recovery speed, arm the cooldown,
// and RE-ATTEMPT the same position at slow speed before
// marking it. The drive's deep recovery (long re-reads
// / ECC) only engages at the slow speed; the failure so
// far is a fast-read miss. Hold the cursor (don't
// advance, don't count damage) and retry — only a
// slow-speed result reaches handle_read_failure below.
// Without this, a single-sector range's first failing
// sector was marked from a MAX-speed read it never got
// to recover. range_slowed gates this to once per range.
reader.set_speed(0x0000);
tracing::info!(
target: "freemkv::disc",
phase = "patch_speed",
lba,
speed = "0x0000",
"patch: range dropped to slow recovery speed on first read failure"
"patch: range dropped to slow recovery speed; retrying the failing read at slow speed before marking"
);
range_slowed = true;
cooldown_pending = true;
continue;
}
match handle_read_failure(