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