/* -*- Mode: C; c-basic-offset:4 ; indent-tabs-mode:nil -*- */ /* * This file is part of libaacs * Copyright (C) 2009-2010 Obliter0n * Copyright (C) 2010-2015 npzacs * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see * . */ #if HAVE_CONFIG_H #include "config.h" #endif #include "mmc_device.h" #include "util/logging.h" #include "util/macro.h" #include "util/strutl.h" #include #include #include #include #include #include /* need to undefine VERSION as one of the members of struct SCSICmd_INQUIRY_StandardData is named VERSION (see IOKit/scsi/SCSICmds_INQUIRY_Definitions.h) */ #undef VERSION #include #include #ifdef HAVE_SYS_TYPES_H #include #endif #ifdef HAVE_SYS_PARAM_H #include #endif #ifdef HAVE_SYS_MOUNT_H #include #endif #ifdef HAVE_LIBGEN_H #include #endif #ifdef HAVE_LIMITS_H #include #endif /* * */ enum disk_state_e { disk_mounted, disk_unmounted, disk_appeared, disk_mounting }; struct mmcdev { /* Interfaces required for low-level device communication */ IOCFPlugInInterface **plugInInterface; MMCDeviceInterface **mmcInterface; SCSITaskDeviceInterface **taskInterface; /* device short name (ie disk1) */ char bsd_name[MNAMELEN]; /* for mounting/unmounting the disc */ DADiskRef disk; DASessionRef session; enum disk_state_e disk_state; dispatch_semaphore_t sync_sem; dispatch_queue_t background_queue; }; int device_send_cmd(MMCDEV *mmc, const uint8_t *cmd, uint8_t *buf, size_t tx, size_t rx) { SCSITaskInterface **task = NULL; SCSI_Sense_Data sense; SCSITaskStatus status; SCSITaskSGElement iov; UInt8 direction; UInt64 sent; int rc; if (NULL == mmc->taskInterface) { return 0; } do { task = (*mmc->taskInterface)->CreateSCSITask(mmc->taskInterface); if (NULL == task) { BD_DEBUG(DBG_MMC, "Could not create SCSI Task\n"); break; } iov.address = (uintptr_t) buf; iov.length = tx ? tx : rx; if (buf) { direction = tx ? kSCSIDataTransfer_FromInitiatorToTarget : kSCSIDataTransfer_FromTargetToInitiator; } else { direction = kSCSIDataTransfer_NoDataTransfer; } SCSICommandDescriptorBlock cdb = {0}; memcpy(cdb, cmd, sizeof(cdb)); rc = (*task)->SetCommandDescriptorBlock(task, cdb, kSCSICDBSize_16Byte); if (kIOReturnSuccess != rc) { BD_DEBUG(DBG_MMC, "Error setting SCSI command\n"); break; } rc = (*task)->SetScatterGatherEntries(task, &iov, 1, iov.length, direction); if (kIOReturnSuccess != rc) { BD_DEBUG(DBG_MMC, "Error setting SCSI scatter gather entries\n"); break; } rc = (*task)->SetTimeoutDuration(task, 5000000); if (kIOReturnSuccess != rc) { BD_DEBUG(DBG_MMC, "Error setting SCSI command timeout\n"); break; } memset(&sense, 0, sizeof (sense)); rc = (*task)->ExecuteTaskSync(task, &sense, &status, &sent); char str[512]; BD_DEBUG(DBG_MMC, "Send SCSI MMC cmd %s:\n", str_print_hex(str, cmd, 16)); if (tx) { BD_DEBUG(DBG_MMC, " Buffer: %s ->\n", str_print_hex(str, buf, tx>255?255:tx)); } else { BD_DEBUG(DBG_MMC, " Buffer: %s <-\n", str_print_hex(str, buf, rx>255?255:rx)); } if (kIOReturnSuccess != rc || status != 0) { BD_DEBUG(DBG_MMC, " Send failed!\n"); break; } else { BD_DEBUG(DBG_MMC, " Send succeeded! sent = %lld status = %u. response = %x\n", (unsigned long long) sent, status, sense.VALID_RESPONSE_CODE); } (*task)->Release(task); return 1; } while (0); if (task) { (*task)->Release(task); } return 0; } static int get_mounted_device_from_path(MMCDEV *mmc, const char *path) { struct statfs stat_info; int rc; rc = statfs(path, &stat_info); if (0 != rc) { return rc; } strncpy(mmc->bsd_name, basename (stat_info.f_mntfromname), sizeof (mmc->bsd_name)); return 0; } static void iokit_unmount_complete(DADiskRef disk, DADissenterRef dissenter, void *context) { (void)disk; /* suppress warning */ MMCDEV *mmc = context; if (dissenter) { BD_DEBUG(DBG_MMC, "Could not unmount the disc\n"); } else { BD_DEBUG(DBG_MMC, "Disc unmounted\n"); mmc->disk_state = disk_unmounted; } dispatch_semaphore_signal(mmc->sync_sem); } static void iokit_mount_complete(DADiskRef disk, DADissenterRef dissenter, void *context) { (void) disk; /* suppress warning */ MMCDEV *mmc = context; if (dissenter) { DAReturn code = DADissenterGetStatus(dissenter); BD_DEBUG(DBG_MMC, "Could not mount the disc (%8X)\n", code); mmc->disk_state = disk_unmounted; } else { BD_DEBUG(DBG_MMC, "Disc mounted\n"); mmc->disk_state = disk_mounted; } dispatch_semaphore_signal(mmc->sync_sem); } /* Unmount the disk at mmc->disk * Note: This MAY NOT be called on the background queue, * as that would lead to a deadlock. */ static int iokit_unmount(MMCDEV *mmc) { if (disk_unmounted == mmc->disk_state) { return 0; /* nothing to do */ } BD_DEBUG(DBG_MMC, "Unmounting disk\n"); DADiskUnmount(mmc->disk, kDADiskUnmountOptionForce, iokit_unmount_complete, mmc); dispatch_semaphore_wait(mmc->sync_sem, DISPATCH_TIME_FOREVER); return (mmc->disk_state == disk_unmounted) ? 0 : -1; } /* Mount the disk at mmc->disk * Note: This MAY NOT be called on the background queue, * as that would lead to a deadlock. */ static int iokit_mount(MMCDEV *mmc) { if (disk_mounted != mmc->disk_state) { if (mmc->disk && mmc->session) { mmc->disk_state = disk_mounting; DADiskMount(mmc->disk, NULL, kDADiskMountOptionDefault, iokit_mount_complete, mmc); dispatch_semaphore_wait(mmc->sync_sem, DISPATCH_TIME_FOREVER); } } return (mmc->disk_state == disk_unmounted) ? 0 : -1; } static int iokit_find_service_matching(MMCDEV *mmc, io_service_t *servp) { CFMutableDictionaryRef matchingDict = IOServiceMatching("IOBDServices"); io_iterator_t deviceIterator; io_service_t service; int rc; assert(NULL != servp); *servp = IO_OBJECT_NULL; if (!matchingDict) { BD_DEBUG(DBG_MMC, "Could not create a matching dictionary for IOBDServices\n"); return -1; } /* this call consumes the reference to the matchingDict. we do not need to release it */ rc = IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDict, &deviceIterator); if (kIOReturnSuccess != rc) { BD_DEBUG(DBG_MMC, "Could not create device iterator\n"); return -1; } while (0 != (service = IOIteratorNext(deviceIterator))) { CFStringRef data; char name[MNAMELEN] = ""; data = IORegistryEntrySearchCFProperty(service, kIOServicePlane, CFSTR(kIOBSDNameKey), kCFAllocatorDefault, kIORegistryIterateRecursively); if (NULL != data) { rc = CFStringGetCString(data, name, sizeof (name), kCFStringEncodingASCII); CFRelease(data); if (0 == strcmp(name, mmc->bsd_name)) { break; } } (void) IOObjectRelease(service); } IOObjectRelease(deviceIterator); *servp = service; return (service != IO_OBJECT_NULL) ? 0 : -1; } static int iokit_find_interfaces(MMCDEV *mmc, io_service_t service) { SInt32 score; int rc; rc = IOCreatePlugInInterfaceForService(service, kIOMMCDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &mmc->plugInInterface, &score); if (kIOReturnSuccess != rc || NULL == mmc->plugInInterface) { return -1; } BD_DEBUG(DBG_MMC, "Getting MMC interface\n"); IOCFPlugInInterface **plugInInterface = mmc->plugInInterface; rc = (*plugInInterface)->QueryInterface(plugInInterface, CFUUIDGetUUIDBytes(kIOMMCDeviceInterfaceID), (LPVOID)&mmc->mmcInterface); if (kIOReturnSuccess != rc || NULL == mmc->mmcInterface) { BD_DEBUG(DBG_MMC, "Could not get multimedia commands (MMC) interface\n"); return -1; } BD_DEBUG(DBG_MMC, "Have an MMC interface (%p). Getting a SCSI task interface...\n", (void*)mmc->mmcInterface); mmc->taskInterface = (*mmc->mmcInterface)->GetSCSITaskDeviceInterface (mmc->mmcInterface); if (NULL == mmc->taskInterface) { BD_DEBUG(DBG_MMC, "Could not get SCSI task device interface\n"); return -1; } return 0; } static DADissenterRef iokit_mount_approval_cb(DADiskRef disk, void *context) { MMCDEV *mmc = context; /* If the disk state is mounted, there is nothing to do here. */ if (disk_mounted == mmc->disk_state) { return NULL; } /* Check if the disk that is to be mounted matches ours, * if not, we do not need to reject mounting. */ if (!CFEqual(disk, mmc->disk)) { return NULL; } BD_DEBUG(DBG_MMC, "Mount approval request for matching disc\n"); /* When we are trying to mount, the mount approval callback is * called too, so we need to allow mounting for ourselves here. */ if (disk_mounting == mmc->disk_state) { BD_DEBUG(DBG_MMC, "Allowing ourselves to mount\n"); return NULL; } mmc->disk_state = disk_appeared; dispatch_semaphore_signal(mmc->sync_sem); CFStringRef reason = CFSTR("Disk is going to be mounted libaacs"); return DADissenterCreate(kCFAllocatorDefault, kDAReturnBusy, reason); } static int iokit_da_init(MMCDEV *mmc) { mmc->session = DASessionCreate(kCFAllocatorDefault); if (NULL == mmc->session) { BD_DEBUG(DBG_MMC | DBG_CRIT, "Could not create a disc arbitration session\n"); return -1; } mmc->disk = DADiskCreateFromBSDName(kCFAllocatorDefault, mmc->session, mmc->bsd_name); if (NULL == mmc->disk) { BD_DEBUG(DBG_MMC | DBG_CRIT, "Could not create a disc arbitration disc for the device\n"); CFRelease(mmc->session); mmc->session = NULL; return -1; } mmc->background_queue = dispatch_queue_create("org.videolan.libaacs", DISPATCH_QUEUE_SERIAL); DASessionSetDispatchQueue(mmc->session, mmc->background_queue); // Register event callbacks DARegisterDiskMountApprovalCallback(mmc->session, NULL, iokit_mount_approval_cb, mmc); mmc->sync_sem = dispatch_semaphore_create(0); return 0; } static void iokit_da_destroy(MMCDEV *mmc) { if (mmc->session) { /* The approval callback must be unregistered here, doing it in the * mount approval callback instead after we got a matching disk would * cause the OS to immediately re-try to mount the disk faster than we * can mount it. */ DAUnregisterApprovalCallback(mmc->session, iokit_mount_approval_cb, mmc); DASessionSetDispatchQueue(mmc->session, NULL); CFRelease(mmc->session); mmc->session = NULL; } if (mmc->disk) { CFRelease(mmc->disk); mmc->disk = NULL; } dispatch_release(mmc->sync_sem); } static int mmc_open_iokit(const char *path, MMCDEV *mmc) { io_service_t service; int rc; mmc->plugInInterface = NULL; mmc->mmcInterface = NULL; mmc->taskInterface = NULL; mmc->disk = NULL; mmc->session = NULL; mmc->disk_state = disk_mounted; /* get the bsd name associated with this mount */ rc = get_mounted_device_from_path(mmc, path); if (0 != rc) { BD_DEBUG(DBG_MMC | DBG_CRIT, "Could not locate mounted device associated with %s\n", path); return rc; } /* find a matching io service (IOBDServices) */ rc = iokit_find_service_matching(mmc, &service); if (0 != rc) { BD_DEBUG(DBG_MMC | DBG_CRIT, "Could not find matching IOBDServices mounted @ %s\n", path); return rc; } /* find mmc and scsi task interfaces */ rc = iokit_find_interfaces(mmc, service); /* done with the ioservice. release it */ (void) IOObjectRelease(service); if (0 != rc) { return rc; } /* Init DiskArbitration */ rc = iokit_da_init(mmc); if (0 != rc) { return rc; } /* unmount the disk so exclusive access can be obtained (this is required to use the scsi task interface) */ rc = iokit_unmount(mmc); if (0 != rc) { BD_DEBUG(DBG_MMC | DBG_CRIT, "Failed to unmount the disc at %s\n", path); return rc; } /* finally, obtain exclusive access */ rc = (*mmc->taskInterface)->ObtainExclusiveAccess(mmc->taskInterface); if (kIOReturnSuccess != rc) { BD_DEBUG(DBG_MMC | DBG_CRIT, "Failed to obtain exclusive access. rc = %x\n", rc); return -1; } BD_DEBUG(DBG_MMC, "MMC Open complete\n"); return 0; } MMCDEV *device_open(const char *path) { MMCDEV *mmc; int rc; mmc = calloc(1, sizeof(MMCDEV)); if (!mmc) { BD_DEBUG(DBG_MKB | DBG_CRIT, "out of memory\n"); return NULL; } rc = mmc_open_iokit(path, mmc); if (0 != rc) { device_close(&mmc); return NULL; } return mmc; } void device_close(MMCDEV **pp) { __block int rc = 0; if (pp && *pp) { MMCDEV *mmc = *pp; /* When the exclusive access to the drive is released, * the OS will see the device like a "new" device and * try to mount it. Therefore we can't just mount the * disk we previously got immediately here as it would * fail with kDAReturnBadArgument as the disk is not * available yet. * Trying to mount the disk after it appears in peek * does not work either as the disk is not yet ready * or in the process of being mounted by the OS so * that would return an kDAReturnBusy error. * The only way that seems to reliably work is to use * a mount approval callback. When the OS tries to * mount the disk, the mount approval callback is * called and we can reject mounting and then proceed * to mount the disk ourselves. * Claiming exclusive access using DADiskClaim in order * to prevent the OS form mounting the disk does not work * either! */ if (mmc->taskInterface) { (*mmc->taskInterface)->ReleaseExclusiveAccess(mmc->taskInterface); (*mmc->taskInterface)->Release(mmc->taskInterface); mmc->taskInterface = NULL; } if (mmc->mmcInterface) { (*mmc->mmcInterface)->Release(mmc->mmcInterface); mmc->mmcInterface = NULL; } if (mmc->plugInInterface) { IODestroyPlugInInterface(mmc->plugInInterface); } if (!mmc->sync_sem) { /* open failed before iokit_da_init() */ X_FREE(*pp); return; } /* Wait for disc to re-appear for 20 seconds. * This timeout was figured out by experimentation with * a USB BD drive which sometimes can take really long to * be in a mountable state again. * For internal drives this is probably much faster * so the long timeout shouldnt do much harm for thse * cases. */ dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 20 * 1E+9); dispatch_semaphore_wait(mmc->sync_sem, timeout); /* It is crucial that this is done on the event handling queue * else callbacks could be received while this code runs. */ dispatch_sync(mmc->background_queue, ^{ if (disk_appeared != mmc->disk_state) { BD_DEBUG(DBG_MMC | DBG_CRIT, "Timeout waiting for the disc to appear again!\n"); iokit_da_destroy(mmc); rc = -1; return; } rc = 0; }); if (rc == 0) { /* Disk appeared successfully, mount it. * Return value is ignored as logging of success or * error takes place in the callback already and there * is nothing we can do really if mounting fails. */ (void) iokit_mount(mmc); iokit_da_destroy(mmc); } X_FREE(*pp); } }