Grep-based test banning raw std::thread::sleep / thread::sleep calls in
production code under src/aacs, src/css and src/ld, per Stop design v5
§5.8. clippy's disallowed-methods can't scope to a subdirectory, hence
a dedicated test rather than a lint config entry. No violations found;
this is a pure addition.
WallClock's wait between TEST UNIT READY polls is now the transport's
cancellable pause (stop-design-v5 §2.3), so a Stop during the poll ends it
at once. The refusal is the dead-bus shape; libfreemkv's Drive::init
reclassifies it as Halted (ST-L2).
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.
stop-design-v5 §2.3 / §5.2 (ST-U1), first commit of the two-commit shape.
Additive: four defaulted ScsiTransport methods (pause, begin_critical,
end_critical, execute_cleanup), so all 32 in-repo impls compile unchanged,
plus the CriticalGuard and AgidGuard types and the StopFake test host that
behaves like libfreemkv's adapter after a Stop.
Red on this commit (nothing is wired yet): UT2 v1/v2 and the CSS release
test (a held AGID is released via execute, which a Stop refuses), UT3,
UT6 (FwFront does not forward), UT7/UT8 (the verify / F1 are refused
mid-upload), UT9 backoff (a real 1 s sleep, no pause). Green: UT1, UT4
(GUARD, D9), UT5 (NEW).
Review of ST-D10 (changes required). await_media_ready now returns a
MediaReady outcome, and run_init keeps the unlock for every outcome, so
LdUnlocker still reports Some and runs probe_disc and the VID read. It
fails only on a dead bus or a Stop. The NotReady ld error (7104) is gone.
It follows libfreemkv wait_ready's terminal states, quoting MMC-6 r02g
Annex F. After 10 x 02/3A (MEDIUM NOT PRESENT) with no 02/04/01 it returns
NoMedium. On 02/04/02 (INITIALIZING CMD. REQUIRED) it sends one START
STOP UNIT (1B 00 00 00 01 00, Table 633 "Start the disc and make ready
for access"), counted as progress. On 02/30/xx it returns Incompatible
at once, and on sense keys 3/4/5 DriveError at once. UNIT ATTENTION
06/29/00 and 06/28/00 keep polling. A T6 stall returns Stalled (warn),
still Ok.
An Err carrying a sense (libfreemkv's adapter returns non-zero status that
way) is a drive answer, not a dead bus. TODO: the SKSV progress indicator
(T6 part 2). New LdUnlocker-level test: a 60 s stall after a confirmed
upload still reports Some, and probe_disc runs after the poll.
Review of ST-D10 (changes required). LdUnlocker exits on `mt.init(scsi)?`,
so the poll's NotReady error turned a real unlock into NotApplicable,
skipping probe_disc and the VID read. Red on 24909ba: a stall, a flapping
drive, 10 x 02/3A without 04/01, 02/30/xx, sense keys 3/4/5 and an Err
carrying a sense (libfreemkv's adapter shape) are errors or 60 s stalls,
and 02/04/02 sends no START STOP UNIT. Guards (green): UNIT ATTENTION
06/29, 06/28 then ready; 3A after 04/01 keeps polling.
A confirmed load_firmware proves only that the new firmware answers vendor
READ BUFFERs. libfreemkv's session runs wait_ready before init and never
after, so run_probe's READ CAPACITY could see NOT READY, probe a UHD disc at
the BD address, and spin validate's five retries.
After a confirmed upload, run_init now polls TEST UNIT READY every 500 ms
until the drive is ready. Per T6 (stop-design-v5 §2.11), progress is a
(sense key, ASC, ASCQ) answer not yet seen in this poll; it fails with the
new internal ld Error::NotReady (7104, maps to NotApplicable) only after
60 s without progress. There is no total cap, and a dead bus aborts at
once. The wait sits behind a ReadyClock seam, so the tests run the window
on a virtual clock. ST-U1 turns the wait into the cancellable pause.
Review of ST-D10: a confirmed load_firmware proves only that the new
firmware answers vendor READ BUFFERs, not that the media is ready, and
libfreemkv's session calls wait_ready before init, never after. Red on
712e58d: run_init returns without a TEST UNIT READY. UT10 now also expects
the one ready TUR.
load_firmware returns Ok only after its own do_unlock succeeded. run_init
then slept 10 s and re-ran do_unlock, and on attempt 2 fell out of the loop
returning Err(last_err), discarding a real unlock. Return Ok at once; the
firmware upload itself is unchanged (stop-design-v5 §1 D10, ST-D10).
stop-design-v5 §5.2 UT10 / G3 (D10). Red on dev: after a confirmed
load_firmware, run_init settles 10 s and loops back into do_unlock, and a
confirmation on the last attempt is discarded as Err(last_err).
Share one IDENTITY reply fixture; assert UnsupportedFirmware/InvalidArgument;
cover the v2 length gate under P-256 and dead-bus/rejected VID after an arm.
Adds the org doc-links caller, cargo test --doc, and a consumers job building
libfreemkv and bdemu against this commit. ci/qa default to contents: read.
0.8.x de-bus via 0x06 is hardware-dependent, not validated; 0.7.1 replies "freemkv 0.7.1".
Note arm_stealth_oem's version gate vs raw reset(); FwMock Bd slot at its default.
Parse only the IDENTITY version token; refuse fw < 0.9 in FreemkvUnlocker and every arm recipe.
AACS arm runs before the cert check; an Encryption=off arm returns the bare VID. Unlocked zeroizes on drop.
Mark the 1.0 drive dead only for a type-0x01 cert failing LA verify or a type-0x11
cert; zeroed/unknown types and step-6/9 failures stay per-cert. Log the skip.
Drop the unreachable ILLEGAL_REQUEST wedge arm; stop retrying host certs after a
drive-side 1.0 verify failure; skip truncated/known-dead v1 creds up front; read
fmt 0x84 only from a Bus Encryption Capable drive; accept unreduced keydb scalars.
find_by_drive_id ran the four-field fallback per section, so an earlier
section's fallback beat a later section's exact match. Exact pass now spans
all sections first; fallback uniqueness is checked across all sections.
The 1.7.6 unified release ships no rip-path change (Linux desktop shell,
notifications, menu_layout refactor, XDG paths). Real-media QA on EC2
adds ~2h + real money to the release cycle to prove code that never
enters the rip pipeline. This empty commit puts `[skip-media]` in qa's
HEAD so the media matrix in qa.yml auto-skips per the documented bypass;
the full-platform builds/tests still run.
Release-orchestrate committed to qa (v1.7.5 version bump, etc.) but did
not push those commits back to dev, so promote's fast-forward gate
refused (qa was diverged from dev). This is an ours-merge: the commit
records qa as an ancestor of dev without applying its content, since
dev has already moved past 1.7.5 to 1.7.6 with the equivalent
information in place.
Bumps Cargo.toml + Cargo.lock to 1.7.6. Also fixes a CI-shape defect the
previous release left behind: dev's CHANGELOG still said
`## [1.7.5] — UNRELEASED` after 1.7.5 shipped (release-orchestrate dated
main but did not push a fresh UNRELEASED heading back to dev). Corrects
that here so the changelog job accepts 1.7.6 on the way to qa:
## [1.7.6] — UNRELEASED
- Version aligned; unified release driven by freemkv 1.7.6.
## [1.7.5] — 2026-09-23
- Version aligned (or actual content, per the shipped main entry).
The driver for 1.7.6 is the freemkv Linux desktop shell (GTK4 +
libadwaita) + rip-finished desktop notification (issue #56).
Firmware 0.9.0 consolidates the Ake (0x06) and Bus (0x07) features into a
single Encryption (0x06) lever; wire id 0x07 no longer exists. The separate
Bus lever was proven inert on BU40N/MT1959 — the one Encryption lever
de-busses on its own — so the unlock recipe now sets one flag where it used
to set two, and the trailing best-effort Set(Bus, off) is gone.
Set(Encryption, STATE_OFF) is the load-bearing step: the drive acts
pre-authenticated, so a bare 0xAD returns the VID with no cert and no AKE,
and content comes back de-bussed. Failure still maps to VidUnavailable.
Hardware-validated against fw 0.9.0 on BU40N 1.00 with a UHD disc: every
sampled unit returns de-bussed and opens with the AACS unit key resolved
from the live keyserver.
Replace comment/leak/identity guard workflows with thin callers into
freemkv/.github reusable guards (guard-comment/leak/identity @v1); fork
PRs still run, same-repo pushes aren't double-run. Delete now-unused
local ci/comment-guard.py and ci/leak-guard.sh. Add top-level
concurrency (cancel-in-progress) to ci.yml and qa.yml and standardize
actions/checkout to @v5.
arm_bypass_bd/arm_bypass_uhd now also Set(Hrl, off) (revocation skip) so a
revoked/absent cert never trips the HRL check on a keyless bypass rip; tests
updated for the new SET/verify order.
FreemkvUnlocker::set sent the vendor SET with DataDirection::None + an
empty buffer, but the SET CDB advertises a MIN_ALLOC_LEN (64B) data-in
allocation and the drive ABORTS a no-data transfer (CHECK CONDITION /
Aborted Command — HW-confirmed on BU40N fw 0.8.1). Every Set(feature)
failed, so the load-bearing Set(Ake,off) failed and the whole freemkv
unlock aborted, silently falling back to the AACS/online key route
(autorip showed 'Unlockers — freemkv: no'). Mirror the working
FirmwareControl::set (exec_in): read the state table back over a
FromDevice phase and ignore it. Adds a regression test modelling the
drive's abort-without-data-phase contract.
Remove the HRL_WIPE_ONCE const, the wipe_hrl_once() setter, and their tests/doc references from the host library so wipe-once is no longer part of the host-facing firmware ABI. HRL exposes only skip (0x01).
Lockstep with freemkv-fw BD-refuse stealth fix: the BD force-disable wire
value moves from STATE_OFF (0x00, the SRAM boot value) to a distinct
sentinel STATE_BD_DISABLE (0x02), so an unarmed firmware image stays
OEM-identical. disable_bd() now sends 0x02; add the const and align the
Feature::Bd / Feature::Uhd ABI-mirror docs (enable dir / STATE_OFF are
OEM no-ops).
Fix two re-audit findings:
1. LOW (correctness): run_cert_handshake_with_anchors computed the
up-front cert-skip has_v2 gate WITHOUT allow_p256, unlike
attempt_one_cert. In production (allow_p256=false) a dead-v1 cert
carrying v2 creds was not skipped up front, reached the drive, failed
the step-7 self-verify, and consumed a MAX_CERT_ATTEMPTS slot -- so
[dead+v2 x3, valid] exhausted the cap and never tried the valid cert.
Mirror attempt_one_cert's gate exactly. Add a regression test.
2. LOW (key hygiene): add the zeroize crate and wipe sensitive key
material on drop -- HostCert private keys, AacsAuth/CertHandshake
derived bus_key/volume_id/read_data_key (ZeroizeOnDrop, non-secret
fields skipped), the ephemeral AKE scalars and raw private-scalar byte
buffers (Zeroizing). BigUint scalars/shared-x are a documented
best-effort residual (num-bigint exposes no zeroization). Debug
redaction and the EC math are untouched.
MEDIUM (coupled):
- aacs/handshake: on the AACS 1.0 path a type-0x11 drive cert is now
REJECTED (verify_cert + step-6 drive-key signature always run) instead
of accepted unverified — the old 0x11-skip did neither yet still ran
ECDH, an attacker-chosen-bus-key hole. run_cert_handshake routes a
genuine 0x11 drive to the native P-256 AKE via the v2-cred fallback.
- aacs/handshake: a post-auth VID/MAC failure (AacsVidMac) with v2 creds
present now falls through to aacs2_authenticate_p256 instead of a
terminal VidUnavailable, so a backward-compat 2.0 drive that fails the
1.0 VID MAC still reaches the P-256 path.
LOW:
- Gate the native AACS 2.0 (P-256) path behind AACS2_P256_EXPERIMENTAL
(default off) at the run_cert_handshake boundary: its cert offsets are
provisional, so production can't silently mis-verify a real disc.
- Release the AGID on the fully-successful handshake too (was leaked,
slowly draining the drive's 4-AGID pool).
- Rejection-sample the ephemeral ECDH private key (drop the modulo bias
of raw % n) in both generate_host_key_pair{,_p256}.
- Guard shared.infinity in compute_bus_key{,_p256} (return None).
- Remove the dead, truncating AacsAuth.drive_cert field.
- Wire UnlockError::NoUsableHostCert for the all-certs-skipped / empty
path (more accurate than HandshakeRejected).
- css: promote crypt_key's debug_assert! bounds checks to real assert!.
Handshake tests reworked onto an anchor-parametrised seam
(run_cert_handshake_with_anchors / *_with_anchor) so the DriveEmu drives
a genuine LA-signed 1.0 AKE under a self-generated test anchor; adds the
0x11-rejection, VID-MAC→P-256 fallback (HybridDrive), AGID-release,
infinity-guard, NoUsableHostCert and crypt_key-assert tests.
Add src/firmware: a hand-kept mirror of freemkv-fw/src/abi.rs (Verb/Feature
enums with pinned repr(u8) values, state constants, CDB builders) plus an
ergonomic FirmwareControl<&mut dyn ScsiTransport> with identity/get/set/reset/
dump, typed setters, states(), and the named recipes (arm_oem_bd, arm_oem_uhd,
arm_bypass_bd, arm_bypass_uhd, arm_stealth_oem). A drift test pins every wire
value against abi.rs.
Migrate FreemkvUnlocker off the old SubFn frame onto the new verb/feature
grammar via the shared builders (region-free=Set(Region,on),
riplock=Set(Speed,max), transport unlock=Set(Ake,null), bus-off=Set(Bus,off)).
Wire opt-in arming into AacsUnlocker::arm_before_unlock (OFF by default): when
set, unlock() detects freemkv via IDENTITY and applies a recipe before the AKE;
a non-freemkv drive or a refused recipe is left untouched.
Add `aacs1_keypair_matches(private_key, cert)` — true iff a stored AACS 1.0
host private key matches its certificate's public key (`priv·G ==
cert_pub_key`), reusing the live handshake's own curve math. A genuine,
LA-signed cert can still be paired in a keydb with the wrong private key,
which never establishes a bus key ("KEY NOT ESTABLISHED"); this primitive
distinguishes that DEAD pairing from a live one. Exposed for the key
service's host-cert health gate. Add `AacsHostSignVerify` (code 7016) for a
host-side self-verify guard on our own produced signature.
Also rework `run_cert_handshake` cert selection: on a rejected/revoked cert
the handshake now backs off and tries the next candidate instead of failing,
with a `cert_host_id_hex` helper and DriveEmu extensions + tests covering
single-valid, back-off-to-second, and roll-past-revoked paths.
find_by_drive_id gated the product-id-blind four-field fallback behind an empty
product_id in 1.7.0, so a drive reporting a specific product_id (e.g. an LG BU40N
reporting "BD-RE BU40N") no longer matched its catalog entry (generic "BD-RE"),
silently skipping the LibreDrive firmware unlocker — the rip fell through to the
AACS host-cert path (LD: no, AACS: yes).
Run the four-field fallback (vendor/revision/vendor_specific/firmware_date)
unconditionally but ambiguity-guarded: bind a UNIQUE catalog match regardless of
the reported product_id (the real-drive case), and require the exact product_id
only when catalogued siblings genuinely share the four-field identity — preserving
the anti-mis-bind protection the 1.7.0 gate intended. Regression test added.
Bump to 1.7.1.
identify() requested a 32-byte data-in allocation while the firmware commits
KNOCK_RESP_LEN (64) bytes on every subfn. The short allocation desyncs the first
command's data-in phase and wedges the drive, so the later bare 0xAD VID read
aborts (CHECK CONDITION) — the "Raw Read GOOD but no VID" failure that left the
keyserver with vid=none. Request KNOCK_RESP_LEN like every other knock; update the
CDB-shape tests. Also sync Cargo.lock to the 1.7.0 manifest.
Every unlocker now implements one method — unlock(&self, scsi, ctx) ->
Result<Option<Unlocked>> — with identical workflow and error handling,
differing only in CDBs/checks. Drops the unlock_features/unlock_bus split,
per-unlocker result state, and the drive_unlocked flag ("unlocked" is just
Some). Only a dead bus is Err; everything else falls through as Ok(None).
- VID read is best-effort everywhere via the shared crate::vid reader: a
VID miss no longer discards an unlock that already removed the bus — the
bug that dropped a freemkv-unlocked drive to the cert path and sent the
keyserver vid=none.
- AACS: host certs injected at construction (AacsUnlocker::new(certs)), not
passed per-call; a successful cert AKE is a real unlock.
- Renesas/Pioneer: the vendor open enables raw reads (LibreDrive parity),
then reads the VID via the same 0xAD reader — no host cert, matching the
firmware routes.
- Rename the MT1959 unlocker to LD (matches the ld/ module); the MediaTek
MT1959 platform driver keeps its name.
- Consumer assembles its own dispatch list from the exported types; no
central factory. Adds a uniform dead-bus contract test over all unlockers.
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).
Matches the ld/ platform-folder convention (MediaTek lives in src/ld/); the
module/struct spelling is normalized renesis->renesas to match the drives it
detects. Pure move + identifier rename, no behavior change.
Declared rust-version and the ci.yml/qa.yml toolchain pins both sit at 1.88 (the
edition-2024 floor once zip 8 and let-chains are accounted for), so CI builds on
exactly the MSRV: an accidental bump to a newer toolchain fails CI by design and
moving the floor takes a deliberate pin edit. The uninlined_format_args allow
reconciles 1.88's clippy with the allow-by-default it became in 1.97+.
Extends the fake ScsiTransport in css::mod to run the full CSS bus-auth
happy path (variant brute-force, drive-challenge/Key2 exchange, best-effort
disc-key REPORT KEY, DvdUnlocker::unlock_bus success) instead of only its
failure branches.
Covers mt1959's run_init retry loop (signature-mismatch abort, generic
do_unlock failure with firmware-reload fallback, firmware-reload transport
fault), run_probe's disc-type detection (capacity sense fallback, UHD vs BD
init address, pass-2 early-end on a rejected probe), and the PlatformDriver
guard clauses.
Covers aacs/handshake's EC math edge cases (mod_inv/ec_add/ec_double/ec_mul
defensive branches, off-curve pubkey rejection), the Debug redaction impls,
the AACS 1.0 type-0x01 cert-verify gate, the P-256 thin wrapper + short-cert
guard + step-6 bad-signature rejection, and drives run_cert_handshake to its
full Ok(CertHandshake) success path via an extended drive emulator.
Adds unit tests for aacs::error::Error and css::error::Error (code(),
scsi_sense/is_transport_failure branches, From<ScsiError> conversions),
a FIPS-197 known-answer test for aacs::aes_ecb_decrypt, and scripted
fake-transport tests covering the authenticate_with_agid / read_disc_key
error branches in css::mod. Raises line coverage 91.16% -> 92.59%.