v0.25.7: BU40N firmware wedge fix in do_handshake
Pre-0.25.7 the AACS authenticate loop fired up to 16 host-cert attempts back-to-back with no pause. Each attempt is 5-10 SCSI REPORT_KEY/SEND_KEY exchanges, so on a disc whose host cert isn't in our KEYDB (or one the drive rejects), the drive saw 80-160 SCSI commands in a few hundred ms and entered a fast-fail firmware wedge state where every subsequent CDB returns sense 05/24 until power-cycled. Three defences: - MAX_CERT_ATTEMPTS capped at 3 (was 16) - 1-second sleep between attempts - Bail immediately on any sense_key == 0x05 (ILLEGAL_REQUEST) so the loop can't deepen the wedge if a regression undoes the attempt cap.
This commit is contained in:
@@ -1,5 +1,33 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.25.7 (2026-05-20)
|
||||||
|
|
||||||
|
### Fixed — BU40N firmware wedge on KEYDB-miss discs
|
||||||
|
|
||||||
|
`disc::encrypt::do_handshake` used to fire up to 16 AACS authenticate
|
||||||
|
attempts back-to-back with no pause between them. Each attempt is
|
||||||
|
5-10 SCSI REPORT_KEY/SEND_KEY exchanges. On a disc whose host cert
|
||||||
|
isn't in our KEYDB (or one the drive rejects), that's 80-160 SCSI
|
||||||
|
commands hammered at the drive in a few hundred milliseconds — and
|
||||||
|
the BU40N (plus most consumer optical drives) responds by entering
|
||||||
|
a fast-fail firmware wedge state where every subsequent CDB returns
|
||||||
|
`ILLEGAL_REQUEST/INVALID_FIELD_IN_CDB` (sense 0x05/0x24) until the
|
||||||
|
drive is physically power-cycled.
|
||||||
|
|
||||||
|
Live wedge event on rip1 2026-05-20 during a Barbie UHD scan
|
||||||
|
(KEYDB miss) confirmed the diagnosis and motivated this fix.
|
||||||
|
|
||||||
|
Defence-in-depth:
|
||||||
|
- `MAX_CERT_ATTEMPTS` capped at 3 (was 16). If three different
|
||||||
|
host certs all fail, more won't help — the drive doesn't have a
|
||||||
|
match.
|
||||||
|
- 1-second sleep between attempts. Gives the drive's firmware time
|
||||||
|
to recover internal state between auth challenges.
|
||||||
|
- Bail immediately on any sense code with sense_key = 0x05
|
||||||
|
(`ILLEGAL_REQUEST`). The drive isn't merely rejecting our cert
|
||||||
|
— it's saying "I won't talk to you anymore" — so trying more
|
||||||
|
certs would deepen the wedge.
|
||||||
|
|
||||||
## 0.25.6 (2026-05-20)
|
## 0.25.6 (2026-05-20)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "libfreemkv"
|
name = "libfreemkv"
|
||||||
version = "0.25.6"
|
version = "0.25.7"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.86"
|
rust-version = "1.86"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|||||||
+41
-3
@@ -61,9 +61,29 @@ impl Disc {
|
|||||||
"handshake starting"
|
"handshake starting"
|
||||||
);
|
);
|
||||||
|
|
||||||
const MAX_CERT_ATTEMPTS: usize = 16;
|
// v0.25.7 wedge fix. Pre-0.25.7 this loop fired up to 16 AACS
|
||||||
|
// authenticate attempts back-to-back with no pause. Each attempt
|
||||||
|
// is 5-10 SCSI REPORT_KEY/SEND_KEY exchanges. On a disc whose
|
||||||
|
// host cert isn't in our KEYDB (or one the drive rejects),
|
||||||
|
// that's 80-160 SCSI commands hammered at the drive in a
|
||||||
|
// few hundred milliseconds — and the BU40N (and most consumer
|
||||||
|
// optical drives) responds by entering a fast-fail firmware
|
||||||
|
// wedge state where every subsequent CDB returns
|
||||||
|
// ILLEGAL_REQUEST/INVALID_FIELD_IN_CDB (sense 05/24) until
|
||||||
|
// power-cycled. Hit live on rip1 2026-05-20 during a Barbie
|
||||||
|
// UHD scan: KEYDB miss → 16 cert attempts in a tight loop →
|
||||||
|
// wedge → forced host reboot + drive disconnect to recover.
|
||||||
|
//
|
||||||
|
// Defense-in-depth: cap attempts, sleep between, and bail
|
||||||
|
// early on the drive's wedge sense so any later regression
|
||||||
|
// can't undo the protection silently.
|
||||||
|
const MAX_CERT_ATTEMPTS: usize = 3;
|
||||||
|
const PER_CERT_BACKOFF_MS: u64 = 1000;
|
||||||
let mut last_err_code: Option<u16> = None;
|
let mut last_err_code: Option<u16> = None;
|
||||||
for (idx, hc) in keydb.host_certs.iter().take(MAX_CERT_ATTEMPTS).enumerate() {
|
for (idx, hc) in keydb.host_certs.iter().take(MAX_CERT_ATTEMPTS).enumerate() {
|
||||||
|
if idx > 0 {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(PER_CERT_BACKOFF_MS));
|
||||||
|
}
|
||||||
match aacs::handshake::aacs_authenticate(session, &hc.private_key, &hc.certificate) {
|
match aacs::handshake::aacs_authenticate(session, &hc.private_key, &hc.certificate) {
|
||||||
Ok(mut auth) => {
|
Ok(mut auth) => {
|
||||||
let volume_id = match aacs::handshake::read_volume_id(session, &mut auth) {
|
let volume_id = match aacs::handshake::read_volume_id(session, &mut auth) {
|
||||||
@@ -94,7 +114,24 @@ impl Disc {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
last_err_code = Some(e.code());
|
let code = e.code();
|
||||||
|
last_err_code = Some(code);
|
||||||
|
// Drive wedge senses (any with high byte 0x05 =
|
||||||
|
// ILLEGAL_REQUEST). The drive isn't merely
|
||||||
|
// rejecting our cert — it's saying "I won't talk
|
||||||
|
// to you anymore." Trying more certs makes the
|
||||||
|
// wedge worse. Bail out immediately.
|
||||||
|
let sense_key = ((code >> 8) & 0xFF) as u8;
|
||||||
|
if sense_key == 0x05 {
|
||||||
|
tracing::warn!(
|
||||||
|
target: "freemkv::disc",
|
||||||
|
phase = "handshake_wedge_detected",
|
||||||
|
cert_index = idx,
|
||||||
|
error_code = code,
|
||||||
|
"drive returned ILLEGAL_REQUEST during auth; bailing out to avoid wedge"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,7 +142,8 @@ impl Disc {
|
|||||||
host_cert_count,
|
host_cert_count,
|
||||||
tried = host_cert_count.min(MAX_CERT_ATTEMPTS),
|
tried = host_cert_count.min(MAX_CERT_ATTEMPTS),
|
||||||
last_error_code = last_err_code,
|
last_error_code = last_err_code,
|
||||||
"all host certs in KEYDB rejected by drive"
|
"all host certs in KEYDB rejected by drive (capped at {} attempts to prevent firmware wedge)",
|
||||||
|
MAX_CERT_ATTEMPTS
|
||||||
);
|
);
|
||||||
// All host certs failed — return None, not a fake success
|
// All host certs failed — return None, not a fake success
|
||||||
None
|
None
|
||||||
|
|||||||
Reference in New Issue
Block a user