X-Git-Url: https://git.saurik.com/apple/security.git/blobdiff_plain/ce3c8656732c924baf7e88df75eab50891bdc471..fa7225c82381bac4432a6edf16f53b5370238d85:/OSX/libsecurity_translocate/lib/SecTranslocate.cpp diff --git a/OSX/libsecurity_translocate/lib/SecTranslocate.cpp b/OSX/libsecurity_translocate/lib/SecTranslocate.cpp new file mode 100644 index 00000000..a86aa4c8 --- /dev/null +++ b/OSX/libsecurity_translocate/lib/SecTranslocate.cpp @@ -0,0 +1,500 @@ +/* + * Copyright (c) 2016 Apple Inc. All Rights Reserved. + * + * @APPLE_LICENSE_HEADER_START@ + * + * This file contains Original Code and/or Modifications of Original Code + * as defined in and that are subject to the Apple Public Source License + * Version 2.0 (the 'License'). You may not use this file except in + * compliance with the License. Please obtain a copy of the License at + * http://www.opensource.apple.com/apsl/ and read it before using this + * file. + * + * The Original Code and all software distributed under the License are + * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER + * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, + * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. + * Please see the License for the specific language governing rights and + * limitations under the License. + * + * @APPLE_LICENSE_HEADER_END@ + */ + +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include "SecTranslocate.h" +#include "SecTranslocateShared.hpp" +#include "SecTranslocateInterface.hpp" +#include "SecTranslocateUtilities.hpp" + + +/* Strategy: + + This library exists to create and destroy app translocation points. To ensure that a given + process using this library is only making or destroying one mountpoint at a time, the two + interface functions are sychronized on a dispatch queue. + + **** App Translocation Strategy w/o a destination path **** + (This functionality is implemented in SecTranslocateShared.hpp/cpp) + + To create a translocation mountpoint, if no destination path is provided, first the app + path to be translocated is realpathed to ensure it exists and there are no symlink's in + the path we work with. Then the calling user's _CS_DARWIN_USER_TEMP_DIR is found. This + is used to calculate the user's AppTranslocation directory. The formula is: + User's App Translocation Directory = realpath(confstr(_CS_DARWIN_USER_TEMP_DIR))+/AppTranslocation/ + + Then the mount table is checked to see whether or not a translocation point already exists + for this user for the app being requested. The rule for an already existing mount is that + there must exist a mountpoint in the user's app translocation directory that is mounted + from realpath of the requested app. + + If a mount exists already for this user, then the path to the app in that mountpoint is + calculated and sanity checked. + + The rules to create the app path inside the mountpoint are: + + original app path = /some/path/ + new app path = realpath(confstr(_CS_DARWIN_USER_TEMP_DIR))+/AppTranslocation//d/ + + The sanity check for the new app path is that: + 1. new app path exists + 2. new app path is in a nullfs mount + 3. new app path is already completely resolved. + + If the sanity checks pass for the new app path, then that path is returned to the caller. + + If no translocation mount point exists already per the mount table then an AppTranslocation + directory is created within the temp dir if it doesn't already exist. After that a UUID is + generated, that UUID is used as the name of a new directory in the AppTranslocation directory. + Once the new directory has been created and sanity checked, mount is called to create the + translocation between the original path and the new directory. Then the new path to the app + within the mountpoint is calculated and sanity checked. + + The sanity check rules for the mountpoint before the mount are: + 1. Something exists at the expected path + 2. That something is a directory + 3. That something is not already a mountpoint + 4. The expected path is fully resolved + + The sanity check for the new app path is listed above (for the mountpoint exists case). + + **** App Translocation strategy w/ a destination path **** + (This functionality is implemented in SecTranslocateShared.hpp/cpp) + + If a destination path is provided, a sequence similar to that described above is followed + with the following modifications. + + The destination path is expected to be of the same form as new app path. This expectation + is verified. + + First we verify that the destination path ends with /d/ and that the + component of the destination path matches the of the original app app path + requested. If not, an error occurs. Everything before the /d/ is treated becomes the + requested destination mount point. + + After the user's app translocation directory is calculated, we ensure that the requested + destination mount point is prefixed by the translocation directory, and contains only one + path component after the user's app translocation path, otherwise an error occurs. + + When we check the mount table, we make sure that if the a translocation of the app already + exists for the user, then the translocation path must exactly match the requested + destination path, otherwise an error occurs. + + If no mountpoint exists for the app, then we attempt to create the requested directory within + the user's app translocation directory. This becomes the mount point, and the mount point + sanity checks listed above are applied. + + If the requested destination mountpoint is successfully created, the flow continues as above + to create the mount and verify the requested path within the mountpoint. The only extra step + here is that at the end, the requested app path, must exactly equal the created app path. + + **** App Translocation error cleanup **** + (This functionality is implemented in SecTranslocateShared.hpp/cpp) + + The error cleanup strategy for translocation creation is to try to destroy any directories + or mount points in the user's translocation directory that were created before an error + was detected. This means tracking whether we created a directory, or it already existed when + a caller asked for it. Clean up is considered best effort. + + **** Deleting an App Translocation point **** + (This functionality is implemented in SecTranslocateShared.hpp/cpp) + + To destroy an app translocation point, the first thing we do is calculate the user's app + translocation directory to ensure that the requested path is actually within that directory. + We also verify that it is in fact a nullfs mount point. If it is, then we attempt to unmount and + remove the translocation point. + + Regardless of whether or not the requested path is a translocation point, we opportunistically + attempt to cleanup the app translocation directory. Clean up means, looping through all the + directories currently in the user's app translocation directory and checking whether or not + they are a mount point. If a directory inside the user's app translocation directory is not + a mountpoint, then we attempt to delete it. + + **** Quarantine considerations **** + (This functionality is implemented in SecTranslocateShared.hpp/cpp and SecTranslocateUtilities.hpp/cpp) + + If the original app path includes files with quarantine extended attributes, then those extended + attributes will be readable through the created app translocation mountpoint. nullfs does not + support removing or setting extended attributes on its vnodes. Changes to the quarantine + attributes at the original path will be reflected in the app translocation mountpoint without + creating a new mount point. + + If the original app path is inside a quarantined mountpoint (such as a quarantined dmg), then + that the quarantine information for that mountpoint is read from the original app path's + mountpoint and applied to the created app translocation mountpoint. + + **** Concurrency considerations **** + This library treats the kernel as the source of truth for the status of the file system. + Unfortunately it has no way to lock the state of the file system and mount table while + it is operating. Because of this, there are two potential areas that have race windows. + + First, if any other system entity (thread within the same process, or other process + within the system) is adding or removing entries from the mount table while + SecTranslocateCreateSecureDirectoryForURL is executing, then there is the possibility that + an incorrect decision will be made about the current existence of a mount point for a user + for an app. This is because getfsstat gets a snapshot of the mount table state rather than a + locked window into the kernel and because we make two seperate calls to getfsstat, one to get + the number of mountpoints, and a second to actually read the mountpoint data. If more than + one process is using this library for the same user, then both processes could attempt to + create a translocation for the same app, and this could result in more than one translocation + for that app for the user. This shouldn't effect the user other than using additional + system resources. We attempt to mitigate this by allocating double the required memory from + the first call and then trying the process again (once) if the initial memory was filled. + + Second, if more than one process is using this library simultaneously and one process calls + SecTranslocateDeleteSecureDirectory for a user and the other calls + SecTranslocateCreateSecureDirectoryForURL for that same user, then the call to + SecTranslocateDeleteSecureDirectory may cause SecTranslocateCreateSecureDirectoryForURL to + fail. This will occur if the loop checking for unmounted directories in the user's app + translocation directory deletes a newly created directory before the mount call finishes. This + race condition will probably result in a failed app launch. A second attempt to launch the app + will probably succeed. + + Concurrency is now split between SecTranslocateClient.hpp/cpp, SecTranslocateServer.hpp/cpp, + SecTranslocateDANotification.hpp/cpp, SecTranslocateLSNotification.hpp/cpp, and + SecTranslocateXPCServer.hpp/cpp. Each of these represent different ways of entering translocation + functionality. + + **** Logging Strategy **** + Use warning logging for interesting conditions (e.g. translocation point created or destroyed). + Use error logging for non-fatal failures. Use critical logging for fatal failures. + */ + +/* Make a CFError from an POSIX error code */ +static CFErrorRef SecTranslocateMakePosixError(CFIndex errorCode) +{ + return CFErrorCreate(NULL, kCFErrorDomainPOSIX, errorCode, NULL); +} + +/* must be called before any other function in this SPI if the process is intended to be the server */ +Boolean SecTranslocateStartListening(CFErrorRef* __nullable error) +{ + Boolean result = false; + CFIndex errorCode = 0; + try + { + /* ask getTranslocator for the server */ + result = Security::SecTranslocate::getTranslocator(true) != NULL; + } + catch (Security::UnixError err) + { + errorCode = err.unixError(); + } + catch(...) + { + Syslog::critical("SecTranslocate: uncaught exception during server initialization"); + errorCode = EINVAL; + } + + if (error && errorCode) + { + *error = SecTranslocateMakePosixError(errorCode); + } + + return result; +} + +/* placeholder api for now to allow for future options at startup */ +Boolean SecTranslocateStartListeningWithOptions(CFDictionaryRef __unused options, CFErrorRef * __nullable outError) +{ + return SecTranslocateStartListening(outError); +} + +/* Register that a (translocated) pid has launched */ +void SecTranslocateAppLaunchCheckin(pid_t pid) +{ + try + { + Security::SecTranslocate::getTranslocator()->appLaunchCheckin(pid); + } + catch (...) + { + Syslog::error("SecTranslocate: error in SecTranslocateAppLaunchCheckin"); + } +} + +/* Create an app translocation point given the original path and an optional destination path. */ +CFURLRef __nullable SecTranslocateCreateSecureDirectoryForURL (CFURLRef pathToTranslocate, + CFURLRef __nullable destinationPath, + CFErrorRef* __nullable error) +{ + CFURLRef result = NULL; + CFIndex errorCode = 0; + + try + { + string sourcePath = cfString(pathToTranslocate); // returns an absolute path + + Security::SecTranslocate::TranslocationPath toTranslocatePath(sourcePath); + + if(!toTranslocatePath.shouldTranslocate()) + { + /* We shouldn't translocate so, just retain so that the return value can be treated as a copy */ + CFRetain(pathToTranslocate); + return pathToTranslocate; + } + + /* We need to translocate so keep going */ + string destPath; + + if(destinationPath) + { + destPath = cfString(destinationPath); //returns an absolute path + } + + string out_path = Security::SecTranslocate::getTranslocator()->translocatePathForUser(toTranslocatePath, destPath); + + if(!out_path.empty()) + { + result = makeCFURL(out_path, true); + } + else + { + Syslog::error("SecTranslocateCreateSecureDirectoryForURL: No mountpoint and no prior exception. Shouldn't be here"); + UnixError::throwMe(EINVAL); + } + + } + catch (Security::UnixError err) + { + errorCode = err.unixError(); + } + catch(...) + { + Syslog::critical("SecTranslocate: uncaught exception during mountpoint creation"); + errorCode = EACCES; + } + + if (error && errorCode) + { + *error = SecTranslocateMakePosixError(errorCode); + } + return result; +} + +/* Destroy the specified translocated path, and clean up the user's translocation directory. */ +Boolean SecTranslocateDeleteSecureDirectory(CFURLRef translocatedPath, CFErrorRef* __nullable error) +{ + bool result = false; + int errorCode = 0; + + if(translocatedPath == NULL) + { + errorCode = EINVAL; + goto end; + } + + try + { + string pathToDestroy = cfString(translocatedPath); + result = Security::SecTranslocate::getTranslocator()->destroyTranslocatedPathForUser(pathToDestroy); + } + catch (Security::UnixError err) + { + errorCode = err.unixError(); + } + catch(...) + { + Syslog::critical("SecTranslocate: uncaught exception during mountpoint deletion"); + errorCode = EACCES; + } +end: + if (error && errorCode) + { + *error = SecTranslocateMakePosixError(errorCode); + } + + return result; +} + +/* Decide whether we need to translocate */ +Boolean SecTranslocateURLShouldRunTranslocated(CFURLRef path, bool* shouldTranslocate, CFErrorRef* __nullable error) +{ + bool result = false; + int errorCode = 0; + + if(path == NULL || shouldTranslocate == NULL) + { + errorCode = EINVAL; + goto end; + } + + try + { + string pathToCheck = cfString(path); + Security::SecTranslocate::TranslocationPath tPath(pathToCheck); + *shouldTranslocate = tPath.shouldTranslocate(); + result = true; + } + catch (Security::UnixError err) + { + errorCode = err.unixError(); + } + catch(...) + { + Syslog::critical("SecTranslocate: uncaught exception during policy check"); + errorCode = EACCES; + } + +end: + if (error && errorCode) + { + *error = SecTranslocateMakePosixError(errorCode); + } + + return result; +} + +/* Answer whether or not the passed in URL is a nullfs URL. This just checks nullfs rather than + nullfs + in the user's translocation path to allow callers like LaunchServices to apply special + handling to nullfs mounts regardless of the calling user (i.e. root lsd can identify all translocated + mount points for all users). + */ +Boolean SecTranslocateIsTranslocatedURL(CFURLRef path, bool* isTranslocated, CFErrorRef* __nullable error) +{ + bool result = false; + int errorCode = 0; + + if(path == NULL || isTranslocated == NULL) + { + if(error) + { + *error = SecTranslocateMakePosixError(EINVAL); + } + return result; + } + + *isTranslocated = false; + + try + { + string cpp_path = cfString(path); + /* "/" i.e. the root volume, cannot be translocated (or mounted on by other file system after boot) + so don't bother to make system calls if "/" is what is being asked about. + This is an optimization to help LaunchServices which expects to use SecTranslocateIsTranslocatedURL + on every App Launch. + */ + if (cpp_path != "/") + { + /* to avoid AppSandbox violations, use a path based check here. + We only look for nullfs file type anyway. */ + struct statfs sfb; + if (statfs(cpp_path.c_str(), &sfb) == 0) + { + *isTranslocated = (strcmp(sfb.f_fstypename, NULLFS_FSTYPE) == 0); + result = true; + } + else + { + errorCode = errno; + Syslog::error("SecTranslocate: can not access %s, error: %s", cpp_path.c_str(), strerror(errorCode)); + } + } + else + { + result = true; + } + } + catch (Security::UnixError err) + { + errorCode = err.unixError(); + } + catch(...) + { + Syslog::critical("SecTranslocate: uncaught exception during policy check"); + errorCode = EACCES; + } + + if (error && errorCode) + { + *error = SecTranslocateMakePosixError(errorCode); + } + + return result; +} + +/* Find the original path for translocation mounts belonging to the calling user. + if the url isn't on a nullfs volume then returned a retained copy of the passed in url. + if the url is on a nullfs volume but that volume doesn't belong to the user, or another + error occurs then null is returned */ +CFURLRef __nullable SecTranslocateCreateOriginalPathForURL(CFURLRef translocatedPath, CFErrorRef* __nullable error) +{ + CFURLRef result = NULL; + int errorCode = 0; + + if(translocatedPath == NULL) + { + errorCode = EINVAL; + goto end; + } + try + { + string path = cfString(translocatedPath); + Security::SecTranslocate::ExtendedAutoFileDesc fd(path); + + if(fd.isFileSystemType(NULLFS_FSTYPE)) + { + bool isDir = false; + string out_path = Security::SecTranslocate::getOriginalPath(fd, &isDir); + if(!out_path.empty()) + { + result = makeCFURL(out_path, isDir); + } + else + { + Syslog::error("SecTranslocateCreateOriginalPath: No original and no prior exception. Shouldn't be here"); + UnixError::throwMe(EINVAL); + } + } + else + { + result = translocatedPath; + CFRetain(result); + } + } + catch (Security::UnixError err) + { + errorCode = err.unixError(); + } + catch(...) + { + Syslog::critical("SecTranslocate: uncaught exception during policy check"); + errorCode = EACCES; + } +end: + if (error && errorCode) + { + *error = SecTranslocateMakePosixError(errorCode); + } + return result; +}