2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
25 #include <sys/param.h>
26 #include <sys/mount.h>
31 #include <security_utilities/cfutilities.h>
32 #include <security_utilities/unix++.h>
33 #include <security_utilities/logging.h>
35 #include "SecTranslocate.h"
36 #include "SecTranslocateShared.hpp"
37 #include "SecTranslocateInterface.hpp"
38 #include "SecTranslocateUtilities.hpp"
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.
47 **** App Translocation Strategy w/o a destination path ****
48 (This functionality is implemented in SecTranslocateShared.hpp/cpp)
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/
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.
61 If a mount exists already for this user, then the path to the app in that mountpoint is
62 calculated and sanity checked.
64 The rules to create the app path inside the mountpoint are:
66 original app path = /some/path/<app name>
67 new app path = realpath(confstr(_CS_DARWIN_USER_TEMP_DIR))+/AppTranslocation/<UUID>/d/<app name>
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.
74 If the sanity checks pass for the new app path, then that path is returned to the caller.
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.
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
89 The sanity check for the new app path is listed above (for the mountpoint exists case).
91 **** App Translocation strategy w/ a destination path ****
92 (This functionality is implemented in SecTranslocateShared.hpp/cpp)
94 If a destination path is provided, a sequence similar to that described above is followed
95 with the following modifications.
97 The destination path is expected to be of the same form as new app path. This expectation
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.
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.
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.
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.
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.
121 **** App Translocation error cleanup ****
122 (This functionality is implemented in SecTranslocateShared.hpp/cpp)
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.
129 **** Deleting an App Translocation point ****
130 (This functionality is implemented in SecTranslocateShared.hpp/cpp)
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.
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.
143 **** Quarantine considerations ****
144 (This functionality is implemented in SecTranslocateShared.hpp/cpp and SecTranslocateUtilities.hpp/cpp)
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.
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.
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.
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.
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.
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
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.
193 /* Make a CFError from an POSIX error code */
194 static CFErrorRef
SecTranslocateMakePosixError(CFIndex errorCode
)
196 return CFErrorCreate(NULL
, kCFErrorDomainPOSIX
, errorCode
, NULL
);
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
)
202 Boolean result
= false;
203 CFIndex errorCode
= 0;
206 /* ask getTranslocator for the server */
207 result
= Security::SecTranslocate::getTranslocator(true) != NULL
;
209 catch (Security::UnixError err
)
211 errorCode
= err
.unixError();
215 Syslog::critical("SecTranslocate: uncaught exception during server initialization");
219 if (error
&& errorCode
)
221 *error
= SecTranslocateMakePosixError(errorCode
);
227 /* placeholder api for now to allow for future options at startup */
228 Boolean
SecTranslocateStartListeningWithOptions(CFDictionaryRef __unused options
, CFErrorRef
* __nullable outError
)
230 return SecTranslocateStartListening(outError
);
233 /* Register that a (translocated) pid has launched */
234 void SecTranslocateAppLaunchCheckin(pid_t pid
)
238 Security::SecTranslocate::getTranslocator()->appLaunchCheckin(pid
);
242 Syslog::error("SecTranslocate: error in SecTranslocateAppLaunchCheckin");
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
)
251 CFURLRef result
= NULL
;
252 CFIndex errorCode
= 0;
256 string sourcePath
= cfString(pathToTranslocate
); // returns an absolute path
258 Security::SecTranslocate::TranslocationPath
toTranslocatePath(sourcePath
, Security::SecTranslocate::TranslocationOptions::Default
);
260 if(!toTranslocatePath
.shouldTranslocate())
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
;
267 /* We need to translocate so keep going */
272 destPath
= cfString(destinationPath
); //returns an absolute path
275 string out_path
= Security::SecTranslocate::getTranslocator()->translocatePathForUser(toTranslocatePath
, destPath
);
277 if(!out_path
.empty())
279 result
= makeCFURL(out_path
, true);
283 Syslog::error("SecTranslocateCreateSecureDirectoryForURL: No mountpoint and no prior exception. Shouldn't be here");
284 UnixError::throwMe(EINVAL
);
288 catch (Security::UnixError err
)
290 errorCode
= err
.unixError();
294 Syslog::critical("SecTranslocate: uncaught exception during mountpoint creation");
298 if (error
&& errorCode
)
300 *error
= SecTranslocateMakePosixError(errorCode
);
305 /* Destroy the specified translocated path, and clean up the user's translocation directory. */
306 Boolean
SecTranslocateDeleteSecureDirectory(CFURLRef translocatedPath
, CFErrorRef
* __nullable error
)
311 if(translocatedPath
== NULL
)
319 string pathToDestroy
= cfString(translocatedPath
);
320 result
= Security::SecTranslocate::getTranslocator()->destroyTranslocatedPathForUser(pathToDestroy
);
322 catch (Security::UnixError err
)
324 errorCode
= err
.unixError();
328 Syslog::critical("SecTranslocate: uncaught exception during mountpoint deletion");
332 if (error
&& errorCode
)
334 *error
= SecTranslocateMakePosixError(errorCode
);
340 CFURLRef __nullable
SecTranslocateCreateGeneric (CFURLRef pathToTranslocate
,
341 CFURLRef destinationPath
,
342 CFErrorRef
* __nullable error
)
344 CFURLRef result
= NULL
;
345 CFIndex errorCode
= 0;
349 string sourcePath
= cfString(pathToTranslocate
);
350 Security::SecTranslocate::GenericTranslocationPath path
{sourcePath
, Security::SecTranslocate::TranslocationOptions::Unveil
};
352 string dpath
= cfString(destinationPath
);
353 string out_path
= Security::SecTranslocate::getTranslocator()->translocatePathForUser(path
, dpath
);
355 if(!out_path
.empty())
357 result
= makeCFURL(out_path
, true);
361 Syslog::error("SecTranslocateCreateGeneric: No mountpoint and no prior exception. Shouldn't be here");
362 UnixError::throwMe(EINVAL
);
366 catch (Security::UnixError err
)
368 errorCode
= err
.unixError();
372 Syslog::critical("SecTranslocateCreateGeneric: uncaught exception during mountpoint creation");
376 if (error
&& errorCode
)
378 *error
= SecTranslocateMakePosixError(errorCode
);
383 /* Decide whether we need to translocate */
384 Boolean
SecTranslocateURLShouldRunTranslocated(CFURLRef path
, bool* shouldTranslocate
, CFErrorRef
* __nullable error
)
389 if(path
== NULL
|| shouldTranslocate
== NULL
)
397 string pathToCheck
= cfString(path
);
398 Security::SecTranslocate::TranslocationPath
tPath(pathToCheck
, Security::SecTranslocate::TranslocationOptions::Default
);
399 *shouldTranslocate
= tPath
.shouldTranslocate();
402 catch (Security::UnixError err
)
404 errorCode
= err
.unixError();
408 Syslog::critical("SecTranslocate: uncaught exception during policy check");
413 if (error
&& errorCode
)
415 *error
= SecTranslocateMakePosixError(errorCode
);
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).
426 Boolean
SecTranslocateIsTranslocatedURL(CFURLRef path
, bool* isTranslocated
, CFErrorRef
* __nullable error
)
431 if(path
== NULL
|| isTranslocated
== NULL
)
435 *error
= SecTranslocateMakePosixError(EINVAL
);
440 *isTranslocated
= false;
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
452 /* to avoid AppSandbox violations, use a path based check here.
453 We only look for nullfs file type anyway. */
455 if (statfs(cpp_path
.c_str(), &sfb
) == 0)
457 *isTranslocated
= (strcmp(sfb
.f_fstypename
, NULLFS_FSTYPE
) == 0);
463 Syslog::error("SecTranslocate: can not access %s, error: %s", cpp_path
.c_str(), strerror(errorCode
));
471 catch (Security::UnixError err
)
473 errorCode
= err
.unixError();
477 Syslog::critical("SecTranslocate: uncaught exception during policy check");
481 if (error
&& errorCode
)
483 *error
= SecTranslocateMakePosixError(errorCode
);
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
)
495 CFURLRef result
= NULL
;
498 if(translocatedPath
== NULL
)
505 string path
= cfString(translocatedPath
);
506 Security::SecTranslocate::ExtendedAutoFileDesc
fd(path
);
508 if(fd
.isFileSystemType(NULLFS_FSTYPE
))
511 string out_path
= Security::SecTranslocate::getOriginalPath(fd
, &isDir
);
512 if(!out_path
.empty())
514 result
= makeCFURL(out_path
, isDir
);
518 Syslog::error("SecTranslocateCreateOriginalPath: No original and no prior exception. Shouldn't be here");
519 UnixError::throwMe(EINVAL
);
524 result
= translocatedPath
;
528 catch (Security::UnixError err
)
530 errorCode
= err
.unixError();
534 Syslog::critical("SecTranslocate: uncaught exception during policy check");
538 if (error
&& errorCode
)
540 *error
= SecTranslocateMakePosixError(errorCode
);