From a1f4dff6f6fb2c7810a52e6f0990c9f1657c50ec Mon Sep 17 00:00:00 2001 From: MattJackson <1085847+MattJackson@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:17:20 -0700 Subject: [PATCH] =?UTF-8?q?v0.16.0:=20IOKit=20registry-based=20drive=20enu?= =?UTF-8?q?meration,=20BSD=20name=20=E2=86=92=20IOBDServices=20matching,?= =?UTF-8?q?=20reverse=20patch=20default?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 2 +- src/disc/mod.rs | 2 +- src/drive/macos.rs | 21 ++-- src/scsi/macos.rs | 65 +++++++----- src/scsi/macos_shim.c | 238 +++++++++++++++++++++++++++++++++++++++++- 5 files changed, 286 insertions(+), 42 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0aa48f2..f511ba0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libfreemkv" -version = "0.14.0" +version = "0.16.0" edition = "2024" rust-version = "1.86" license = "AGPL-3.0-only" diff --git a/src/disc/mod.rs b/src/disc/mod.rs index 429fe16..cafd0c3 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -1305,7 +1305,7 @@ impl Disc { decrypt: opts.decrypt, block_sectors: Some(1), full_recovery: true, - reverse: false, + reverse: true, wedged_threshold: 50, progress: opts.progress, halt: opts.halt.clone(), diff --git a/src/drive/macos.rs b/src/drive/macos.rs index d9a6223..409a699 100644 --- a/src/drive/macos.rs +++ b/src/drive/macos.rs @@ -1,26 +1,25 @@ //! macOS drive discovery and device resolution. +//! +//! `find_drives` uses IOKit registry enumeration (via `scsi::list_drives`) +//! to discover optical drives without exclusive access or unmounts. Only +//! the returned paths are then opened for INQUIRY to build full `DriveId`. use crate::error::{Error, Result}; use crate::identity::DriveId; pub fn find_drives() -> Vec<(String, DriveId)> { let mut drives = Vec::new(); - for i in 0..16 { - let path = format!("/dev/disk{}", i); - if !std::path::Path::new(&path).exists() { - continue; - } - match crate::scsi::open(std::path::Path::new(&path)) { + let discovered = crate::scsi::list_drives(); + for info in discovered { + let path = std::path::Path::new(&info.path); + match crate::scsi::open(path) { Ok(mut transport) => { if let Ok(id) = DriveId::from_drive(transport.as_mut()) { - if !id.raw_inquiry.is_empty() && (id.raw_inquiry[0] & 0x1F) == 0x05 { - drives.push((path, id)); - } + drives.push((info.path.clone(), id)); } } Err(_) => { - // Device exists but can't be opened (likely mounted). - // Use `diskutil unmountDisk /dev/diskN` to unmount before accessing. + continue; } } } diff --git a/src/scsi/macos.rs b/src/scsi/macos.rs index 5de8760..5dff080 100644 --- a/src/scsi/macos.rs +++ b/src/scsi/macos.rs @@ -4,11 +4,15 @@ //! through `SCSITaskDeviceInterface::ExecuteTaskSync` — 1:1 with the Linux //! SG_IO backend. The C shim (`macos_shim.c`) handles: //! -//! 1. `diskutil unmountDisk force` so the kernel block-storage driver releases -//! 2. Find `IOBDServices` directly via `IOServiceMatching` (not IOMedia walk) +//! 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}; @@ -16,6 +20,15 @@ use std::path::Path; const K_SENSE_DATA_SIZE: usize = 32; +#[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(); @@ -30,6 +43,7 @@ unsafe extern "C" { task_status_out: *mut u8, transfer_count: *mut u64, ) -> i32; + fn shim_list_drives(out: *mut ShimDriveInfo, max_entries: i32) -> i32; } pub struct MacScsiTransport { @@ -131,42 +145,39 @@ impl ScsiTransport for MacScsiTransport { } } -// ── Drive enumeration ──────────────────────────────────────────────────── +// ── Drive enumeration (registry-based, no exclusive access) ────────────── pub(super) fn list_drives() -> Vec { + 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 i in 0..K_DEV_DISK_MAX { - let path = format!("/dev/disk{i}"); - if !std::path::Path::new(&path).exists() { - continue; - } - let mut transport = match MacScsiTransport::open(std::path::Path::new(&path)) { - Ok(t) => t, - Err(_) => continue, - }; - let inquiry = match super::inquiry(&mut transport) { - Ok(r) => r, - Err(_) => continue, - }; - if inquiry.raw.is_empty() - || (inquiry.raw[K_INQUIRY_TYPE_BYTE] & K_INQUIRY_TYPE_MASK) != K_SCSI_TYPE_OPTICAL - { + for i in 0..(count as usize).min(buf.len()) { + let info = &buf[i]; + let bsd_name = cstr_to_str(&info.bsd_name); + if bsd_name.is_empty() { continue; } out.push(super::DriveInfo { - path, - vendor: inquiry.vendor_id, - model: inquiry.model, - firmware: inquiry.firmware, + 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 } -const K_DEV_DISK_MAX: u8 = 16; -const K_INQUIRY_TYPE_BYTE: usize = 0; -const K_INQUIRY_TYPE_MASK: u8 = 0x1F; -const K_SCSI_TYPE_OPTICAL: u8 = 0x05; +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 { let mut transport = MacScsiTransport::open(path)?; diff --git a/src/scsi/macos_shim.c b/src/scsi/macos_shim.c index 485e663..941c338 100644 --- a/src/scsi/macos_shim.c +++ b/src/scsi/macos_shim.c @@ -1,10 +1,13 @@ #include #include #include +#include #include #include #include +// ── Types ────────────────────────────────────────────────────────────────── + typedef struct { IOCFPlugInInterface **plugin; MMCDeviceInterface **mmc; @@ -12,8 +15,196 @@ typedef struct { int exclusive; } ShimHandle; +typedef struct { + char bsd_name[32]; + char vendor[32]; + char model[48]; + char firmware[16]; +} ShimDriveInfo; + +// ── Global handle (single-drive, same as before) ────────────────────────── + static ShimHandle g_handle = {NULL, NULL, NULL, 0}; +// ── Registry helpers ────────────────────────────────────────────────────── + +static int cfstring_to_cstr(CFStringRef cf, char *buf, size_t buflen) { + if (!cf) return 0; + if (!CFStringGetCString(cf, buf, buflen, kCFStringEncodingUTF8)) return 0; + return 1; +} + +static int registry_entry_bsd_name(io_registry_entry_t entry, char *buf, size_t buflen) { + CFStringRef cf = IORegistryEntryCreateCFProperty(entry, CFSTR("BSD Name"), + kCFAllocatorDefault, 0); + if (!cf) return 0; + int ok = cfstring_to_cstr(cf, buf, buflen); + CFRelease(cf); + return ok; +} + +static io_registry_entry_t find_iomedia_child(io_registry_entry_t parent) { + io_iterator_t iter; + kern_return_t kr = IORegistryEntryGetChildIterator(parent, kIOServicePlane, &iter); + if (kr != KERN_SUCCESS) return 0; + + io_registry_entry_t child; + while ((child = IOIteratorNext(iter)) != 0) { + char cls[128]; + kr = IOObjectGetClass(child, cls); + if (kr == KERN_SUCCESS) { + if (strcmp(cls, "IOMedia") == 0) { + IOObjectRelease(iter); + return child; + } + } + IOObjectRelease(child); + } + IOObjectRelease(iter); + return 0; +} + +static io_registry_entry_t find_child_of_class(io_registry_entry_t parent, const char *target_class) { + io_iterator_t iter; + kern_return_t kr = IORegistryEntryGetChildIterator(parent, kIOServicePlane, &iter); + if (kr != KERN_SUCCESS) return 0; + + io_registry_entry_t child; + while ((child = IOIteratorNext(iter)) != 0) { + char cls[128]; + kr = IOObjectGetClass(child, cls); + if (kr == KERN_SUCCESS && strcmp(cls, target_class) == 0) { + IOObjectRelease(iter); + return child; + } + IOObjectRelease(child); + } + IOObjectRelease(iter); + return 0; +} + +static io_registry_entry_t find_parent_of_class(io_registry_entry_t entry, const char *target_class) { + io_registry_entry_t parent; + kern_return_t kr = IORegistryEntryGetParentEntry(entry, kIOServicePlane, &parent); + if (kr != KERN_SUCCESS) return 0; + + char cls[128]; + kr = IOObjectGetClass(parent, cls); + if (kr == KERN_SUCCESS && strcmp(cls, target_class) == 0) { + return parent; + } + IOObjectRelease(parent); + return 0; +} + +// Given an IOBDServices, find the BSD name of its IOMedia child. +// Chain: IOBDServices -> IOBDBlockStorageDriver -> IOMedia (has "BSD Name") +static int bdsvc_to_bsd_name(io_registry_entry_t bdsvc, char *buf, size_t buflen) { + io_registry_entry_t driver = find_child_of_class(bdsvc, "IOBDBlockStorageDriver"); + if (!driver) return 0; + + io_registry_entry_t media = find_iomedia_child(driver); + IOObjectRelease(driver); + if (!media) return 0; + + int ok = registry_entry_bsd_name(media, buf, buflen); + IOObjectRelease(media); + return ok; +} + +// Given an IOBDServices, extract Device Characteristics strings. +static void bdsvc_device_info(io_registry_entry_t bdsvc, ShimDriveInfo *info) { + CFDictionaryRef dc = IORegistryEntryCreateCFProperty(bdsvc, + CFSTR("Device Characteristics"), kCFAllocatorDefault, 0); + if (!dc) return; + + CFStringRef val; + + val = CFDictionaryGetValue(dc, CFSTR("Vendor Name")); + if (val) cfstring_to_cstr(val, info->vendor, sizeof(info->vendor)); + + val = CFDictionaryGetValue(dc, CFSTR("Product Name")); + if (val) cfstring_to_cstr(val, info->model, sizeof(info->model)); + + val = CFDictionaryGetValue(dc, CFSTR("Product Revision Level")); + if (val) cfstring_to_cstr(val, info->firmware, sizeof(info->firmware)); + + CFRelease(dc); +} + +// Find the IOBDServices that owns the given BSD name. +// Returns a retained io_service_t (caller must release), or 0. +static io_service_t find_bdsvc_by_bsd_name(mach_port_t mp, const char *bsd_name) { + CFMutableDictionaryRef matching = IOServiceMatching("IOBDServices"); + if (!matching) return 0; + + io_iterator_t iter; + kern_return_t kr = IOServiceGetMatchingServices(mp, matching, &iter); + if (kr != KERN_SUCCESS) return 0; + + io_service_t result = 0; + io_service_t svc; + while ((svc = IOIteratorNext(iter)) != 0) { + char name[64]; + if (bdsvc_to_bsd_name(svc, name, sizeof(name))) { + if (strcmp(name, bsd_name) == 0) { + result = svc; + break; + } + } + IOObjectRelease(svc); + } + + if (!result) { + IOIteratorReset(iter); + while ((svc = IOIteratorNext(iter)) != 0) { + IOObjectRelease(svc); + } + } + + IOObjectRelease(iter); + return result; +} + +// Find the IOBDServices that owns the given BSD name by walking from +// IOMedia upward. Used as fallback when bdsvc_to_bsd_name fails +// (e.g. disc under exclusive access, no IOMedia child). +// Chain: IOMedia -> IOBDBlockStorageDriver -> IOBDServices +static io_service_t find_bdsvc_from_iomedia(mach_port_t mp, const char *bsd_name) { + CFMutableDictionaryRef matching = IOServiceMatching("IOMedia"); + if (!matching) return 0; + + io_iterator_t iter; + kern_return_t kr = IOServiceGetMatchingServices(mp, matching, &iter); + if (kr != KERN_SUCCESS) return 0; + + io_service_t result = 0; + io_service_t media; + while ((media = IOIteratorNext(iter)) != 0) { + char name[64]; + if (registry_entry_bsd_name(media, name, sizeof(name)) + && strcmp(name, bsd_name) == 0) + { + io_registry_entry_t driver = find_parent_of_class(media, "IOBDBlockStorageDriver"); + if (driver) { + io_registry_entry_t bdsvc = find_parent_of_class(driver, "IOBDServices"); + IOObjectRelease(driver); + if (bdsvc) { + result = bdsvc; + IOObjectRelease(media); + break; + } + } + } + IOObjectRelease(media); + } + + IOObjectRelease(iter); + return result; +} + +// ── Public API ──────────────────────────────────────────────────────────── + int shim_open_exclusive(const char *bsd_name) { kern_return_t kr; HRESULT hr; @@ -31,8 +222,14 @@ int shim_open_exclusive(const char *bsd_name) { mach_port_t mp; IOMainPort(0, &mp); - CFMutableDictionaryRef matching = IOServiceMatching("IOBDServices"); - io_service_t svc = IOServiceGetMatchingService(mp, matching); + io_service_t svc = find_bdsvc_by_bsd_name(mp, bsd_name); + if (!svc) { + svc = find_bdsvc_from_iomedia(mp, bsd_name); + } + if (!svc) { + CFMutableDictionaryRef matching = IOServiceMatching("IOBDServices"); + svc = IOServiceGetMatchingService(mp, matching); + } if (!svc) return -1; kr = IOCreatePlugInInterfaceForService(svc, @@ -140,3 +337,40 @@ int shim_execute(const unsigned char *cdb, unsigned char cdb_len, return (int)kr; } + +// ── Registry-based drive enumeration ────────────────────────────────────── +// +// Walks IOBDServices entries in the IOKit registry. No exclusive access, +// no SCSI commands, no unmounts. Returns up to max_entries drives. + +int shim_list_drives(ShimDriveInfo *out, int max_entries) { + mach_port_t mp; + IOReturn ret = IOMainPort(0, &mp); + if (ret != kIOReturnSuccess) return 0; + + CFMutableDictionaryRef matching = IOServiceMatching("IOBDServices"); + if (!matching) return 0; + + io_iterator_t iter; + kern_return_t kr = IOServiceGetMatchingServices(mp, matching, &iter); + if (kr != KERN_SUCCESS) return 0; + + int count = 0; + io_service_t svc; + while ((svc = IOIteratorNext(iter)) != 0 && count < max_entries) { + ShimDriveInfo *info = &out[count]; + memset(info, 0, sizeof(*info)); + + bdsvc_device_info(svc, info); + bdsvc_to_bsd_name(svc, info->bsd_name, sizeof(info->bsd_name)); + + if (info->bsd_name[0]) { + count++; + } + + IOObjectRelease(svc); + } + + IOObjectRelease(iter); + return count; +}