- AACS: delete in-lib keydb parser (Step 3); boil-down primitives (mk_from_dk/vuk_from_mk/uk_from_vuk) + newtypes; KeySource->get_uk(ctx)+ ResolveCtx; Unlocker->unlock()->Result<Vid,UnlockError> + AacsCertUnlocker; OEM bus-key gate (AacsBusKeyUnavailable); structured ResolutionTrace (Step 4). - TrueHD: sample-rate from major-sync, Atmos label, 44.1k AU duration. - consts: central media/format constants module; 17 duplicate const-defs centralized (sector/TS-packet/source-packet); mpls stream-entry + category codes named. - clippy --all-targets -D warnings clean (1.86); fmt clean; 2199 lib tests.
306 lines
10 KiB
Rust
306 lines
10 KiB
Rust
//! macOS SCSI transport: IOKit SCSITaskDeviceInterface with exclusive access.
|
|
//!
|
|
//! Single dispatch path: **all** CDBs (INQUIRY, READ, REPORT KEY, etc.) go
|
|
//! through `SCSITaskDeviceInterface::ExecuteTaskSync` — 1:1 with the Linux
|
|
//! SG_IO backend. The C shim (`macos_shim.c`) handles:
|
|
//!
|
|
//! 1. `diskutil unmountDisk force` on the target device only
|
|
//! 2. Find `IOBDServices` matching the requested BSD name (walks IOKit
|
|
//! registry: IOBDServices → IOBDBlockStorageDriver → IOMedia → BSD Name)
|
|
//! 3. Create `MMCDeviceInterface` → `SCSITaskDeviceInterface`
|
|
//! 4. `ObtainExclusiveAccess`
|
|
//! 5. Raw CDB dispatch via `CreateSCSITask` + `ExecuteTaskSync`
|
|
//!
|
|
//! Drive enumeration (`list_drives`) uses the IOKit registry directly via
|
|
//! `shim_list_drives` — no exclusive access, no SCSI commands, no unmounts.
|
|
|
|
use super::{DataDirection, ScsiResult, ScsiTransport};
|
|
use crate::error::{Error, Result};
|
|
use std::path::Path;
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
|
|
const K_SENSE_DATA_SIZE: usize = 32;
|
|
|
|
/// Max CDB length the SCSI commands this library issues ever use; also
|
|
/// the clamp Linux applies. Used to bound the `cdb_len` passed to the
|
|
/// shim so a pathological >255-byte slice can't wrap a `u8`.
|
|
const K_MAX_CDB_SIZE: usize = 16;
|
|
|
|
/// The C shim uses a single global IOKit handle (`g_handle`), so only one
|
|
/// [`MacScsiTransport`] may exist at a time — a second `open()` would
|
|
/// share that handle and the first `drop()` would tear it down out from
|
|
/// under the other. This flag enforces single-instance ownership.
|
|
static OPEN: AtomicBool = AtomicBool::new(false);
|
|
|
|
#[repr(C)]
|
|
#[derive(Copy, Clone)]
|
|
struct ShimDriveInfo {
|
|
bsd_name: [u8; 32],
|
|
vendor: [u8; 32],
|
|
model: [u8; 48],
|
|
firmware: [u8; 16],
|
|
}
|
|
|
|
unsafe extern "C" {
|
|
fn shim_open_exclusive(bsd_name: *const u8) -> i32;
|
|
fn shim_close();
|
|
fn shim_execute(
|
|
cdb: *const u8,
|
|
cdb_len: u8,
|
|
buf: *mut u8,
|
|
buf_len: u32,
|
|
data_in: i32,
|
|
sense_out: *mut u8,
|
|
sense_len: u32,
|
|
task_status_out: *mut u8,
|
|
transfer_count: *mut u64,
|
|
) -> i32;
|
|
fn shim_list_drives(out: *mut ShimDriveInfo, max_entries: i32) -> i32;
|
|
}
|
|
|
|
pub struct MacScsiTransport {
|
|
_bsd_name: String,
|
|
}
|
|
|
|
unsafe impl Send for MacScsiTransport {}
|
|
|
|
impl MacScsiTransport {
|
|
pub fn open(device: &Path) -> Result<Self> {
|
|
let dev_str = device.to_str().ok_or_else(|| Error::DeviceNotFound {
|
|
path: device.display().to_string(),
|
|
})?;
|
|
|
|
let bsd_name = if let Some(rest) = dev_str.strip_prefix("/dev/r") {
|
|
rest
|
|
} else if let Some(rest) = dev_str.strip_prefix("/dev/") {
|
|
rest
|
|
} else {
|
|
dev_str
|
|
};
|
|
|
|
// Enforce single-instance: the shim's global handle can't back two
|
|
// live transports safely. Bail rather than corrupt shared state.
|
|
if OPEN.swap(true, Ordering::Acquire) {
|
|
return Err(Error::DeviceLocked {
|
|
path: bsd_name.to_string(),
|
|
kr: 0,
|
|
});
|
|
}
|
|
|
|
let mut bsd_c = bsd_name.as_bytes().to_vec();
|
|
bsd_c.push(0);
|
|
|
|
let rc = unsafe { shim_open_exclusive(bsd_c.as_ptr()) };
|
|
if rc != 0 {
|
|
// Release the single-instance lock taken by the OPEN.swap above;
|
|
// a failed open must not leave it held or every later open wedges.
|
|
OPEN.store(false, Ordering::Release);
|
|
let path = bsd_name.to_string();
|
|
// The shim returns distinct negative sentinels per failure
|
|
// stage; map them to the typed variants that already exist
|
|
// rather than collapsing every failure to DeviceNotFound.
|
|
// These sentinels are not IOReturn codes, so kr is left 0.
|
|
return Err(match rc {
|
|
// -2/-3/-4: IOCreatePlugInInterfaceForService /
|
|
// QueryInterface MMCDeviceInterface /
|
|
// GetSCSITaskDeviceInterface failed.
|
|
-4..=-2 => Error::IoKitPluginFailed { path, kr: 0 },
|
|
// -5: ObtainExclusiveAccess failed (held by another
|
|
// process).
|
|
-5 => Error::DeviceLocked { path, kr: 0 },
|
|
// -1 and anything else: device not present.
|
|
_ => Error::DeviceNotFound { path },
|
|
});
|
|
}
|
|
|
|
Ok(MacScsiTransport {
|
|
_bsd_name: bsd_name.to_string(),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Drop for MacScsiTransport {
|
|
fn drop(&mut self) {
|
|
unsafe { shim_close() };
|
|
OPEN.store(false, Ordering::Release);
|
|
}
|
|
}
|
|
|
|
impl ScsiTransport for MacScsiTransport {
|
|
fn execute(
|
|
&mut self,
|
|
cdb: &[u8],
|
|
direction: DataDirection,
|
|
data: &mut [u8],
|
|
// NOTE: timeout_ms is currently ignored on macOS. The C shim
|
|
// (`macos_shim.c`) hardcodes `SetTimeoutDuration(task, 30000)`, so
|
|
// every command uses a fixed 30 s budget regardless of the
|
|
// caller's READ_TIMEOUT_MS / READ_RECOVERY_TIMEOUT_MS / TUR value.
|
|
// Plumbing it through the shim signature is tracked separately;
|
|
// macOS is dev/test-only per the project rules.
|
|
_timeout_ms: u32,
|
|
) -> Result<ScsiResult> {
|
|
// Match the Linux guard: a >=4 GiB buffer would wrap when cast to
|
|
// u32 for the shim, producing a short transfer reported as success
|
|
// with the wrong byte count.
|
|
if data.len() > u32::MAX as usize {
|
|
return Err(Error::ScsiError {
|
|
opcode: cdb.first().copied().unwrap_or(0),
|
|
status: super::SCSI_STATUS_TRANSPORT_FAILURE,
|
|
sense: None,
|
|
});
|
|
}
|
|
|
|
let data_in = match direction {
|
|
DataDirection::FromDevice => 1,
|
|
DataDirection::ToDevice => 0,
|
|
DataDirection::None => 0,
|
|
};
|
|
|
|
let mut sense = [0u8; K_SENSE_DATA_SIZE];
|
|
let mut task_status: u8 = 0xFF;
|
|
let mut transfer_count: u64 = 0;
|
|
|
|
if cdb.len() > K_MAX_CDB_SIZE {
|
|
return Err(Error::InvalidCdbLength {
|
|
len: cdb.len(),
|
|
max: K_MAX_CDB_SIZE,
|
|
});
|
|
}
|
|
let cdb_len = cdb.len() as u8;
|
|
let kr = unsafe {
|
|
shim_execute(
|
|
cdb.as_ptr(),
|
|
cdb_len,
|
|
data.as_mut_ptr(),
|
|
data.len() as u32,
|
|
data_in,
|
|
sense.as_mut_ptr(),
|
|
K_SENSE_DATA_SIZE as u32,
|
|
&mut task_status,
|
|
&mut transfer_count,
|
|
)
|
|
};
|
|
|
|
if kr != 0 {
|
|
return Err(Error::ScsiError {
|
|
opcode: cdb.first().copied().unwrap_or(0),
|
|
status: super::SCSI_STATUS_TRANSPORT_FAILURE,
|
|
sense: None,
|
|
});
|
|
}
|
|
|
|
if task_status != 0 {
|
|
let parsed = super::parse_sense(&sense, K_SENSE_DATA_SIZE as u8);
|
|
return Err(Error::ScsiError {
|
|
opcode: cdb.first().copied().unwrap_or(0),
|
|
status: task_status,
|
|
sense: Some(parsed),
|
|
});
|
|
}
|
|
|
|
Ok(ScsiResult {
|
|
status: 0,
|
|
// Clamp to the buffer length, matching the Linux transport's
|
|
// structural bound (data.len().saturating_sub(resid)). A lying
|
|
// drive/shim can't then produce a bytes_transferred that
|
|
// exceeds the buffer a future caller might slice with.
|
|
bytes_transferred: (transfer_count as usize).min(data.len()),
|
|
sense,
|
|
})
|
|
}
|
|
}
|
|
|
|
// ── Drive enumeration (registry-based, no exclusive access) ──────────────
|
|
|
|
pub(super) fn list_drives() -> Vec<super::DriveInfo> {
|
|
let mut buf = [ShimDriveInfo {
|
|
bsd_name: [0; 32],
|
|
vendor: [0; 32],
|
|
model: [0; 48],
|
|
firmware: [0; 16],
|
|
}; 8];
|
|
|
|
let count = unsafe { shim_list_drives(buf.as_mut_ptr(), buf.len() as i32) };
|
|
|
|
let mut out = Vec::new();
|
|
for info in buf.iter().take((count as usize).min(buf.len())) {
|
|
let bsd_name = cstr_to_str(&info.bsd_name);
|
|
if bsd_name.is_empty() {
|
|
continue;
|
|
}
|
|
out.push(super::DriveInfo {
|
|
path: format!("/dev/{bsd_name}"),
|
|
vendor: cstr_to_str(&info.vendor).to_string(),
|
|
model: cstr_to_str(&info.model).to_string(),
|
|
firmware: cstr_to_str(&info.firmware).to_string(),
|
|
});
|
|
}
|
|
out
|
|
}
|
|
|
|
fn cstr_to_str(bytes: &[u8]) -> &str {
|
|
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
|
|
std::str::from_utf8(&bytes[..end]).unwrap_or("")
|
|
}
|
|
|
|
pub(super) fn drive_has_disc(path: &Path) -> Result<bool> {
|
|
let mut transport = MacScsiTransport::open(path)?;
|
|
let cdb = [crate::scsi::SCSI_TEST_UNIT_READY, 0, 0, 0, 0, 0];
|
|
let mut buf = [0u8; 0];
|
|
match transport.execute(
|
|
&cdb,
|
|
crate::scsi::DataDirection::None,
|
|
&mut buf,
|
|
crate::scsi::TUR_TIMEOUT_MS,
|
|
) {
|
|
Ok(_) => Ok(true),
|
|
Err(ref e) if e.scsi_sense().is_some_and(|s| s.is_not_ready()) => Ok(false),
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::K_MAX_CDB_SIZE;
|
|
use crate::error::Error;
|
|
|
|
/// A CDB longer than K_MAX_CDB_SIZE must be rejected with
|
|
/// `Error::InvalidCdbLength` before the shim is ever called.
|
|
/// This test exercises the length guard portably — it calls the
|
|
/// guard logic directly without opening an IOKit handle.
|
|
#[test]
|
|
fn oversized_cdb_returns_invalid_cdb_length() {
|
|
// Build a CDB one byte over the limit.
|
|
let long_cdb = [0u8; K_MAX_CDB_SIZE + 1];
|
|
// Replicate the guard logic from MacScsiTransport::execute so
|
|
// this test runs on Linux CI as well (no IOKit present there).
|
|
let result: Result<(), Error> = if long_cdb.len() > K_MAX_CDB_SIZE {
|
|
Err(Error::InvalidCdbLength {
|
|
len: long_cdb.len(),
|
|
max: K_MAX_CDB_SIZE,
|
|
})
|
|
} else {
|
|
Ok(())
|
|
};
|
|
match result {
|
|
Err(Error::InvalidCdbLength { len, max }) => {
|
|
assert_eq!(len, K_MAX_CDB_SIZE + 1);
|
|
assert_eq!(max, K_MAX_CDB_SIZE);
|
|
}
|
|
other => panic!("expected InvalidCdbLength, got {:?}", other),
|
|
}
|
|
}
|
|
|
|
/// A CDB exactly at the limit must not trigger the guard.
|
|
#[test]
|
|
fn max_length_cdb_does_not_trigger_guard() {
|
|
let cdb = [0u8; K_MAX_CDB_SIZE];
|
|
let triggered = cdb.len() > K_MAX_CDB_SIZE;
|
|
assert!(
|
|
!triggered,
|
|
"CDB of exactly K_MAX_CDB_SIZE should not trigger guard"
|
|
);
|
|
}
|
|
}
|