v1.0.0-rc.1

CSS keyless decrypt (Stevenson), AACS 1.0/2.0/2.1, MPEG-2 DVD, multi-OS SCSI, multipass recovery, mux highway, audit hardening
This commit is contained in:
Matthew Jackson
2026-06-21 21:06:07 -07:00
parent e8bb6225ac
commit 5941c059c6
53 changed files with 7439 additions and 2340 deletions
+33 -1
View File
@@ -65,6 +65,11 @@ pub struct SgIoTransport {
pub fd: i32,
device_path: std::path::PathBuf,
pub fd_recovery: std::sync::Arc<std::sync::atomic::AtomicI32>,
/// Set to `true` by `Drop` before the transport is torn down. The
/// recovery thread checks this after a successful `compare_exchange`
/// and closes `new_fd` itself when the transport is already gone,
/// preventing an fd leak when Drop races the recovery thread.
dead: std::sync::Arc<std::sync::atomic::AtomicBool>,
}
impl SgIoTransport {
@@ -85,6 +90,7 @@ impl SgIoTransport {
fd,
device_path: device,
fd_recovery: std::sync::Arc::new(std::sync::atomic::AtomicI32::new(-1)),
dead: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
})
}
@@ -122,7 +128,12 @@ impl SgIoTransport {
hdr.flags = SG_FLAG_Q_AT_HEAD;
let ret = unsafe { libc::ioctl(fd, SG_IO as _, &mut hdr as *mut sg_io_hdr) };
if ret < 0 || hdr.status != 0 || hdr.host_status != 0 || hdr.driver_status != 0 {
// Mask DRIVER_SENSE (0x08): it only signals "sense data present", not a
// failure, and a command can complete-with-sense. Matches execute()'s
// `driver_status_real` handling (0.13.23) so a benign sense response
// here is not misread as a transport error.
let driver_status_real = hdr.driver_status & !super::DRIVER_SENSE;
if ret < 0 || hdr.status != 0 || hdr.host_status != 0 || driver_status_real != 0 {
Err(())
} else {
Ok(())
@@ -171,6 +182,11 @@ impl Drop for SgIoTransport {
let _ = Self::raw_command(self.fd, &[0x1E, 0, 0, 0, 0, 0], 3_000);
unsafe { libc::close(self.fd) };
}
// Signal the recovery thread that this transport is gone. Must
// be set before the fd_recovery swap so the recovery thread
// cannot observe dead=false and then store into an fd_recovery
// slot that Drop is no longer going to drain.
self.dead.store(true, std::sync::atomic::Ordering::Release);
// A failed execute() spawns a detached thread that opens a fresh
// fd into fd_recovery; that slot is normally drained at the top of
// the next execute(). If the transport is dropped before another
@@ -341,6 +357,7 @@ impl ScsiTransport for SgIoTransport {
self.fd = -1;
let path = self.device_path.clone();
let recovery = self.fd_recovery.clone();
let dead = self.dead.clone();
std::thread::spawn(move || {
if old_fd >= 0 {
@@ -377,7 +394,22 @@ impl ScsiTransport for SgIoTransport {
)
.is_err()
{
// Another recovery thread already stored its fd; ours
// was not stored so it's our responsibility to close it.
unsafe { libc::close(new_fd) };
return;
}
// We stored new_fd into fd_recovery. Check whether Drop
// raced us: if the transport is already dead it won't
// drain fd_recovery, so we must close new_fd ourselves.
// Use a swap to atomically claim the slot we just stored;
// if Drop already swapped it to -1 the swap returns -1
// and Drop already closed it, so we do nothing.
if dead.load(std::sync::atomic::Ordering::Acquire) {
let claimed = recovery.swap(-1, std::sync::atomic::Ordering::AcqRel);
if claimed >= 0 {
unsafe { libc::close(claimed) };
}
}
});
+51 -1
View File
@@ -161,7 +161,13 @@ impl ScsiTransport for MacScsiTransport {
let mut task_status: u8 = 0xFF;
let mut transfer_count: u64 = 0;
let cdb_len = cdb.len().min(K_MAX_CDB_SIZE) as u8;
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(),
@@ -238,6 +244,50 @@ fn cstr_to_str(bytes: &[u8]) -> &str {
std::str::from_utf8(&bytes[..end]).unwrap_or("")
}
#[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 = vec![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 = vec![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"
);
}
}
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];
+26 -9
View File
@@ -5,6 +5,11 @@
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <spawn.h>
#include <sys/wait.h>
#include <fcntl.h>
extern char **environ;
// ── Types ──────────────────────────────────────────────────────────────────
@@ -214,16 +219,28 @@ int shim_open_exclusive(const char *bsd_name) {
return 0;
}
// Use a shell wrapper so the device path is not subject to buffer limits.
// snprintf into 128 bytes could truncate long BSD names (e.g. disk12s3s1),
// producing a broken command. sh -c with $1 passes the arg via argv.
const char *shell_fmt = "sh -c 'diskutil unmountDisk force \"$1\" >/dev/null 2>&1' _ %s";
char cmd[512];
int written = snprintf(cmd, sizeof(cmd), shell_fmt, bsd_name);
if (written < 0 || (size_t)written >= sizeof(cmd)) {
return -1;
// Unmount via diskutil, invoked directly with posix_spawn (no shell) so
// the BSD device name can never be interpreted as shell syntax. A shell
// wrapper here (system()/sh -c) was a command-injection vector for an
// attacker-controlled device argument. Passing bsd_name as a discrete
// argv element also sidesteps the old buffer-truncation concern entirely.
// stdout/stderr go to /dev/null to keep diskutil chatter out of the
// caller's streams.
{
posix_spawn_file_actions_t fa;
posix_spawn_file_actions_init(&fa);
posix_spawn_file_actions_addopen(&fa, STDOUT_FILENO, "/dev/null", O_WRONLY, 0);
posix_spawn_file_actions_addopen(&fa, STDERR_FILENO, "/dev/null", O_WRONLY, 0);
char *const argv[] = {
"diskutil", "unmountDisk", "force", (char *)bsd_name, NULL
};
pid_t pid;
if (posix_spawn(&pid, "/usr/sbin/diskutil", &fa, NULL, argv, environ) == 0) {
int status;
waitpid(pid, &status, 0);
}
posix_spawn_file_actions_destroy(&fa);
}
system(cmd);
usleep(500000);
mach_port_t mp;