The worst of the five was a regression suite that never touched the code it guarded: nine batch-count tests called `safe_batch_count` and `buggy_batch_count`, both defined in the test file itself. The u16 truncation they exist to prevent could be reintroduced in sector/prefetched.rs with every one of them green. They now drive the real producer through the public API, and reinstating the truncation fails five of the nine. Worth recording that the symptom has changed since the original fix: the unit-alignment clamp below floors a zero batch at three sectors, so the bug is now a twenty-fold throughput cliff rather than the stall it once was. The MP4 reserve test's only numeric case was dominated by the floor and the buffer, so BYTES_PER_SAMPLE could be zeroed without failing it. It now has a case where the per-sample term dominates. The zero-count guard in FileSectorSource was likewise unfalsifiable — seek-past-EOF and a zero-length read both succeed — so the test now observes the file cursor. The AACS media-key ambiguity guard had no test at all; the pool scan is extracted so the verifier can be injected, because a genuine two-key collision needs one ciphertext decrypting under two AES-128 keys to plaintexts sharing a 64-bit magic, which is a 2^64 search and not a fixture. macOS implemented the documented cheap, side-effect-free presence probe by building a full exclusive transport — which force-unmounts the disc. Linux and Windows issue one TEST UNIT READY with no unmount; macOS was the outlier. It now walks the IOKit registry for the media object instead. The C shim's registry reads assumed CoreFoundation types the registry does not guarantee, so a driver publishing a CFNumber where a CFString was expected aborted the process from inside public API. Types are checked and a wrong type treated as absent. The unbounded waitpid on the unmount child is now a polled deadline, and the last-resort match gained the NULL check its two siblings already had. The empty-CDB guard existed only on Linux while a shared helper's comment claimed all three backends had it. Moved into the helper, so the comment is now true and macOS and Windows are covered. One finding was REJECTED with evidence rather than fixed. The TrueHD buffer-cap test was indeed bogus, but MAX_TRUEHD_BUF turns out to be unreachable by any input: the parser only retains data when the buffer is shorter than the declared AU, and that declaration is twelve bits, so the worst case is 8189 bytes against a 256 KiB cap. An exhaustive sweep over all 65536 AU headers confirmed it. The fixture now sits at the reachable ceiling and asserts that instead. The cap itself is left in place as defence, unreachable by construction, matching how the AC-3 resync guard was handled earlier in this audit. Two behaviour changes worth naming: Linux's empty-CDB error becomes InvalidCdbLength rather than a transport failure, and an unknown device now reports absent media rather than a not-found error, because the registry cannot tell an empty drive from a missing one. The latter is a conflation of the kind this audit has fixed three times; it is recorded for the next round rather than left silent.
515 lines
19 KiB
C
515 lines
19 KiB
C
#include <IOKit/IOKitLib.h>
|
|
#include <IOKit/IOCFPlugIn.h>
|
|
#include <IOKit/scsi/SCSITaskLib.h>
|
|
#include <CoreFoundation/CoreFoundation.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <spawn.h>
|
|
#include <sys/wait.h>
|
|
#include <fcntl.h>
|
|
#include <signal.h>
|
|
|
|
extern char **environ;
|
|
|
|
// ── Types ──────────────────────────────────────────────────────────────────
|
|
|
|
typedef struct {
|
|
IOCFPlugInInterface **plugin;
|
|
MMCDeviceInterface **mmc;
|
|
SCSITaskDeviceInterface **scsi;
|
|
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 ──────────────────────────────────────────────────────
|
|
|
|
// Convert a registry property to a C string.
|
|
//
|
|
// The value is taken as CFTypeRef, not CFStringRef, and its type is checked
|
|
// before use. IORegistryEntryCreateCFProperty / CFDictionaryGetValue return
|
|
// whatever the driver published: the IOKit registry contract (Apple, "Accessing
|
|
// Hardware From Applications" — Device Access and the I/O Kit) fixes the
|
|
// property KEYS, not the CoreFoundation type behind them, and a third-party
|
|
// optical driver publishing a CFNumber or CFData for "BSD Name" or "Product
|
|
// Revision Level" is legal. CFStringGetCString on a non-CFString aborts the
|
|
// process (CFRuntime type assertion) — from inside the public
|
|
// scsi::list_drives(), which is documented never to fail. Wrong type → treated
|
|
// as absent.
|
|
static int cfstring_to_cstr(CFTypeRef cf, char *buf, size_t buflen) {
|
|
if (!cf) return 0;
|
|
if (CFGetTypeID(cf) != CFStringGetTypeID()) return 0;
|
|
if (!CFStringGetCString((CFStringRef)cf, buf, buflen, kCFStringEncodingUTF8)) return 0;
|
|
return 1;
|
|
}
|
|
|
|
static int registry_entry_bsd_name(io_registry_entry_t entry, char *buf, size_t buflen) {
|
|
CFTypeRef 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 || strcmp(cls, "IOBDMedia") == 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) {
|
|
// "Device Characteristics" is declared a dictionary, but the value is
|
|
// driver-published and the registry contract does not enforce the type.
|
|
// CFDictionaryGetValue on a non-dictionary aborts the process, so the type
|
|
// is checked before it is used as one. Each member string is type-checked
|
|
// in turn by cfstring_to_cstr.
|
|
CFTypeRef dc = IORegistryEntryCreateCFProperty(bdsvc,
|
|
CFSTR("Device Characteristics"), kCFAllocatorDefault, 0);
|
|
if (!dc) return;
|
|
if (CFGetTypeID(dc) != CFDictionaryGetTypeID()) {
|
|
CFRelease(dc);
|
|
return;
|
|
}
|
|
CFDictionaryRef dict = (CFDictionaryRef)dc;
|
|
|
|
CFTypeRef val;
|
|
|
|
val = CFDictionaryGetValue(dict, CFSTR("Vendor Name"));
|
|
if (val) cfstring_to_cstr(val, info->vendor, sizeof(info->vendor));
|
|
|
|
val = CFDictionaryGetValue(dict, CFSTR("Product Name"));
|
|
if (val) cfstring_to_cstr(val, info->model, sizeof(info->model));
|
|
|
|
val = CFDictionaryGetValue(dict, 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;
|
|
SInt32 score = 0;
|
|
|
|
if (g_handle.exclusive && g_handle.scsi) {
|
|
return 0;
|
|
}
|
|
|
|
// 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) {
|
|
// BOUNDED wait. A plain blocking waitpid() here hung the public
|
|
// scsi::open() forever whenever the unmount wedged — diskutil
|
|
// blocks indefinitely on a volume whose filesystem is stuck (a
|
|
// hung network mount, a fs process not answering the unmount
|
|
// notification), and there is no signal, timeout or cancellation
|
|
// reaching this frame. Poll with WNOHANG to a deadline, then
|
|
// SIGKILL and reap so no zombie is left behind.
|
|
//
|
|
// Continuing after a killed unmount is deliberate:
|
|
// ObtainExclusiveAccess below is the real gate, and it reports the
|
|
// still-mounted disc through the shim's -5 sentinel (mapped to
|
|
// Error::DeviceLocked) — a typed error the caller can act on,
|
|
// instead of a process that never returns.
|
|
const int poll_us = 50000; // 50 ms
|
|
const int max_polls = 400; // 400 x 50 ms = 20 s
|
|
int status;
|
|
int reaped = 0;
|
|
for (int i = 0; i <= max_polls; i++) {
|
|
pid_t r = waitpid(pid, &status, WNOHANG);
|
|
if (r == pid) { reaped = 1; break; }
|
|
// r < 0 means the child is already gone (ECHILD) — nothing to
|
|
// wait for, and looping would spin to the deadline.
|
|
if (r < 0) { reaped = 1; break; }
|
|
if (i == max_polls) break;
|
|
usleep(poll_us);
|
|
}
|
|
if (!reaped) {
|
|
kill(pid, SIGKILL);
|
|
// SIGKILL is uncatchable, so this reap converges; still poll
|
|
// rather than block, so the shim has no unbounded wait at all.
|
|
for (int i = 0; i < 100; i++) {
|
|
if (waitpid(pid, &status, WNOHANG) != 0) break;
|
|
usleep(10000); // 10 ms x 100 = 1 s
|
|
}
|
|
}
|
|
}
|
|
posix_spawn_file_actions_destroy(&fa);
|
|
}
|
|
usleep(500000);
|
|
|
|
mach_port_t mp;
|
|
// Check the return before using the port. On failure IOMainPort leaves `mp`
|
|
// untouched, so every IOKit call below would run against an uninitialised
|
|
// mach port. shim_list_drives does check it; this path did not.
|
|
if (IOMainPort(0, &mp) != kIOReturnSuccess) return -1;
|
|
|
|
io_service_t svc = find_bdsvc_by_bsd_name(mp, bsd_name);
|
|
if (!svc) {
|
|
svc = find_bdsvc_from_iomedia(mp, bsd_name);
|
|
}
|
|
if (!svc) {
|
|
// IOServiceMatching returns NULL on allocation failure. Both other call
|
|
// sites in this file check it; this one did not, and
|
|
// IOServiceGetMatchingService with a NULL matching dictionary is
|
|
// undefined (it consumes the reference it is given).
|
|
CFMutableDictionaryRef matching = IOServiceMatching("IOBDServices");
|
|
if (matching) {
|
|
svc = IOServiceGetMatchingService(mp, matching);
|
|
}
|
|
}
|
|
if (!svc) return -1;
|
|
|
|
kr = IOCreatePlugInInterfaceForService(svc,
|
|
kIOMMCDeviceUserClientTypeID, kIOCFPlugInInterfaceID,
|
|
&g_handle.plugin, &score);
|
|
IOObjectRelease(svc);
|
|
|
|
if (kr != KERN_SUCCESS || !g_handle.plugin) return -2;
|
|
|
|
hr = (*g_handle.plugin)->QueryInterface(g_handle.plugin,
|
|
CFUUIDGetUUIDBytes(kIOMMCDeviceInterfaceID), (LPVOID *)&g_handle.mmc);
|
|
if (hr != S_OK || !g_handle.mmc) {
|
|
IODestroyPlugInInterface(g_handle.plugin);
|
|
g_handle.plugin = NULL;
|
|
return -3;
|
|
}
|
|
|
|
g_handle.scsi = (*g_handle.mmc)->GetSCSITaskDeviceInterface(g_handle.mmc);
|
|
if (!g_handle.scsi) {
|
|
(*g_handle.mmc)->Release(g_handle.mmc);
|
|
IODestroyPlugInInterface(g_handle.plugin);
|
|
g_handle.mmc = NULL;
|
|
g_handle.plugin = NULL;
|
|
return -4;
|
|
}
|
|
|
|
for (int retry = 0; retry < 10; retry++) {
|
|
kr = (*g_handle.scsi)->ObtainExclusiveAccess(g_handle.scsi);
|
|
if (kr == kIOReturnSuccess) break;
|
|
usleep(500000);
|
|
}
|
|
if (kr != kIOReturnSuccess) {
|
|
(*g_handle.scsi)->Release(g_handle.scsi);
|
|
(*g_handle.mmc)->Release(g_handle.mmc);
|
|
IODestroyPlugInInterface(g_handle.plugin);
|
|
g_handle.scsi = NULL;
|
|
g_handle.mmc = NULL;
|
|
g_handle.plugin = NULL;
|
|
return -5;
|
|
}
|
|
|
|
g_handle.exclusive = 1;
|
|
return 0;
|
|
}
|
|
|
|
void shim_close(void) {
|
|
if (g_handle.exclusive && g_handle.scsi) {
|
|
(*g_handle.scsi)->ReleaseExclusiveAccess(g_handle.scsi);
|
|
}
|
|
if (g_handle.scsi) {
|
|
(*g_handle.scsi)->Release(g_handle.scsi);
|
|
g_handle.scsi = NULL;
|
|
}
|
|
if (g_handle.mmc) {
|
|
(*g_handle.mmc)->Release(g_handle.mmc);
|
|
g_handle.mmc = NULL;
|
|
}
|
|
if (g_handle.plugin) {
|
|
IODestroyPlugInInterface(g_handle.plugin);
|
|
g_handle.plugin = NULL;
|
|
}
|
|
g_handle.exclusive = 0;
|
|
}
|
|
|
|
int shim_execute(const unsigned char *cdb, unsigned char cdb_len,
|
|
void *buf, unsigned int buf_len, int data_in,
|
|
unsigned char *sense_out, unsigned int sense_len,
|
|
unsigned char *task_status_out, unsigned long long *transfer_count) {
|
|
if (!g_handle.scsi) return -1;
|
|
|
|
SCSITaskInterface **task = (*g_handle.scsi)->CreateSCSITask(g_handle.scsi);
|
|
if (!task) return -2;
|
|
|
|
SCSICommandDescriptorBlock cdb_buf;
|
|
memset(&cdb_buf, 0, sizeof(cdb_buf));
|
|
memcpy(&cdb_buf, cdb, cdb_len);
|
|
|
|
(*task)->SetCommandDescriptorBlock(task, cdb_buf, cdb_len);
|
|
|
|
if (buf_len > 0 && buf) {
|
|
SCSITaskSGElement sg;
|
|
sg.address = (UInt64)(uintptr_t)buf;
|
|
sg.length = buf_len;
|
|
(*task)->SetScatterGatherEntries(task, &sg, 1, buf_len,
|
|
data_in ? kSCSIDataTransfer_FromTargetToInitiator
|
|
: kSCSIDataTransfer_FromInitiatorToTarget);
|
|
} else {
|
|
(*task)->SetScatterGatherEntries(task, NULL, 0, 0,
|
|
kSCSIDataTransfer_NoDataTransfer);
|
|
}
|
|
|
|
(*task)->SetTimeoutDuration(task, 30000);
|
|
|
|
SCSI_Sense_Data sense;
|
|
memset(&sense, 0, sizeof(sense));
|
|
SCSITaskStatus status = 0xFF;
|
|
UInt64 count = 0;
|
|
|
|
IOReturn kr = (*task)->ExecuteTaskSync(task, &sense, &status, &count);
|
|
|
|
if (sense_out && sense_len > 0) {
|
|
size_t copy = sense_len < sizeof(sense) ? sense_len : sizeof(sense);
|
|
memcpy(sense_out, &sense, copy);
|
|
}
|
|
if (task_status_out) *task_status_out = (unsigned char)status;
|
|
if (transfer_count) *transfer_count = count;
|
|
|
|
(*task)->Release(task);
|
|
|
|
return (int)kr;
|
|
}
|
|
|
|
// ── Registry-based media-presence probe ───────────────────────────────────
|
|
//
|
|
// "Is a disc inserted?" answered from the IOKit registry alone: no exclusive
|
|
// access, no unmount, no SCSI command, no state change of any kind.
|
|
//
|
|
// Apple's IOStorageFamily publishes an IOMedia object for a removable device
|
|
// only while media is present, and tears it down on eject — that is the
|
|
// documented media lifecycle (Apple, "Mass Storage Device Driver Programming
|
|
// Guide": Media Objects / media arrival and removal). So the presence of an
|
|
// IOMedia whose "BSD Name" is the requested device IS the presence of a disc.
|
|
//
|
|
// Returns 1 (media present), 0 (no media), or -1 (IOKit unavailable).
|
|
int shim_media_present(const char *bsd_name) {
|
|
mach_port_t mp;
|
|
if (IOMainPort(0, &mp) != kIOReturnSuccess) return -1;
|
|
|
|
CFMutableDictionaryRef matching = IOServiceMatching("IOMedia");
|
|
if (!matching) return -1;
|
|
|
|
io_iterator_t iter;
|
|
// Consumes `matching` whether it succeeds or fails.
|
|
if (IOServiceGetMatchingServices(mp, matching, &iter) != KERN_SUCCESS) return -1;
|
|
|
|
int found = 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)
|
|
{
|
|
found = 1;
|
|
}
|
|
IOObjectRelease(media);
|
|
if (found) break;
|
|
}
|
|
|
|
// Drain the rest so no entry is leaked when we broke early.
|
|
while ((media = IOIteratorNext(iter)) != 0) {
|
|
IOObjectRelease(media);
|
|
}
|
|
IOObjectRelease(iter);
|
|
return found;
|
|
}
|
|
|
|
// ── 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;
|
|
}
|