0.31.0: hardening and correctness pass across mux, codec, AACS/CSS, UDF/MPLS/CLPI, recovery, drive/SCSI, labels, and I/O
Library-wide review-and-fix pass: tightened AACS keydb/handshake/variant handling and trailing-partial-unit policy, corrected MPLS mark offset and added UDF allocation bounds, hardened the mux/codec framing and M2TS paths, guarded SCSI READ CAPACITY short transfers and unified error mapping, added overflow guards on untrusted disc input, and made prefetch shutdown deterministic. Release profile now builds with thin LTO + single codegen unit.
This commit is contained in:
@@ -76,8 +76,17 @@ pub fn detect_fd(fd: std::os::unix::io::RawFd) -> FsType {
|
||||
detect_fd_impl(fd)
|
||||
}
|
||||
|
||||
/// Non-Linux stub for [`detect_fd`].
|
||||
///
|
||||
/// Always returns [`FsType::Unknown`]: only Linux keys its writeback
|
||||
/// policy off this classification, so other platforms have nothing to
|
||||
/// detect. The `fd` parameter is a bare `i32` rather than
|
||||
/// `std::os::unix::io::RawFd` because this arm also compiles on Windows,
|
||||
/// which has no `RawFd` — the universal integer keeps one signature across
|
||||
/// all non-Linux targets. Unused on these targets (no caller invokes it),
|
||||
/// hence `allow(dead_code)`.
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[allow(dead_code)] // API parity with the linux impl; callers cfg-gate.
|
||||
#[allow(dead_code)]
|
||||
pub fn detect_fd(_fd: i32) -> FsType {
|
||||
FsType::Unknown
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
//!
|
||||
//! Heuristic-only: any UNC path (`\\server\share\...`) is treated as a
|
||||
//! network mount and bucketed into `Nfs`. Strictly, SMB is not NFS, but
|
||||
//! the buffering-policy outcome for our purposes is the same — there is
|
||||
//! no platform `WritebackFile` machinery on Windows yet, so the worst
|
||||
//! case of a false positive is using `LocalFileSink` regardless. A
|
||||
//! the buffering-policy outcome is the same here — there is no platform
|
||||
//! `WritebackFile` machinery on Windows yet, so both `Nfs` and `Local`
|
||||
//! select `LocalFileSink`. A misclassification in either direction is
|
||||
//! therefore harmless on Windows today: the sink choice does not change. A
|
||||
//! proper `GetVolumeInformation` query is a Phase 4 concern.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
@@ -18,6 +18,8 @@ const BUFFER_ID_B: u8 = 0x77;
|
||||
// ── SCSI opcodes ──────────────────────────────────────────────────────
|
||||
const SCSI_READ_BUFFER: u8 = 0x3C;
|
||||
const SCSI_READ_CAPACITY: u8 = 0x25;
|
||||
/// Shared by both firmware-upload variants (see `variant_a` / `variant_b`).
|
||||
pub(super) const SCSI_WRITE_BUFFER: u8 = 0x3B;
|
||||
|
||||
// ── Sub-commands (shared A/B) ─────────────────────────────────────────
|
||||
const SUB_CMD_UNLOCK: u8 = 0x00;
|
||||
@@ -111,6 +113,14 @@ impl Mt1959 {
|
||||
buf: &mut [u8],
|
||||
expected: usize,
|
||||
) -> Result<usize> {
|
||||
// The READ_BUFFER CDB transfer-length is a single byte; an
|
||||
// `expected` above 255 cannot be expressed and would silently
|
||||
// truncate. All in-crate callers pass small fixed sizes (4); guard
|
||||
// the invariant rather than emit a malformed CDB.
|
||||
debug_assert!(
|
||||
expected <= u8::MAX as usize,
|
||||
"read_buffer_probe expected exceeds 1-byte CDB length field"
|
||||
);
|
||||
let cdb = self.read_buffer_sub(sub_cmd, address, expected as u8);
|
||||
let result = scsi.execute(&cdb, DataDirection::FromDevice, buf, 5_000)?;
|
||||
if result.bytes_transferred != expected {
|
||||
@@ -146,16 +156,23 @@ impl Mt1959 {
|
||||
0x00,
|
||||
];
|
||||
let mut response = vec![0u8; UNLOCK_RESPONSE_SIZE as usize];
|
||||
scsi.execute(&cdb, DataDirection::FromDevice, &mut response, 30_000)?;
|
||||
let result = scsi.execute(&cdb, DataDirection::FromDevice, &mut response, 30_000)?;
|
||||
|
||||
if response.len() >= 4 && response[0..4] != self.profile.signature {
|
||||
// `response` is a fixed 64-byte buffer, so `response.len()` is
|
||||
// always >= every offset below — the meaningful bound is how many
|
||||
// bytes the drive actually delivered. Validate against
|
||||
// `bytes_transferred` so a short/partial transfer (stale trailing
|
||||
// zeros) can't be read as if the drive sent real marker bytes.
|
||||
let n = result.bytes_transferred.min(response.len());
|
||||
|
||||
if n >= 4 && response[0..4] != self.profile.signature {
|
||||
return Err(Error::SignatureMismatch {
|
||||
expected: self.profile.signature,
|
||||
got: response[0..4].try_into().unwrap_or([0; 4]),
|
||||
});
|
||||
}
|
||||
|
||||
if response.len() >= FIRMWARE_ACTIVE_OFFSET + 4
|
||||
if n >= FIRMWARE_ACTIVE_OFFSET + 4
|
||||
&& response[FIRMWARE_ACTIVE_OFFSET..FIRMWARE_ACTIVE_OFFSET + 4] != FIRMWARE_ACTIVE_SIG
|
||||
{
|
||||
return Err(Error::UnlockFailed);
|
||||
@@ -170,7 +187,7 @@ impl Mt1959 {
|
||||
// the rest of the response. Requiring both before we tell the
|
||||
// upper layer "OEM path is live" keeps any partial / corrupted
|
||||
// response from steering us off the cert-auth fallback.
|
||||
self.unlocked = response.len() >= FIRMWARE_MODE_OFFSET + 4
|
||||
self.unlocked = n >= FIRMWARE_MODE_OFFSET + 4
|
||||
&& response[FIRMWARE_ACTIVE_OFFSET..FIRMWARE_ACTIVE_OFFSET + 4] == FIRMWARE_ACTIVE_SIG
|
||||
&& response[FIRMWARE_MODE_OFFSET..FIRMWARE_MODE_OFFSET + 4] == FIRMWARE_MODE_SIG;
|
||||
|
||||
@@ -272,7 +289,11 @@ impl Mt1959 {
|
||||
.execute(&cap_cdb, DataDirection::FromDevice, &mut cap_buf, 5_000)
|
||||
.is_ok()
|
||||
{
|
||||
u32::from_be_bytes([cap_buf[0], cap_buf[1], cap_buf[2], cap_buf[3]]) + 1
|
||||
// last_lba + 1 = sector count. A 0xFFFFFFFF last-LBA is the
|
||||
// READ CAPACITY(10) "capacity exceeds 32 bits" sentinel; saturate
|
||||
// rather than wrap to 0 (which would misclassify a huge disc as
|
||||
// BD). A saturated count stays above the UHD threshold -> UHD.
|
||||
u32::from_be_bytes([cap_buf[0], cap_buf[1], cap_buf[2], cap_buf[3]]).saturating_add(1)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
@@ -6,17 +6,28 @@ use super::Mt1959;
|
||||
use crate::error::Result;
|
||||
use crate::scsi::{DataDirection, ScsiTransport};
|
||||
|
||||
const SCSI_WRITE_BUFFER: u8 = 0x3B;
|
||||
use super::SCSI_WRITE_BUFFER;
|
||||
|
||||
const VERIFY_BUFFER_ID: u8 = 0x45;
|
||||
|
||||
/// WRITE_BUFFER carries a 24-bit transfer length, so a firmware blob
|
||||
/// larger than this cannot be uploaded in one command.
|
||||
const WRITE_BUFFER_MAX_LEN: usize = 0x00FF_FFFF;
|
||||
|
||||
pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
||||
let firmware = &mt.profile.firmware;
|
||||
if firmware.is_empty() {
|
||||
return Err(crate::error::Error::UnlockFailed);
|
||||
}
|
||||
|
||||
// Upload firmware via WRITE_BUFFER
|
||||
// Upload firmware via WRITE_BUFFER. The CDB's length is a 24-bit field;
|
||||
// if the blob exceeds that, the encoded length would silently disagree
|
||||
// with the bytes actually sent (`data`). Reject rather than upload a
|
||||
// length-mismatched command.
|
||||
let len = firmware.len();
|
||||
if len > WRITE_BUFFER_MAX_LEN {
|
||||
return Err(crate::error::Error::UnlockFailed);
|
||||
}
|
||||
let cdb = [
|
||||
SCSI_WRITE_BUFFER,
|
||||
0x06,
|
||||
@@ -53,8 +64,11 @@ pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Re
|
||||
5_000,
|
||||
);
|
||||
|
||||
// Double unlock after firmware upload
|
||||
mt.do_unlock(scsi)?;
|
||||
// Double unlock after firmware upload. The first establishes the
|
||||
// unlock and is fatal on failure; the second is a confirmation pass and
|
||||
// is best-effort (matching variant B), so a benign hiccup on the
|
||||
// redundant call doesn't fail an already-successful unlock.
|
||||
mt.do_unlock(scsi)?;
|
||||
let _ = mt.do_unlock(scsi);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
//!
|
||||
//! MODE SELECT (0x55) → read metadata → WRITE_BUFFER → vendor verify (0xF1) → unlock × 5+1
|
||||
|
||||
use super::Mt1959;
|
||||
use super::{Mt1959, SCSI_READ_BUFFER, SCSI_WRITE_BUFFER};
|
||||
use crate::error::Result;
|
||||
use crate::scsi::{DataDirection, ScsiTransport};
|
||||
|
||||
const SCSI_MODE_SELECT: u8 = 0x55;
|
||||
const SCSI_WRITE_BUFFER: u8 = 0x3B;
|
||||
const SCSI_READ_BUFFER: u8 = 0x3C;
|
||||
const FIRMWARE_MAX_SIZE: usize = 0x9C0;
|
||||
const FIRMWARE_EXTRA: [u8; 16] = [0; 16];
|
||||
const VENDOR_VERIFY: [u8; 10] = [0xF1, 0x01, 0x02, 0x00, 0x0D, 0x30, 0x01, 0xF3, 0xAD, 0x23];
|
||||
@@ -19,7 +17,13 @@ pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Re
|
||||
return Err(crate::error::Error::UnlockFailed);
|
||||
}
|
||||
|
||||
// Step 1: Upload firmware via MODE SELECT
|
||||
// Step 1: Upload firmware via MODE SELECT. Variant-B firmware blobs are
|
||||
// exactly FIRMWARE_MAX_SIZE; a larger blob means a corrupt/wrong profile,
|
||||
// and silently truncating it would upload a partial image that can't
|
||||
// unlock. Reject it explicitly instead.
|
||||
if firmware.len() > FIRMWARE_MAX_SIZE {
|
||||
return Err(crate::error::Error::UnlockFailed);
|
||||
}
|
||||
let write_len = FIRMWARE_MAX_SIZE.min(firmware.len());
|
||||
let mode_select_cdb = [
|
||||
SCSI_MODE_SELECT,
|
||||
@@ -77,7 +81,11 @@ pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Re
|
||||
let mut dummy = [0u8; 0];
|
||||
let _ = scsi.execute(&VENDOR_VERIFY, DataDirection::None, &mut dummy, 5_000);
|
||||
|
||||
// Step 5: Unlock retries (up to 5, then final attempt)
|
||||
// Step 5: Unlock retries (up to 5, then a final fatal attempt). On a
|
||||
// successful unlock we issue one confirmation pass; its result is
|
||||
// intentionally best-effort — the first call already established the
|
||||
// unlock state, so a hiccup on the redundant confirmation must not fail
|
||||
// an otherwise-good unlock.
|
||||
for _attempt in 0..5 {
|
||||
if mt.do_unlock(scsi).is_ok() {
|
||||
let _ = mt.do_unlock(scsi);
|
||||
|
||||
Reference in New Issue
Block a user