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:
+33
-1
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user