]>
Commit | Line | Data |
---|---|---|
fa7225c8 A |
1 | /* |
2 | * Copyright (c) 2016 Apple Inc. All Rights Reserved. | |
3 | * | |
4 | * @APPLE_LICENSE_HEADER_START@ | |
5 | * | |
6 | * This file contains Original Code and/or Modifications of Original Code | |
7 | * as defined in and that are subject to the Apple Public Source License | |
8 | * Version 2.0 (the 'License'). You may not use this file except in | |
9 | * compliance with the License. Please obtain a copy of the License at | |
10 | * http://www.opensource.apple.com/apsl/ and read it before using this | |
11 | * file. | |
12 | * | |
13 | * The Original Code and all software distributed under the License are | |
14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER | |
15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, | |
16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, | |
17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. | |
18 | * Please see the License for the specific language governing rights and | |
19 | * limitations under the License. | |
20 | * | |
21 | * @APPLE_LICENSE_HEADER_END@ | |
22 | */ | |
23 | ||
24 | #include <stdio.h> | |
25 | #include <sys/param.h> | |
26 | #include <sys/mount.h> | |
27 | ||
28 | #include <string> | |
29 | #include <vector> | |
30 | ||
31 | #include <security_utilities/cfutilities.h> | |
32 | #include <security_utilities/unix++.h> | |
33 | #include <security_utilities/logging.h> | |
34 | ||
35 | #include "SecTranslocate.h" | |
36 | #include "SecTranslocateShared.hpp" | |
37 | #include "SecTranslocateInterface.hpp" | |
38 | #include "SecTranslocateUtilities.hpp" | |
39 | ||
40 | ||
41 | /* Strategy: | |
42 | ||
43 | This library exists to create and destroy app translocation points. To ensure that a given | |
44 | process using this library is only making or destroying one mountpoint at a time, the two | |
45 | interface functions are sychronized on a dispatch queue. | |
46 | ||
47 | **** App Translocation Strategy w/o a destination path **** | |
48 | (This functionality is implemented in SecTranslocateShared.hpp/cpp) | |
49 | ||
50 | To create a translocation mountpoint, if no destination path is provided, first the app | |
51 | path to be translocated is realpathed to ensure it exists and there are no symlink's in | |
52 | the path we work with. Then the calling user's _CS_DARWIN_USER_TEMP_DIR is found. This | |
53 | is used to calculate the user's AppTranslocation directory. The formula is: | |
54 | User's App Translocation Directory = realpath(confstr(_CS_DARWIN_USER_TEMP_DIR))+/AppTranslocation/ | |
55 | ||
56 | Then the mount table is checked to see whether or not a translocation point already exists | |
57 | for this user for the app being requested. The rule for an already existing mount is that | |
58 | there must exist a mountpoint in the user's app translocation directory that is mounted | |
59 | from realpath of the requested app. | |
60 | ||
61 | If a mount exists already for this user, then the path to the app in that mountpoint is | |
62 | calculated and sanity checked. | |
63 | ||
64 | The rules to create the app path inside the mountpoint are: | |
65 | ||
66 | original app path = /some/path/<app name> | |
67 | new app path = realpath(confstr(_CS_DARWIN_USER_TEMP_DIR))+/AppTranslocation/<UUID>/d/<app name> | |
68 | ||
69 | The sanity check for the new app path is that: | |
70 | 1. new app path exists | |
71 | 2. new app path is in a nullfs mount | |
72 | 3. new app path is already completely resolved. | |
73 | ||
74 | If the sanity checks pass for the new app path, then that path is returned to the caller. | |
75 | ||
76 | If no translocation mount point exists already per the mount table then an AppTranslocation | |
77 | directory is created within the temp dir if it doesn't already exist. After that a UUID is | |
78 | generated, that UUID is used as the name of a new directory in the AppTranslocation directory. | |
79 | Once the new directory has been created and sanity checked, mount is called to create the | |
80 | translocation between the original path and the new directory. Then the new path to the app | |
81 | within the mountpoint is calculated and sanity checked. | |
82 | ||
83 | The sanity check rules for the mountpoint before the mount are: | |
84 | 1. Something exists at the expected path | |
85 | 2. That something is a directory | |
86 | 3. That something is not already a mountpoint | |
87 | 4. The expected path is fully resolved | |
88 | ||
89 | The sanity check for the new app path is listed above (for the mountpoint exists case). | |
90 | ||
91 | **** App Translocation strategy w/ a destination path **** | |
92 | (This functionality is implemented in SecTranslocateShared.hpp/cpp) | |
93 | ||
94 | If a destination path is provided, a sequence similar to that described above is followed | |
95 | with the following modifications. | |
96 | ||
97 | The destination path is expected to be of the same form as new app path. This expectation | |
98 | is verified. | |
99 | ||
100 | First we verify that the destination path ends with /d/<app name> and that the <app name> | |
101 | component of the destination path matches the <app name> of the original app app path | |
102 | requested. If not, an error occurs. Everything before the /d/ is treated becomes the | |
103 | requested destination mount point. | |
104 | ||
105 | After the user's app translocation directory is calculated, we ensure that the requested | |
106 | destination mount point is prefixed by the translocation directory, and contains only one | |
107 | path component after the user's app translocation path, otherwise an error occurs. | |
108 | ||
109 | When we check the mount table, we make sure that if the a translocation of the app already | |
110 | exists for the user, then the translocation path must exactly match the requested | |
111 | destination path, otherwise an error occurs. | |
112 | ||
113 | If no mountpoint exists for the app, then we attempt to create the requested directory within | |
114 | the user's app translocation directory. This becomes the mount point, and the mount point | |
115 | sanity checks listed above are applied. | |
116 | ||
117 | If the requested destination mountpoint is successfully created, the flow continues as above | |
118 | to create the mount and verify the requested path within the mountpoint. The only extra step | |
119 | here is that at the end, the requested app path, must exactly equal the created app path. | |
120 | ||
121 | **** App Translocation error cleanup **** | |
122 | (This functionality is implemented in SecTranslocateShared.hpp/cpp) | |
123 | ||
124 | The error cleanup strategy for translocation creation is to try to destroy any directories | |
125 | or mount points in the user's translocation directory that were created before an error | |
126 | was detected. This means tracking whether we created a directory, or it already existed when | |
127 | a caller asked for it. Clean up is considered best effort. | |
128 | ||
129 | **** Deleting an App Translocation point **** | |
130 | (This functionality is implemented in SecTranslocateShared.hpp/cpp) | |
131 | ||
132 | To destroy an app translocation point, the first thing we do is calculate the user's app | |
133 | translocation directory to ensure that the requested path is actually within that directory. | |
134 | We also verify that it is in fact a nullfs mount point. If it is, then we attempt to unmount and | |
135 | remove the translocation point. | |
136 | ||
137 | Regardless of whether or not the requested path is a translocation point, we opportunistically | |
138 | attempt to cleanup the app translocation directory. Clean up means, looping through all the | |
139 | directories currently in the user's app translocation directory and checking whether or not | |
140 | they are a mount point. If a directory inside the user's app translocation directory is not | |
141 | a mountpoint, then we attempt to delete it. | |
142 | ||
143 | **** Quarantine considerations **** | |
144 | (This functionality is implemented in SecTranslocateShared.hpp/cpp and SecTranslocateUtilities.hpp/cpp) | |
145 | ||
146 | If the original app path includes files with quarantine extended attributes, then those extended | |
147 | attributes will be readable through the created app translocation mountpoint. nullfs does not | |
148 | support removing or setting extended attributes on its vnodes. Changes to the quarantine | |
149 | attributes at the original path will be reflected in the app translocation mountpoint without | |
150 | creating a new mount point. | |
151 | ||
152 | If the original app path is inside a quarantined mountpoint (such as a quarantined dmg), then | |
153 | that the quarantine information for that mountpoint is read from the original app path's | |
154 | mountpoint and applied to the created app translocation mountpoint. | |
155 | ||
156 | **** Concurrency considerations **** | |
157 | This library treats the kernel as the source of truth for the status of the file system. | |
158 | Unfortunately it has no way to lock the state of the file system and mount table while | |
159 | it is operating. Because of this, there are two potential areas that have race windows. | |
160 | ||
161 | First, if any other system entity (thread within the same process, or other process | |
162 | within the system) is adding or removing entries from the mount table while | |
163 | SecTranslocateCreateSecureDirectoryForURL is executing, then there is the possibility that | |
164 | an incorrect decision will be made about the current existence of a mount point for a user | |
165 | for an app. This is because getfsstat gets a snapshot of the mount table state rather than a | |
166 | locked window into the kernel and because we make two seperate calls to getfsstat, one to get | |
167 | the number of mountpoints, and a second to actually read the mountpoint data. If more than | |
168 | one process is using this library for the same user, then both processes could attempt to | |
169 | create a translocation for the same app, and this could result in more than one translocation | |
170 | for that app for the user. This shouldn't effect the user other than using additional | |
171 | system resources. We attempt to mitigate this by allocating double the required memory from | |
172 | the first call and then trying the process again (once) if the initial memory was filled. | |
173 | ||
174 | Second, if more than one process is using this library simultaneously and one process calls | |
175 | SecTranslocateDeleteSecureDirectory for a user and the other calls | |
176 | SecTranslocateCreateSecureDirectoryForURL for that same user, then the call to | |
177 | SecTranslocateDeleteSecureDirectory may cause SecTranslocateCreateSecureDirectoryForURL to | |
178 | fail. This will occur if the loop checking for unmounted directories in the user's app | |
179 | translocation directory deletes a newly created directory before the mount call finishes. This | |
180 | race condition will probably result in a failed app launch. A second attempt to launch the app | |
181 | will probably succeed. | |
182 | ||
183 | Concurrency is now split between SecTranslocateClient.hpp/cpp, SecTranslocateServer.hpp/cpp, | |
184 | SecTranslocateDANotification.hpp/cpp, SecTranslocateLSNotification.hpp/cpp, and | |
185 | SecTranslocateXPCServer.hpp/cpp. Each of these represent different ways of entering translocation | |
186 | functionality. | |
187 | ||
188 | **** Logging Strategy **** | |
189 | Use warning logging for interesting conditions (e.g. translocation point created or destroyed). | |
190 | Use error logging for non-fatal failures. Use critical logging for fatal failures. | |
191 | */ | |
192 | ||
193 | /* Make a CFError from an POSIX error code */ | |
194 | static CFErrorRef SecTranslocateMakePosixError(CFIndex errorCode) | |
195 | { | |
196 | return CFErrorCreate(NULL, kCFErrorDomainPOSIX, errorCode, NULL); | |
197 | } | |
198 | ||
199 | /* must be called before any other function in this SPI if the process is intended to be the server */ | |
200 | Boolean SecTranslocateStartListening(CFErrorRef* __nullable error) | |
201 | { | |
202 | Boolean result = false; | |
203 | CFIndex errorCode = 0; | |
204 | try | |
205 | { | |
206 | /* ask getTranslocator for the server */ | |
207 | result = Security::SecTranslocate::getTranslocator(true) != NULL; | |
208 | } | |
209 | catch (Security::UnixError err) | |
210 | { | |
211 | errorCode = err.unixError(); | |
212 | } | |
213 | catch(...) | |
214 | { | |
215 | Syslog::critical("SecTranslocate: uncaught exception during server initialization"); | |
216 | errorCode = EINVAL; | |
217 | } | |
218 | ||
219 | if (error && errorCode) | |
220 | { | |
221 | *error = SecTranslocateMakePosixError(errorCode); | |
222 | } | |
223 | ||
224 | return result; | |
225 | } | |
226 | ||
227 | /* placeholder api for now to allow for future options at startup */ | |
228 | Boolean SecTranslocateStartListeningWithOptions(CFDictionaryRef __unused options, CFErrorRef * __nullable outError) | |
229 | { | |
230 | return SecTranslocateStartListening(outError); | |
231 | } | |
232 | ||
233 | /* Register that a (translocated) pid has launched */ | |
234 | void SecTranslocateAppLaunchCheckin(pid_t pid) | |
235 | { | |
236 | try | |
237 | { | |
238 | Security::SecTranslocate::getTranslocator()->appLaunchCheckin(pid); | |
239 | } | |
240 | catch (...) | |
241 | { | |
242 | Syslog::error("SecTranslocate: error in SecTranslocateAppLaunchCheckin"); | |
243 | } | |
244 | } | |
245 | ||
246 | /* Create an app translocation point given the original path and an optional destination path. */ | |
247 | CFURLRef __nullable SecTranslocateCreateSecureDirectoryForURL (CFURLRef pathToTranslocate, | |
248 | CFURLRef __nullable destinationPath, | |
249 | CFErrorRef* __nullable error) | |
250 | { | |
251 | CFURLRef result = NULL; | |
252 | CFIndex errorCode = 0; | |
253 | ||
254 | try | |
255 | { | |
256 | string sourcePath = cfString(pathToTranslocate); // returns an absolute path | |
257 | ||
d64be36e | 258 | Security::SecTranslocate::TranslocationPath toTranslocatePath(sourcePath, Security::SecTranslocate::TranslocationOptions::Default); |
fa7225c8 A |
259 | |
260 | if(!toTranslocatePath.shouldTranslocate()) | |
261 | { | |
262 | /* We shouldn't translocate so, just retain so that the return value can be treated as a copy */ | |
263 | CFRetain(pathToTranslocate); | |
264 | return pathToTranslocate; | |
265 | } | |
266 | ||
267 | /* We need to translocate so keep going */ | |
268 | string destPath; | |
269 | ||
270 | if(destinationPath) | |
271 | { | |
272 | destPath = cfString(destinationPath); //returns an absolute path | |
273 | } | |
274 | ||
275 | string out_path = Security::SecTranslocate::getTranslocator()->translocatePathForUser(toTranslocatePath, destPath); | |
276 | ||
277 | if(!out_path.empty()) | |
278 | { | |
279 | result = makeCFURL(out_path, true); | |
280 | } | |
281 | else | |
282 | { | |
283 | Syslog::error("SecTranslocateCreateSecureDirectoryForURL: No mountpoint and no prior exception. Shouldn't be here"); | |
284 | UnixError::throwMe(EINVAL); | |
285 | } | |
286 | ||
287 | } | |
288 | catch (Security::UnixError err) | |
289 | { | |
290 | errorCode = err.unixError(); | |
291 | } | |
292 | catch(...) | |
293 | { | |
294 | Syslog::critical("SecTranslocate: uncaught exception during mountpoint creation"); | |
295 | errorCode = EACCES; | |
296 | } | |
297 | ||
298 | if (error && errorCode) | |
299 | { | |
300 | *error = SecTranslocateMakePosixError(errorCode); | |
301 | } | |
302 | return result; | |
303 | } | |
304 | ||
305 | /* Destroy the specified translocated path, and clean up the user's translocation directory. */ | |
306 | Boolean SecTranslocateDeleteSecureDirectory(CFURLRef translocatedPath, CFErrorRef* __nullable error) | |
307 | { | |
308 | bool result = false; | |
309 | int errorCode = 0; | |
310 | ||
311 | if(translocatedPath == NULL) | |
312 | { | |
313 | errorCode = EINVAL; | |
314 | goto end; | |
315 | } | |
316 | ||
317 | try | |
318 | { | |
319 | string pathToDestroy = cfString(translocatedPath); | |
320 | result = Security::SecTranslocate::getTranslocator()->destroyTranslocatedPathForUser(pathToDestroy); | |
321 | } | |
322 | catch (Security::UnixError err) | |
323 | { | |
324 | errorCode = err.unixError(); | |
325 | } | |
326 | catch(...) | |
327 | { | |
328 | Syslog::critical("SecTranslocate: uncaught exception during mountpoint deletion"); | |
329 | errorCode = EACCES; | |
330 | } | |
331 | end: | |
332 | if (error && errorCode) | |
333 | { | |
334 | *error = SecTranslocateMakePosixError(errorCode); | |
335 | } | |
336 | ||
337 | return result; | |
338 | } | |
339 | ||
d64be36e A |
340 | CFURLRef __nullable SecTranslocateCreateGeneric (CFURLRef pathToTranslocate, |
341 | CFURLRef destinationPath, | |
342 | CFErrorRef* __nullable error) | |
343 | { | |
344 | CFURLRef result = NULL; | |
345 | CFIndex errorCode = 0; | |
346 | ||
347 | try | |
348 | { | |
349 | string sourcePath = cfString(pathToTranslocate); | |
350 | Security::SecTranslocate::GenericTranslocationPath path{sourcePath, Security::SecTranslocate::TranslocationOptions::Unveil}; | |
351 | ||
352 | string dpath = cfString(destinationPath); | |
353 | string out_path = Security::SecTranslocate::getTranslocator()->translocatePathForUser(path, dpath); | |
354 | ||
355 | if(!out_path.empty()) | |
356 | { | |
357 | result = makeCFURL(out_path, true); | |
358 | } | |
359 | else | |
360 | { | |
361 | Syslog::error("SecTranslocateCreateGeneric: No mountpoint and no prior exception. Shouldn't be here"); | |
362 | UnixError::throwMe(EINVAL); | |
363 | } | |
364 | ||
365 | } | |
366 | catch (Security::UnixError err) | |
367 | { | |
368 | errorCode = err.unixError(); | |
369 | } | |
370 | catch(...) | |
371 | { | |
372 | Syslog::critical("SecTranslocateCreateGeneric: uncaught exception during mountpoint creation"); | |
373 | errorCode = EACCES; | |
374 | } | |
375 | ||
376 | if (error && errorCode) | |
377 | { | |
378 | *error = SecTranslocateMakePosixError(errorCode); | |
379 | } | |
380 | return result; | |
381 | } | |
382 | ||
fa7225c8 A |
383 | /* Decide whether we need to translocate */ |
384 | Boolean SecTranslocateURLShouldRunTranslocated(CFURLRef path, bool* shouldTranslocate, CFErrorRef* __nullable error) | |
385 | { | |
386 | bool result = false; | |
387 | int errorCode = 0; | |
388 | ||
389 | if(path == NULL || shouldTranslocate == NULL) | |
390 | { | |
391 | errorCode = EINVAL; | |
392 | goto end; | |
393 | } | |
394 | ||
395 | try | |
396 | { | |
397 | string pathToCheck = cfString(path); | |
d64be36e | 398 | Security::SecTranslocate::TranslocationPath tPath(pathToCheck, Security::SecTranslocate::TranslocationOptions::Default); |
fa7225c8 A |
399 | *shouldTranslocate = tPath.shouldTranslocate(); |
400 | result = true; | |
401 | } | |
402 | catch (Security::UnixError err) | |
403 | { | |
404 | errorCode = err.unixError(); | |
405 | } | |
406 | catch(...) | |
407 | { | |
408 | Syslog::critical("SecTranslocate: uncaught exception during policy check"); | |
409 | errorCode = EACCES; | |
410 | } | |
411 | ||
412 | end: | |
413 | if (error && errorCode) | |
414 | { | |
415 | *error = SecTranslocateMakePosixError(errorCode); | |
416 | } | |
417 | ||
418 | return result; | |
419 | } | |
420 | ||
421 | /* Answer whether or not the passed in URL is a nullfs URL. This just checks nullfs rather than | |
422 | nullfs + in the user's translocation path to allow callers like LaunchServices to apply special | |
423 | handling to nullfs mounts regardless of the calling user (i.e. root lsd can identify all translocated | |
424 | mount points for all users). | |
425 | */ | |
426 | Boolean SecTranslocateIsTranslocatedURL(CFURLRef path, bool* isTranslocated, CFErrorRef* __nullable error) | |
427 | { | |
428 | bool result = false; | |
429 | int errorCode = 0; | |
430 | ||
431 | if(path == NULL || isTranslocated == NULL) | |
432 | { | |
433 | if(error) | |
434 | { | |
435 | *error = SecTranslocateMakePosixError(EINVAL); | |
436 | } | |
437 | return result; | |
438 | } | |
439 | ||
440 | *isTranslocated = false; | |
441 | ||
442 | try | |
443 | { | |
444 | string cpp_path = cfString(path); | |
445 | /* "/" i.e. the root volume, cannot be translocated (or mounted on by other file system after boot) | |
446 | so don't bother to make system calls if "/" is what is being asked about. | |
447 | This is an optimization to help LaunchServices which expects to use SecTranslocateIsTranslocatedURL | |
448 | on every App Launch. | |
449 | */ | |
450 | if (cpp_path != "/") | |
451 | { | |
452 | /* to avoid AppSandbox violations, use a path based check here. | |
453 | We only look for nullfs file type anyway. */ | |
454 | struct statfs sfb; | |
455 | if (statfs(cpp_path.c_str(), &sfb) == 0) | |
456 | { | |
457 | *isTranslocated = (strcmp(sfb.f_fstypename, NULLFS_FSTYPE) == 0); | |
458 | result = true; | |
459 | } | |
460 | else | |
461 | { | |
462 | errorCode = errno; | |
463 | Syslog::error("SecTranslocate: can not access %s, error: %s", cpp_path.c_str(), strerror(errorCode)); | |
464 | } | |
465 | } | |
466 | else | |
467 | { | |
468 | result = true; | |
469 | } | |
470 | } | |
471 | catch (Security::UnixError err) | |
472 | { | |
473 | errorCode = err.unixError(); | |
474 | } | |
475 | catch(...) | |
476 | { | |
477 | Syslog::critical("SecTranslocate: uncaught exception during policy check"); | |
478 | errorCode = EACCES; | |
479 | } | |
480 | ||
481 | if (error && errorCode) | |
482 | { | |
483 | *error = SecTranslocateMakePosixError(errorCode); | |
484 | } | |
485 | ||
486 | return result; | |
487 | } | |
488 | ||
489 | /* Find the original path for translocation mounts belonging to the calling user. | |
490 | if the url isn't on a nullfs volume then returned a retained copy of the passed in url. | |
491 | if the url is on a nullfs volume but that volume doesn't belong to the user, or another | |
492 | error occurs then null is returned */ | |
493 | CFURLRef __nullable SecTranslocateCreateOriginalPathForURL(CFURLRef translocatedPath, CFErrorRef* __nullable error) | |
494 | { | |
495 | CFURLRef result = NULL; | |
496 | int errorCode = 0; | |
497 | ||
498 | if(translocatedPath == NULL) | |
499 | { | |
500 | errorCode = EINVAL; | |
501 | goto end; | |
502 | } | |
503 | try | |
504 | { | |
505 | string path = cfString(translocatedPath); | |
506 | Security::SecTranslocate::ExtendedAutoFileDesc fd(path); | |
507 | ||
508 | if(fd.isFileSystemType(NULLFS_FSTYPE)) | |
509 | { | |
510 | bool isDir = false; | |
511 | string out_path = Security::SecTranslocate::getOriginalPath(fd, &isDir); | |
512 | if(!out_path.empty()) | |
513 | { | |
514 | result = makeCFURL(out_path, isDir); | |
515 | } | |
516 | else | |
517 | { | |
518 | Syslog::error("SecTranslocateCreateOriginalPath: No original and no prior exception. Shouldn't be here"); | |
519 | UnixError::throwMe(EINVAL); | |
520 | } | |
521 | } | |
522 | else | |
523 | { | |
524 | result = translocatedPath; | |
525 | CFRetain(result); | |
526 | } | |
527 | } | |
528 | catch (Security::UnixError err) | |
529 | { | |
530 | errorCode = err.unixError(); | |
531 | } | |
532 | catch(...) | |
533 | { | |
534 | Syslog::critical("SecTranslocate: uncaught exception during policy check"); | |
535 | errorCode = EACCES; | |
536 | } | |
537 | end: | |
538 | if (error && errorCode) | |
539 | { | |
540 | *error = SecTranslocateMakePosixError(errorCode); | |
541 | } | |
542 | return result; | |
543 | } |