diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ca1807..7e1e7e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # 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) ### Changed diff --git a/Cargo.toml b/Cargo.toml index 269a857..19a9b29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libfreemkv" -version = "0.25.6" +version = "0.25.7" edition = "2024" rust-version = "1.86" license = "AGPL-3.0-only" diff --git a/src/disc/encrypt.rs b/src/disc/encrypt.rs index 70d9f04..ff75b84 100644 --- a/src/disc/encrypt.rs +++ b/src/disc/encrypt.rs @@ -61,9 +61,29 @@ impl Disc { "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 = None; 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) { Ok(mut auth) => { let volume_id = match aacs::handshake::read_volume_id(session, &mut auth) { @@ -94,7 +114,24 @@ impl Disc { }); } 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; } } @@ -105,7 +142,8 @@ impl Disc { host_cert_count, tried = host_cert_count.min(MAX_CERT_ATTEMPTS), 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 None