1 /*
2  * Copyright 2014-2017 Frank Hunleth
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 #if __APPLE__
18 #include "mmc.h"
19 #include "util.h"
20 
21 #include <CoreFoundation/CoreFoundation.h>
22 #include <DiskArbitration/DiskArbitration.h>
23 #include <IOKit/storage/IOStorageProtocolCharacteristics.h>
24 
25 #include <sys/socket.h>
26 #include <sys/param.h>
27 
28 // DiskArbitration API session
29 static DASessionRef da_session;
30 
31 /**
32  * Initialize mmc support
33  */
mmc_init()34 void mmc_init()
35 {
36     da_session = DASessionCreate(kCFAllocatorDefault);
37     DASessionScheduleWithRunLoop(da_session, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
38 }
39 
40 /**
41  * Free memory and resources for working with MMC devices
42  */
mmc_finalize()43 void mmc_finalize()
44 {
45     CFRelease(da_session);
46 }
47 
mmc_path_to_bsdname(const char * mmc_device)48 static const char *mmc_path_to_bsdname(const char *mmc_device)
49 {
50     // mmc_devices have two forms: "/dev/diskX" and "/dev/rdiskX"
51     // DADiskCreateFromBSDName wants "diskX"
52 
53     // Check for enough characters in the device name for either form.
54     if (strlen(mmc_device) < 10)
55         return NULL;
56 
57     const char *bsdname = NULL;
58     if (memcmp(mmc_device, "/dev/disk", 9) == 0)
59         bsdname = &mmc_device[5];
60     else if (memcmp(mmc_device, "/dev/rdisk", 10) == 0)
61         bsdname = &mmc_device[6];
62     else
63         return NULL;
64 
65     // Check that we're passed a whole disk as the mmc_device (e.g. only numbers between disk and the end of string)
66     int offset = strspn(&bsdname[4], "0123456789");
67     if (bsdname[4 + offset] != '\0')
68         return NULL;
69 
70     return bsdname;
71 }
72 
mmc_device_to_diskref(const char * mmc_device)73 static DADiskRef mmc_device_to_diskref(const char *mmc_device)
74 {
75     const char *bsdname = mmc_path_to_bsdname(mmc_device);
76     if (bsdname == NULL)
77         return NULL;
78 
79     // Let the Disk Arbitration API perform any additional checks and return the DADiskRef
80     return DADiskCreateFromBSDName(kCFAllocatorDefault, da_session, bsdname);
81 }
82 
83 struct scan_context
84 {
85     struct mmc_device *devices;
86     int max_devices;
87     int count;
88 };
89 
scan_disk_appeared_cb(DADiskRef disk,void * c)90 static void scan_disk_appeared_cb(DADiskRef disk, void *c)
91 {
92     struct scan_context *context = (struct scan_context *) c;
93     int ix = context->count;
94     if (ix < context->max_devices) {
95         snprintf(context->devices[ix].path, sizeof(context->devices[ix].path), "/dev/r%s", DADiskGetBSDName(disk));
96 
97         CFDictionaryRef info = DADiskCopyDescription(disk);
98         CFNumberRef cf_size = CFDictionaryGetValue(info, kDADiskDescriptionMediaSizeKey);
99         int64_t size;
100         CFNumberGetValue(cf_size, kCFNumberSInt64Type, &size);
101         context->devices[ix].size = size;
102 
103         CFStringRef cf_name = CFDictionaryGetValue(info, kDADiskDescriptionMediaNameKey);
104         CFStringGetCString(cf_name, context->devices[ix].name, MMC_DEVICE_NAME_LEN, kCFStringEncodingISOLatin1);
105 
106         CFStringRef cf_device_protocol = CFDictionaryGetValue(info, kDADiskDescriptionDeviceProtocolKey);
107         const char *protocol = CFStringGetCStringPtr(cf_device_protocol, kCFStringEncodingUTF8);
108         bool is_virtual = protocol != 0 && strcmp(protocol, kIOPropertyPhysicalInterconnectTypeVirtual) == 0;
109 
110         CFRelease(info);
111 
112         // Filter out virtual devices like Time Machine network backup drives
113         if (is_virtual)
114             return;
115 
116         context->count++;
117     }
118 }
119 
timeout_cb(CFRunLoopTimerRef timer,void * context)120 static void timeout_cb(CFRunLoopTimerRef timer, void *context)
121 {
122     (void) timer;
123 
124     bool *timed_out = (bool *) context;
125     *timed_out = true;
126 
127     CFRunLoopStop(CFRunLoopGetCurrent());
128 }
129 
run_loop_for_time(double duration)130 static int run_loop_for_time(double duration)
131 {
132     bool timed_out = false;
133     CFRunLoopTimerContext timer_context = { 0, &timed_out, NULL, NULL, NULL };
134     CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + duration, 0.0, 0, 0, timeout_cb, &timer_context);
135     CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopCommonModes);
136 
137     CFRunLoopRun();
138     CFRunLoopRemoveTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopCommonModes);
139     CFRelease(timer);
140 
141     return timed_out ? -1 : 0;
142 }
143 
mmc_scan_for_devices(struct mmc_device * devices,int max_devices)144 int mmc_scan_for_devices(struct mmc_device *devices, int max_devices)
145 {
146     memset(devices, 0, max_devices * sizeof(struct mmc_device));
147 
148     // Only look for removable media
149     CFMutableDictionaryRef toMatch =
150             CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
151     CFDictionaryAddValue(toMatch, kDADiskDescriptionMediaWholeKey, kCFBooleanTrue);
152     CFDictionaryAddValue(toMatch, kDADiskDescriptionMediaRemovableKey, kCFBooleanTrue);
153     CFDictionaryAddValue(toMatch, kDADiskDescriptionMediaWritableKey, kCFBooleanTrue);
154 
155     struct scan_context context;
156     context.devices = devices;
157     context.max_devices = max_devices;
158     context.count = 0;
159     DARegisterDiskAppearedCallback(da_session, toMatch, scan_disk_appeared_cb, &context);
160 
161     // Scan for removable media for 100 ms
162     // NOTE: It's not clear how long the event loop has to run. Ideally, it would
163     // terminate after all devices have been found, but I don't know how to do that.
164     run_loop_for_time(0.1);
165 
166     return context.count;
167 }
168 
169 /**
170  * @brief Run authopen to acquire a file descriptor to the mmc device
171  *
172  * Like Linux, OSX does not allow processes to read and write devices as
173  * normal users. OSX provides a utility called authopen that can ask the
174  * user for permission to access a file.
175  *
176  * @param pathname the full path to the device
177  * @return a descriptor or -1 on error
178  */
authopen_fd(char * const pathname)179 static int authopen_fd(char * const pathname)
180 {
181     int sockets[2];
182     if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) < 0)
183         fwup_err(EXIT_FAILURE, "Can't create socketpair");
184 
185     pid_t pid = fork();
186     if (pid == 0) {
187         // child
188         int devnull = open("/dev/null", O_RDWR);
189         if (devnull < 0)
190             fwup_err(EXIT_FAILURE, "/dev/null");
191 
192         close(STDIN_FILENO);
193         close(STDOUT_FILENO);
194         if (dup2(devnull, STDIN_FILENO) < 0)
195             fwup_err(EXIT_FAILURE, "dup2 devnull");
196         if (dup2(sockets[1], STDOUT_FILENO) < 0)
197             fwup_err(EXIT_FAILURE, "dup2 pipe");
198         close(devnull);
199 
200         char permissions[16];
201         snprintf(permissions, sizeof(permissions), "%d", O_RDWR);
202         char * const exec_argv[] = { "/usr/libexec/authopen",
203                               "-stdoutpipe",
204                               "-o",
205                               permissions,
206                               pathname,
207                               0 };
208         execvp(exec_argv[0], exec_argv);
209 
210         // Not supposed to reach here.
211         fwup_err(EXIT_FAILURE, "execvp failed");
212     } else {
213         // parent
214         close(sockets[1]); // No writes to the pipe
215 
216         // Receive the authorized file descriptor from authopen
217         char buffer[sizeof(struct cmsghdr) + sizeof(int)];
218         struct iovec io_vec[1];
219         io_vec[0].iov_base = buffer;
220         io_vec[0].iov_len = sizeof(buffer);
221 
222         struct msghdr message;
223         memset(&message, 0, sizeof(message));
224         message.msg_iov = io_vec;
225         message.msg_iovlen = 1;
226 
227         char cmsg_socket[CMSG_SPACE(sizeof(int))];
228         message.msg_control = cmsg_socket;
229         message.msg_controllen = sizeof(cmsg_socket);
230 
231         int fd = -1;
232         for (;;) {
233             ssize_t size = recvmsg(sockets[0], &message, 0);
234             if (size > 0) {
235                 struct cmsghdr* cmsg_socket_header = CMSG_FIRSTHDR(&message);
236                 if (cmsg_socket_header &&
237                         cmsg_socket_header->cmsg_level == SOL_SOCKET &&
238                         cmsg_socket_header->cmsg_type == SCM_RIGHTS) {
239                     // Got file descriptor
240                     memcpy(&fd, CMSG_DATA(cmsg_socket_header), sizeof(fd));
241                     break;
242                 }
243             } else if (errno != EINTR) {
244                 // Any other cause
245                 break;
246             }
247         }
248 
249         // No more reads from the pipe.
250         close(sockets[0]);
251 
252         return fd;
253     }
254 }
255 
mmc_device_size(const char * mmc_path,off_t * end_offset)256 int mmc_device_size(const char *mmc_path, off_t *end_offset)
257 {
258     // Initialize to "unknown" size should anything go wrong.
259     *end_offset = 0;
260 
261     DADiskRef disk = mmc_device_to_diskref(mmc_path);
262     int rc = -1;
263     if (disk) {
264         CFDictionaryRef info = DADiskCopyDescription(disk);
265         CFNumberRef cfsize = CFDictionaryGetValue(info, kDADiskDescriptionMediaSizeKey);
266         int64_t size;
267         CFNumberGetValue(cfsize, kCFNumberSInt64Type, &size);
268         *end_offset = size;
269         CFRelease(info);
270         CFRelease(disk);
271         rc = 0;
272     }
273     return rc;
274 }
275 
276 /**
277  * Return a file handle to the specified path for mmc devices
278  *
279  * @param mmc_path
280  * @return
281  */
mmc_open(const char * mmc_path)282 int mmc_open(const char *mmc_path)
283 {
284     const char *bsdname = mmc_path_to_bsdname(mmc_path);
285     if (bsdname == NULL)
286         return -1;
287 
288     // always operate on the raw device
289     char raw_path[16];
290     snprintf(raw_path, sizeof(raw_path), "/dev/r%s", bsdname);
291 
292     // Use authopen to get permissions to the device
293     return authopen_fd(raw_path);
294 }
295 
296 struct disk_op_context
297 {
298     const char *operation;
299     bool succeeded;
300 };
301 
disk_op_done_cb(DADiskRef disk,DADissenterRef dissenter,void * c)302 static void disk_op_done_cb(DADiskRef disk, DADissenterRef dissenter, void *c)
303 {
304     (void) disk;
305 
306     struct disk_op_context *context = (struct disk_op_context *) c;
307     if (dissenter) {
308         CFStringRef what = DADissenterGetStatusString(dissenter);
309         fwup_warnx("%s failed: 0x%x (%d) %s)",
310                    context->operation,
311                    DADissenterGetStatus(dissenter),
312                    DADissenterGetStatus(dissenter),
313                    CFStringGetCStringPtr(what, kCFStringEncodingMacRoman));
314 
315         context->succeeded = false;
316     } else {
317         context->succeeded = true;
318     }
319 
320     CFRunLoopStop(CFRunLoopGetCurrent());
321 }
322 
mmc_umount_all(const char * mmc_device)323 int mmc_umount_all(const char *mmc_device)
324 {
325     DADiskRef disk = mmc_device_to_diskref(mmc_device);
326     int rc = -1;
327     if (disk) {
328         struct disk_op_context context;
329         context.operation = "unmount";
330         DADiskUnmount(disk, kDADiskUnmountOptionWhole, disk_op_done_cb, &context);
331 
332         // Wait for a while since unmounting sometimes takes time.
333         if (run_loop_for_time(10) < 0)
334             fwup_warnx("unmount timed out");
335 
336         if (context.succeeded)
337             rc = 0;
338         CFRelease(disk);
339     }
340     return rc;
341 }
342 
mmc_eject(const char * mmc_device)343 int mmc_eject(const char *mmc_device)
344 {
345     DADiskRef disk = mmc_device_to_diskref(mmc_device);
346     int rc = -1;
347     if (disk) {
348         struct disk_op_context context;
349         context.operation = "eject";
350         DADiskEject(disk, kDADiskEjectOptionDefault, disk_op_done_cb,  &context);
351 
352         if (run_loop_for_time(10) < 0)
353             fwup_warnx("eject timed out");
354 
355         if (context.succeeded)
356             rc = 0;
357 
358         CFRelease(disk);
359     }
360     return rc;
361 }
362 
mmc_is_path_on_device(const char * file_path,const char * device_path)363 int mmc_is_path_on_device(const char *file_path, const char *device_path)
364 {
365     // Not implemented - I don't think there's a use case for this on Mac.
366     (void) file_path;
367     (void) device_path;
368     return -1;
369 }
370 
mmc_is_path_at_device_offset(const char * file_path,off_t block_offset)371 int mmc_is_path_at_device_offset(const char *file_path, off_t block_offset)
372 {
373     // Not implemented - I don't think there's a use case for this on Mac.
374     (void) file_path;
375     (void) block_offset;
376     return -1;
377 }
378 
mmc_trim(int fd,off_t offset,off_t count)379 int mmc_trim(int fd, off_t offset, off_t count)
380 {
381     // Not implemented
382     fwup_warnx("TRIM command not implemented.");
383     (void) fd;
384     (void) offset;
385     (void) count;
386     return 0;
387 }
388 #endif // __APPLE__
389