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.
Fixed
- ld/profile: find_by_drive_id no longer mis-binds a drive whose reported
product_id is real but uncatalogued to a sibling model's profile (and its wrong
LibreDrive microcode); the product-id-blind loose match is gated on an empty id.
Docs / CI
- Third-party NOTICE for the LibreDrive microcode bundled in profiles.json;
cargo-deny gate (#15) and audience-based comment-guard (#17).
Two failure CLASSES ran through this crate, and both had the same
shape: an outcome that means "stop" was recorded and then not acted on.
1. TRANSPORT-FAULT MISCLASSIFICATION.
`ScsiTransport::execute` is the load-bearing contract here: `Ok` even
on a SCSI sense (inspect `status`), `Err` ONLY on a transport-layer
fault. That split is the abort/fall-through boundary — a dead bus must
abort the consumer, everything else may fall through to the next
unlocker. Almost every call site got one half of it wrong.
Discarding the `Err`: `Err(_) => false` in the Renesas identity probe
(the FIRST command the unlocker issues, so a dead bus read as "not a
Renesas drive"); `_ => false` with no `Err` arm at all in the DVD
profile probe; `.map_err(|_| CssAuthFailed)` at every CSS bus-auth step,
which made the `Transport` arm of css's own error conversion
unreachable from any real call site; `Err(_)` in the MT1959 init retry
loop, which re-uploaded firmware into a dead bus three times and then
returned a generic `UnlockFailed` that classifies as "not applicable";
`continue` in the AACS cert loop, which ran three certs with a 1 s
backoff against a dead bus and reported HandshakeRejected — a
credentials problem — for what is a replug; and an unconditional
`VidUnavailable` after a successful authentication.
Ignoring the `Ok`: the AACS handshake's two SCSI helpers inspected
neither `status` nor `bytes_transferred`, so a CHECK CONDITION — which
per the contract arrives as `Ok` — handed each step back the caller's
own ZERO-FILLED buffer to parse as drive data. That is latent only
because the single transport implementor violates the contract in the
opposite direction; a conforming transport would have turned the whole
handshake into a zero-buffer success. Same hole in the MT1959 unlock
and validate reads, the OEM Volume ID read, and the CSS steps.
And once, the inverse: a merely SHORT probe response was fabricated
into a transport-failure status, hard-aborting a rip that would have
succeeded.
2. A COMPUTED-BUT-UNREAD UNLOCK FLAG.
`Mt1959::do_unlock` computes `unlocked` from three conditions, with a
comment explaining that requiring all of them "keeps any partial /
corrupted response from steering us off the cert-auth fallback". Nothing
outside the tests ever read it. `firmware_unlock` set
`drive_unlocked: true` whenever `init()` returned `Ok` — and `init()`
returning `Ok` only means the handshake completed. A half-unlocked drive
was reported to the consumer as serving clear content, which suppressed
the cert-auth fallback. Worse, each of `do_unlock`'s three checks was
individually guarded by its own length test, so a drive delivering ZERO
bytes skipped all three and returned `Ok` on a 64-byte buffer it had
never written: a fully unlocked drive, out of nothing.
Also fixed: `read_data_keys` returned AES-decrypted garbage as
`Ok((key, key))` from an all-zero response with nothing to authenticate
it; the probe's two passes walked the same 88 addresses twice per disc;
a bad bundled profiles.json cached `None` forever unlogged, making every
drive look unsupported; `DriveProfile` re-exported the MediaTek firmware
blob and the vendor CDB templates to any downstream crate; firmware
verify reads were discarded with `let _ =` despite being called verify
steps; AGIDs leaked on error paths; the VID MAC compare short-circuited;
`Copyright Information` labelled the Disc Key block; and two intra-doc
links pointed at items in another crate.
THE TEST GAP THAT HID ALL OF IT.
No mock in this crate could return `Err`, a non-zero status, or a short
transfer, and nothing drove the AACS or CSS handshakes through a
transport at all — `aacs/handshake.rs`'s 41 tests were pure math. One
test actively PINNED the bug (it fed a transport `Err` and asserted
NotApplicable). A crate-wide mock transport that expresses all three
contract outcomes now lives in `scsi.rs`; it is the red-before-green
vehicle for every fix above. `lib.rs`, `scsi.rs` and `ld/error.rs` had
zero tests and now cover the dispatch order, the sense layout and the
abort/fall-through boundary itself; `ld/cdb.rs`'s tests compiled only
under a non-default feature and now run in CI.
Behaviour changes worth knowing: a half-unlocked drive now falls through
to cert-auth instead of claiming the bus is clear; a dead bus now aborts
from the Renesas probe, the DVD probe, CSS bus-auth, MT1959 init and the
AACS cert loop instead of falling through; and drive prep issues 88
fewer SCSI commands per disc.
Per the seam audit: keep /src purely the generic contract and move everything
unlocker-specific into its module.
- ld's error type moves src/error.rs -> src/ld/error.rs; it owns the conversions
From<scsi::ScsiError> (transport fault in) and From<ld::Error> for UnlockError
(outcome out). src/scsi.rs keeps only the generic transport contract + a
generic ScsiError; the trait no longer references any concrete error.
- Remove set_max_read_speed from the Unlocker trait — raising read speed is
drive tuning, NOT bus removal. libfreemkv will issue a generic SET CD SPEED
itself at drive-open (tracked).
29 tests pass.
Replace the ld/aacs/css member-crate workspace with a single freemkv-unlock
crate: the generic Unlocker contract + SCSI transport contract at the crate
root (lib.rs, scsi.rs, error.rs), and the firmware unlocker as the self-
contained src/ld module. The contract is repo-agnostic raw types (DriveId,
HostCert, DiscKind, Unlocked, UnlockError) with NO libfreemkv dependency, so
libfreemkv will depend on this crate (one-way, no cycle) and dispatch via
all_unlockers(). ld now impl crate::Unlocker, takes its own DriveId (4 raw
fields, no INQUIRY parsing), and returns a raw Option<[u8;16]> VID. The old
aacs/css plugin wrappers are removed; their crypto moves in from libfreemkv in
the next stages. 31 tests pass.