Four round-6 findings. FileSectorSource::read_sectors guarded its output buffer with a debug_assert, which is compiled out in release — so an undersized buffer panicked with 'range end index out of range' instead of returning an error, out of a public SectorSource impl where the length is caller input. Drive::read_fua already carries this exact guard, with a comment recording the same panic being fixed there, and PrefetchedSectorSource has a regression test for the same case; this impl had been given neither. The new test is red in release for precisely the predicted reason: 'range end index 8192 out of range for slice of length 2049'. parse_track's sample-rate ladder ended in an unconditional S48, so any SamplingFrequency below 44100 was recorded as 48 kHz. A 32000 Hz AC-3 or DTS track is legal and common in broadcast-sourced content, and the wrong rate then propagated into the reconstructed AudioStream. Anything below the lowest mapped rate is now Unknown, which is what the crate's canonical SampleRate::from_hz already returned — the ladder disagreed with it. The ladder itself stays, because the MKV element is a float and wants tolerance rather than exact equality. shim_open_exclusive used the mach port from IOMainPort without checking the return; on failure the port is left untouched and every IOKit call below ran against an uninitialised value. shim_list_drives in the same file does check it. build.rs treated cc and ar as successful if the process merely SPAWNED, so a genuine compile error in the macOS C shim produced no object file and surfaced later as an unexplained link failure against a missing symbol. The shim is macOS-only and is neither linted nor compiled on the other two platforms, so a mistake in it has exactly one chance to be noticed. The last two have no test: one needs IOMainPort to fail, the other needs a deliberately broken C shim, and neither is reachable from the test harness. Both mirror a correct sibling in the same file, which is the evidence available.
408 lines
14 KiB
C
408 lines
14 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>
|
|
|
|
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 ──────────────────────────────────────────────────────
|
|
|
|
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 || 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) {
|
|
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;
|
|
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) {
|
|
int status;
|
|
waitpid(pid, &status, 0);
|
|
}
|
|
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) {
|
|
CFMutableDictionaryRef matching = IOServiceMatching("IOBDServices");
|
|
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 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;
|
|
}
|