1 /* -*- Mode: C; c-basic-offset:4 ; indent-tabs-mode:nil -*- */
2 /*
3 * This file is part of libaacs
4 * Copyright (C) 2009-2010 Obliter0n
5 * Copyright (C) 2010-2015 npzacs
6 *
7 * This library is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU Lesser General Public
9 * License as published by the Free Software Foundation; either
10 * version 2.1 of the License, or (at your option) any later version.
11 *
12 * This library is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Lesser General Public License for more details.
16 *
17 * You should have received a copy of the GNU Lesser General Public
18 * License along with this library. If not, see
19 * <http://www.gnu.org/licenses/>.
20 */
21
22 #if HAVE_CONFIG_H
23 #include "config.h"
24 #endif
25
26 #include "mmc_device.h"
27
28 #include "util/logging.h"
29 #include "util/macro.h"
30 #include "util/strutl.h"
31
32 #include <stdlib.h>
33 #include <string.h>
34
35 #include <IOKit/IOKitLib.h>
36 #include <IOKit/IOBSD.h>
37 #include <IOKit/IOCFPlugIn.h>
38
39 #include <DiskArbitration/DiskArbitration.h>
40
41 /* need to undefine VERSION as one of the members of struct
42 SCSICmd_INQUIRY_StandardData is named VERSION (see
43 IOKit/scsi/SCSICmds_INQUIRY_Definitions.h) */
44 #undef VERSION
45 #include <IOKit/scsi/SCSITaskLib.h>
46
47 #include <IOKit/storage/IOBDMediaBSDClient.h>
48
49 #ifdef HAVE_SYS_TYPES_H
50 #include <sys/types.h>
51 #endif
52
53 #ifdef HAVE_SYS_PARAM_H
54 #include <sys/param.h>
55 #endif
56
57 #ifdef HAVE_SYS_MOUNT_H
58 #include <sys/mount.h>
59 #endif
60
61 #ifdef HAVE_LIBGEN_H
62 #include <libgen.h>
63 #endif
64
65 #ifdef HAVE_LIMITS_H
66 #include <limits.h>
67 #endif
68
69
70 /*
71 *
72 */
73 enum disk_state_e {
74 disk_mounted,
75 disk_unmounted,
76 disk_appeared,
77 disk_mounting
78 };
79
80 struct mmcdev {
81 /* Interfaces required for low-level device communication */
82 IOCFPlugInInterface **plugInInterface;
83 MMCDeviceInterface **mmcInterface;
84 SCSITaskDeviceInterface **taskInterface;
85
86 /* device short name (ie disk1) */
87 char bsd_name[MNAMELEN];
88
89 /* for mounting/unmounting the disc */
90 DADiskRef disk;
91 DASessionRef session;
92 enum disk_state_e disk_state;
93 dispatch_semaphore_t sync_sem;
94 dispatch_queue_t background_queue;
95 };
96
device_send_cmd(MMCDEV * mmc,const uint8_t * cmd,uint8_t * buf,size_t tx,size_t rx)97 int device_send_cmd(MMCDEV *mmc, const uint8_t *cmd, uint8_t *buf, size_t tx, size_t rx)
98 {
99 SCSITaskInterface **task = NULL;
100 SCSI_Sense_Data sense;
101 SCSITaskStatus status;
102 SCSITaskSGElement iov;
103 UInt8 direction;
104 UInt64 sent;
105 int rc;
106
107 if (NULL == mmc->taskInterface) {
108 return 0;
109 }
110
111 do {
112 task = (*mmc->taskInterface)->CreateSCSITask(mmc->taskInterface);
113 if (NULL == task) {
114 BD_DEBUG(DBG_MMC, "Could not create SCSI Task\n");
115 break;
116 }
117
118 iov.address = (uintptr_t) buf;
119 iov.length = tx ? tx : rx;
120
121 if (buf) {
122 direction = tx ? kSCSIDataTransfer_FromInitiatorToTarget :
123 kSCSIDataTransfer_FromTargetToInitiator;
124 } else {
125 direction = kSCSIDataTransfer_NoDataTransfer;
126 }
127
128 SCSICommandDescriptorBlock cdb = {0};
129 memcpy(cdb, cmd, sizeof(cdb));
130
131 rc = (*task)->SetCommandDescriptorBlock(task, cdb, kSCSICDBSize_16Byte);
132 if (kIOReturnSuccess != rc) {
133 BD_DEBUG(DBG_MMC, "Error setting SCSI command\n");
134 break;
135 }
136
137 rc = (*task)->SetScatterGatherEntries(task, &iov, 1, iov.length, direction);
138 if (kIOReturnSuccess != rc) {
139 BD_DEBUG(DBG_MMC, "Error setting SCSI scatter gather entries\n");
140 break;
141 }
142
143 rc = (*task)->SetTimeoutDuration(task, 5000000);
144 if (kIOReturnSuccess != rc) {
145 BD_DEBUG(DBG_MMC, "Error setting SCSI command timeout\n");
146 break;
147 }
148
149 memset(&sense, 0, sizeof (sense));
150
151 rc = (*task)->ExecuteTaskSync(task, &sense, &status, &sent);
152
153 char str[512];
154 BD_DEBUG(DBG_MMC, "Send SCSI MMC cmd %s:\n", str_print_hex(str, cmd, 16));
155 if (tx) {
156 BD_DEBUG(DBG_MMC, " Buffer: %s ->\n", str_print_hex(str, buf, tx>255?255:tx));
157 } else {
158 BD_DEBUG(DBG_MMC, " Buffer: %s <-\n", str_print_hex(str, buf, rx>255?255:rx));
159 }
160
161 if (kIOReturnSuccess != rc || status != 0) {
162 BD_DEBUG(DBG_MMC, " Send failed!\n");
163 break;
164 } else {
165 BD_DEBUG(DBG_MMC, " Send succeeded! sent = %lld status = %u. response = %x\n",
166 (unsigned long long) sent, status, sense.VALID_RESPONSE_CODE);
167 }
168
169 (*task)->Release(task);
170
171 return 1;
172 } while (0);
173
174 if (task) {
175 (*task)->Release(task);
176 }
177
178 return 0;
179 }
180
get_mounted_device_from_path(MMCDEV * mmc,const char * path)181 static int get_mounted_device_from_path(MMCDEV *mmc, const char *path) {
182 struct statfs stat_info;
183 int rc;
184
185 rc = statfs(path, &stat_info);
186 if (0 != rc) {
187 return rc;
188 }
189
190 strncpy(mmc->bsd_name, basename (stat_info.f_mntfromname), sizeof (mmc->bsd_name));
191
192 return 0;
193 }
194
iokit_unmount_complete(DADiskRef disk,DADissenterRef dissenter,void * context)195 static void iokit_unmount_complete(DADiskRef disk, DADissenterRef dissenter,
196 void *context) {
197 (void)disk; /* suppress warning */
198 MMCDEV *mmc = context;
199
200 if (dissenter) {
201 BD_DEBUG(DBG_MMC, "Could not unmount the disc\n");
202 } else {
203 BD_DEBUG(DBG_MMC, "Disc unmounted\n");
204 mmc->disk_state = disk_unmounted;
205 }
206 dispatch_semaphore_signal(mmc->sync_sem);
207 }
208
iokit_mount_complete(DADiskRef disk,DADissenterRef dissenter,void * context)209 static void iokit_mount_complete(DADiskRef disk, DADissenterRef dissenter,
210 void *context) {
211 (void) disk; /* suppress warning */
212 MMCDEV *mmc = context;
213
214 if (dissenter) {
215 DAReturn code = DADissenterGetStatus(dissenter);
216 BD_DEBUG(DBG_MMC, "Could not mount the disc (%8X)\n", code);
217 mmc->disk_state = disk_unmounted;
218 } else {
219 BD_DEBUG(DBG_MMC, "Disc mounted\n");
220 mmc->disk_state = disk_mounted;
221 }
222 dispatch_semaphore_signal(mmc->sync_sem);
223 }
224
225 /* Unmount the disk at mmc->disk
226 * Note: This MAY NOT be called on the background queue,
227 * as that would lead to a deadlock.
228 */
iokit_unmount(MMCDEV * mmc)229 static int iokit_unmount(MMCDEV *mmc) {
230 if (disk_unmounted == mmc->disk_state) {
231 return 0; /* nothing to do */
232 }
233
234 BD_DEBUG(DBG_MMC, "Unmounting disk\n");
235
236 DADiskUnmount(mmc->disk, kDADiskUnmountOptionForce, iokit_unmount_complete, mmc);
237 dispatch_semaphore_wait(mmc->sync_sem, DISPATCH_TIME_FOREVER);
238
239 return (mmc->disk_state == disk_unmounted) ? 0 : -1;
240 }
241
242 /* Mount the disk at mmc->disk
243 * Note: This MAY NOT be called on the background queue,
244 * as that would lead to a deadlock.
245 */
iokit_mount(MMCDEV * mmc)246 static int iokit_mount(MMCDEV *mmc) {
247 if (disk_mounted != mmc->disk_state) {
248 if (mmc->disk && mmc->session) {
249 mmc->disk_state = disk_mounting;
250 DADiskMount(mmc->disk, NULL, kDADiskMountOptionDefault, iokit_mount_complete, mmc);
251 dispatch_semaphore_wait(mmc->sync_sem, DISPATCH_TIME_FOREVER);
252 }
253 }
254 return (mmc->disk_state == disk_unmounted) ? 0 : -1;
255 }
256
iokit_find_service_matching(MMCDEV * mmc,io_service_t * servp)257 static int iokit_find_service_matching(MMCDEV *mmc, io_service_t *servp) {
258 CFMutableDictionaryRef matchingDict = IOServiceMatching("IOBDServices");
259 io_iterator_t deviceIterator;
260 io_service_t service;
261 int rc;
262
263 assert(NULL != servp);
264
265 *servp = IO_OBJECT_NULL;
266
267 if (!matchingDict) {
268 BD_DEBUG(DBG_MMC, "Could not create a matching dictionary for IOBDServices\n");
269 return -1;
270 }
271
272 /* this call consumes the reference to the matchingDict. we do not need to release it */
273 rc = IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDict, &deviceIterator);
274 if (kIOReturnSuccess != rc) {
275 BD_DEBUG(DBG_MMC, "Could not create device iterator\n");
276 return -1;
277 }
278
279 while (0 != (service = IOIteratorNext(deviceIterator))) {
280 CFStringRef data;
281 char name[MNAMELEN] = "";
282
283 data = IORegistryEntrySearchCFProperty(service, kIOServicePlane, CFSTR(kIOBSDNameKey),
284 kCFAllocatorDefault, kIORegistryIterateRecursively);
285
286 if (NULL != data) {
287 rc = CFStringGetCString(data, name, sizeof (name), kCFStringEncodingASCII);
288 CFRelease(data);
289 if (0 == strcmp(name, mmc->bsd_name)) {
290 break;
291 }
292 }
293
294 (void) IOObjectRelease(service);
295 }
296
297 IOObjectRelease(deviceIterator);
298
299 *servp = service;
300
301 return (service != IO_OBJECT_NULL) ? 0 : -1;
302 }
303
iokit_find_interfaces(MMCDEV * mmc,io_service_t service)304 static int iokit_find_interfaces(MMCDEV *mmc, io_service_t service) {
305 SInt32 score;
306 int rc;
307
308 rc = IOCreatePlugInInterfaceForService(service, kIOMMCDeviceUserClientTypeID,
309 kIOCFPlugInInterfaceID, &mmc->plugInInterface,
310 &score);
311 if (kIOReturnSuccess != rc || NULL == mmc->plugInInterface) {
312 return -1;
313 }
314
315 BD_DEBUG(DBG_MMC, "Getting MMC interface\n");
316 IOCFPlugInInterface **plugInInterface = mmc->plugInInterface;
317
318 rc = (*plugInInterface)->QueryInterface(plugInInterface,
319 CFUUIDGetUUIDBytes(kIOMMCDeviceInterfaceID),
320 (LPVOID)&mmc->mmcInterface);
321
322 if (kIOReturnSuccess != rc || NULL == mmc->mmcInterface) {
323 BD_DEBUG(DBG_MMC, "Could not get multimedia commands (MMC) interface\n");
324 return -1;
325 }
326
327 BD_DEBUG(DBG_MMC, "Have an MMC interface (%p). Getting a SCSI task interface...\n", (void*)mmc->mmcInterface);
328
329 mmc->taskInterface = (*mmc->mmcInterface)->GetSCSITaskDeviceInterface (mmc->mmcInterface);
330 if (NULL == mmc->taskInterface) {
331 BD_DEBUG(DBG_MMC, "Could not get SCSI task device interface\n");
332 return -1;
333 }
334
335 return 0;
336 }
337
iokit_mount_approval_cb(DADiskRef disk,void * context)338 static DADissenterRef iokit_mount_approval_cb(DADiskRef disk, void *context)
339 {
340 MMCDEV *mmc = context;
341
342 /* If the disk state is mounted, there is nothing to do here. */
343 if (disk_mounted == mmc->disk_state) {
344 return NULL;
345 }
346
347 /* Check if the disk that is to be mounted matches ours,
348 * if not, we do not need to reject mounting.
349 */
350 if (!CFEqual(disk, mmc->disk)) {
351 return NULL;
352 }
353
354 BD_DEBUG(DBG_MMC, "Mount approval request for matching disc\n");
355
356 /* When we are trying to mount, the mount approval callback is
357 * called too, so we need to allow mounting for ourselves here.
358 */
359 if (disk_mounting == mmc->disk_state) {
360 BD_DEBUG(DBG_MMC, "Allowing ourselves to mount\n");
361 return NULL;
362 }
363
364 mmc->disk_state = disk_appeared;
365 dispatch_semaphore_signal(mmc->sync_sem);
366
367 CFStringRef reason = CFSTR("Disk is going to be mounted libaacs");
368 return DADissenterCreate(kCFAllocatorDefault, kDAReturnBusy, reason);
369 }
370
iokit_da_init(MMCDEV * mmc)371 static int iokit_da_init(MMCDEV *mmc) {
372 mmc->session = DASessionCreate(kCFAllocatorDefault);
373 if (NULL == mmc->session) {
374 BD_DEBUG(DBG_MMC | DBG_CRIT, "Could not create a disc arbitration session\n");
375 return -1;
376 }
377
378 mmc->disk = DADiskCreateFromBSDName(kCFAllocatorDefault, mmc->session, mmc->bsd_name);
379 if (NULL == mmc->disk) {
380 BD_DEBUG(DBG_MMC | DBG_CRIT, "Could not create a disc arbitration disc for the device\n");
381 CFRelease(mmc->session);
382 mmc->session = NULL;
383 return -1;
384 }
385
386 mmc->background_queue = dispatch_queue_create("org.videolan.libaacs", DISPATCH_QUEUE_SERIAL);
387 DASessionSetDispatchQueue(mmc->session, mmc->background_queue);
388
389 // Register event callbacks
390 DARegisterDiskMountApprovalCallback(mmc->session, NULL, iokit_mount_approval_cb, mmc);
391
392 mmc->sync_sem = dispatch_semaphore_create(0);
393
394 return 0;
395 }
396
iokit_da_destroy(MMCDEV * mmc)397 static void iokit_da_destroy(MMCDEV *mmc) {
398 if (mmc->session) {
399 /* The approval callback must be unregistered here, doing it in the
400 * mount approval callback instead after we got a matching disk would
401 * cause the OS to immediately re-try to mount the disk faster than we
402 * can mount it.
403 */
404 DAUnregisterApprovalCallback(mmc->session, iokit_mount_approval_cb, mmc);
405 DASessionSetDispatchQueue(mmc->session, NULL);
406 CFRelease(mmc->session);
407 mmc->session = NULL;
408 }
409
410 if (mmc->disk) {
411 CFRelease(mmc->disk);
412 mmc->disk = NULL;
413 }
414
415 dispatch_release(mmc->sync_sem);
416 }
417
mmc_open_iokit(const char * path,MMCDEV * mmc)418 static int mmc_open_iokit(const char *path, MMCDEV *mmc) {
419 io_service_t service;
420 int rc;
421
422 mmc->plugInInterface = NULL;
423 mmc->mmcInterface = NULL;
424 mmc->taskInterface = NULL;
425 mmc->disk = NULL;
426 mmc->session = NULL;
427 mmc->disk_state = disk_mounted;
428
429 /* get the bsd name associated with this mount */
430 rc = get_mounted_device_from_path(mmc, path);
431 if (0 != rc) {
432 BD_DEBUG(DBG_MMC | DBG_CRIT, "Could not locate mounted device associated with %s\n", path);
433 return rc;
434 }
435
436 /* find a matching io service (IOBDServices) */
437 rc = iokit_find_service_matching(mmc, &service);
438 if (0 != rc) {
439 BD_DEBUG(DBG_MMC | DBG_CRIT, "Could not find matching IOBDServices mounted @ %s\n", path);
440 return rc;
441 }
442
443 /* find mmc and scsi task interfaces */
444 rc = iokit_find_interfaces(mmc, service);
445
446 /* done with the ioservice. release it */
447 (void) IOObjectRelease(service);
448
449 if (0 != rc) {
450 return rc;
451 }
452
453 /* Init DiskArbitration */
454 rc = iokit_da_init(mmc);
455 if (0 != rc) {
456 return rc;
457 }
458
459 /* unmount the disk so exclusive access can be obtained (this is required
460 to use the scsi task interface) */
461 rc = iokit_unmount(mmc);
462 if (0 != rc) {
463 BD_DEBUG(DBG_MMC | DBG_CRIT, "Failed to unmount the disc at %s\n", path);
464 return rc;
465 }
466
467 /* finally, obtain exclusive access */
468 rc = (*mmc->taskInterface)->ObtainExclusiveAccess(mmc->taskInterface);
469 if (kIOReturnSuccess != rc) {
470 BD_DEBUG(DBG_MMC | DBG_CRIT, "Failed to obtain exclusive access. rc = %x\n", rc);
471 return -1;
472 }
473
474 BD_DEBUG(DBG_MMC, "MMC Open complete\n");
475
476 return 0;
477 }
478
device_open(const char * path)479 MMCDEV *device_open(const char *path)
480 {
481 MMCDEV *mmc;
482 int rc;
483
484 mmc = calloc(1, sizeof(MMCDEV));
485 if (!mmc) {
486 BD_DEBUG(DBG_MKB | DBG_CRIT, "out of memory\n");
487 return NULL;
488 }
489
490 rc = mmc_open_iokit(path, mmc);
491 if (0 != rc) {
492 device_close(&mmc);
493 return NULL;
494 }
495
496 return mmc;
497 }
498
device_close(MMCDEV ** pp)499 void device_close(MMCDEV **pp)
500 {
501 __block int rc = 0;
502 if (pp && *pp) {
503 MMCDEV *mmc = *pp;
504
505 /* When the exclusive access to the drive is released,
506 * the OS will see the device like a "new" device and
507 * try to mount it. Therefore we can't just mount the
508 * disk we previously got immediately here as it would
509 * fail with kDAReturnBadArgument as the disk is not
510 * available yet.
511 * Trying to mount the disk after it appears in peek
512 * does not work either as the disk is not yet ready
513 * or in the process of being mounted by the OS so
514 * that would return an kDAReturnBusy error.
515 * The only way that seems to reliably work is to use
516 * a mount approval callback. When the OS tries to
517 * mount the disk, the mount approval callback is
518 * called and we can reject mounting and then proceed
519 * to mount the disk ourselves.
520 * Claiming exclusive access using DADiskClaim in order
521 * to prevent the OS form mounting the disk does not work
522 * either!
523 */
524
525 if (mmc->taskInterface) {
526 (*mmc->taskInterface)->ReleaseExclusiveAccess(mmc->taskInterface);
527 (*mmc->taskInterface)->Release(mmc->taskInterface);
528 mmc->taskInterface = NULL;
529 }
530
531 if (mmc->mmcInterface) {
532 (*mmc->mmcInterface)->Release(mmc->mmcInterface);
533 mmc->mmcInterface = NULL;
534 }
535
536 if (mmc->plugInInterface) {
537 IODestroyPlugInInterface(mmc->plugInInterface);
538 }
539
540 if (!mmc->sync_sem) {
541 /* open failed before iokit_da_init() */
542 X_FREE(*pp);
543 return;
544 }
545
546 /* Wait for disc to re-appear for 20 seconds.
547 * This timeout was figured out by experimentation with
548 * a USB BD drive which sometimes can take really long to
549 * be in a mountable state again.
550 * For internal drives this is probably much faster
551 * so the long timeout shouldnt do much harm for thse
552 * cases.
553 */
554 dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 20 * 1E+9);
555 dispatch_semaphore_wait(mmc->sync_sem, timeout);
556
557 /* It is crucial that this is done on the event handling queue
558 * else callbacks could be received while this code runs.
559 */
560 dispatch_sync(mmc->background_queue, ^{
561 if (disk_appeared != mmc->disk_state) {
562 BD_DEBUG(DBG_MMC | DBG_CRIT, "Timeout waiting for the disc to appear again!\n");
563 iokit_da_destroy(mmc);
564 rc = -1;
565 return;
566 }
567 rc = 0;
568 });
569
570 if (rc == 0) {
571 /* Disk appeared successfully, mount it.
572 * Return value is ignored as logging of success or
573 * error takes place in the callback already and there
574 * is nothing we can do really if mounting fails.
575 */
576 (void) iokit_mount(mmc);
577 iokit_da_destroy(mmc);
578 }
579 X_FREE(*pp);
580 }
581 }
582