X-Git-Url: https://git.saurik.com/apple/security.git/blobdiff_plain/ce3c8656732c924baf7e88df75eab50891bdc471..fa7225c82381bac4432a6edf16f53b5370238d85:/OSX/libsecurity_translocate/lib/SecTranslocateShared.cpp?ds=inline diff --git a/OSX/libsecurity_translocate/lib/SecTranslocateShared.cpp b/OSX/libsecurity_translocate/lib/SecTranslocateShared.cpp new file mode 100644 index 00000000..f03c070e --- /dev/null +++ b/OSX/libsecurity_translocate/lib/SecTranslocateShared.cpp @@ -0,0 +1,1023 @@ +/* + * 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 +#include +#include + +#define __APPLE_API_PRIVATE +#include +#undef __APPLE_API_PRIVATE + +#include +#include +#include +#include + +#include "SecTranslocateShared.hpp" +#include "SecTranslocateUtilities.hpp" + + +namespace Security { + +namespace SecTranslocate { + +using namespace std; + +/* String Constants for XPC dictionary passing */ +/* XPC Function keys */ +const char* kSecTranslocateXPCFuncCreate = "create"; +const char* kSecTranslocateXPCFuncCheckIn = "check-in"; + +/* XPC message argument keys */ +const char* kSecTranslocateXPCMessageFunction = "function"; +const char* kSecTranslocateXPCMessageOriginalPath = "original"; +const char* kSecTranslocateXPCMessageDestinationPath = "dest"; +const char* kSecTranslocateXPCMessagePid = "pid"; + +/*XPC message reply keys */ +const char* kSecTranslocateXPCReplyError = "error"; +const char* kSecTranslocateXPCReplySecurePath = "result"; + +//Functions used only within this file +static void setMountPointQuarantineIfNecessary(const string &mountPoint, const string &originalPath); +static string getMountpointFromAppPath(const string &appPath, const string &originalPath); + +static vector getMountTableSnapshot(); +static string mountExistsForUser(const string &translationDirForUser, const string &originalPath, const string &destMount); +static void validateMountpoint(const string &mountpoint, bool owned=false); +static string makeNewMountpoint(const string &translationDir); +static string newAppPath (const string &mountPoint, const TranslocationPath &originalPath); +static void cleanupTranslocationDirForUser(const string &userDir); +static int removeMountPoint(const string &mountpoint, bool force = false); + +/* calculate whether a translocation should occur and where from */ +TranslocationPath::TranslocationPath(string originalPath) +{ + + /* To support testing of translocation the policy is as follows: + 1. When the quarantine translocation sysctl is off, always translocate + if we aren't already on a translocated mount point. + 2. When the quarantine translocation sysctl is on, use the quarantine + bits to decide. + when asking if a path should run translocated need to: + check the current quarantine state of the path asked about + if it is already on a nullfs mount + do not translocate + else if it is unquarantined + do not translocate + else + if not QTN_FLAG_TRANSLOCATE or QTN_FLAG_DO_NOT_TRANSLOCATE + do not translocate + else + find the outermost acceptable code bundle + if not QTN_FLAG_TRANSLOCATE or QTN_FLAG_DO_NOT_TRANSLOCATE + don't translocate + else + translocate + + See findOuterMostCodeBundleForFD for more info about what an acceptable outermost bundle is + in particular it should be noted that the outermost acceptable bundle for a quarantined inner + bundle can not be unquarantined. If the inner bundle is quarantined then any bundle containing it + must also have been quarantined. + */ + + ExtendedAutoFileDesc fd(originalPath); + + should = false; + realOriginalPath = fd.getRealPath(); + + /* don't translocate if it already is */ + /* only consider translocation if the thing being asked about is marked for translocation */ + if(!fd.isFileSystemType(NULLFS_FSTYPE) && fd.isQuarantined() && fd.shouldTranslocate()) + { + ExtendedAutoFileDesc &&outermost = findOuterMostCodeBundleForFD(fd); + + should = outermost.isQuarantined() && outermost.shouldTranslocate(); + pathToTranslocate = outermost.getRealPath(); + + /* Calculate the path that will be needed to give the caller the path they asked for originally but in the translocated place */ + if (should) + { + vector originalComponents = splitPath(realOriginalPath); + vector toTranslocateComponents = splitPath(pathToTranslocate); + + if (toTranslocateComponents.size() == 0 || + toTranslocateComponents.size() > originalComponents.size()) + { + Syslog::error("SecTranslocate, TranslocationPath, path calculation failed:\n\toriginal: %s\n\tcalculated: %s", + realOriginalPath.c_str(), + pathToTranslocate.c_str()); + UnixError::throwMe(EINVAL); + } + + for(size_t cnt = 0; cnt < originalComponents.size(); cnt++) + { + if (cnt < toTranslocateComponents.size()) + { + if (toTranslocateComponents[cnt] != originalComponents[cnt]) + { + Syslog::error("SecTranslocate, TranslocationPath, translocation path calculation failed:\n\toriginal: %s\n\tcalculated: %s", + realOriginalPath.c_str(), + pathToTranslocate.c_str()); + UnixError::throwMe(EINVAL); + } + } + else + { + /* + want pathInsideTranslocationPoint to look like: + a/b/c + i.e. internal / but not at the front or back. + */ + if(pathInsideTranslocationPoint.empty()) + { + pathInsideTranslocationPoint = originalComponents[cnt]; + } + else + { + pathInsideTranslocationPoint += "/" + originalComponents[cnt]; + } + } + } + } + } +} + +/* if we should translocate and a stored path inside the translocation point exists, then add it to the + passed in string. If no path inside is stored, then return the passed in string if translocation + should occur, and the original path for the TranslocationPath if translocation shouldn't occur */ +string TranslocationPath::getTranslocatedPathToOriginalPath(const string &translocationPoint) const +{ + string seperator = translocationPoint.back() != '/' ? "/" : ""; + + if (should) + { + if(!pathInsideTranslocationPoint.empty()) + { + return translocationPoint + seperator + pathInsideTranslocationPoint; + } + else + { + return translocationPoint; + } + } + else + { + //If we weren't supposed to translocate return the original path. + return realOriginalPath; + } +} + +/* Given an fd for a path find the outermost acceptable code bundle and return an fd for that. + an acceptable outermost bundle is quarantined, user approved, and a code bundle. + If nothing is found outside the path to the fd provided, then passed in fd or a copy there of is returned.*/ +ExtendedAutoFileDesc TranslocationPath::findOuterMostCodeBundleForFD(ExtendedAutoFileDesc &fd) +{ + if( fd.isMountPoint() || !fd.isQuarantined()) + { + return fd; + } + vector path = splitPath(fd.getRealPath()); + size_t currentIndex = path.size() - 1; + size_t lastGoodIndex = currentIndex; + + string pathToCheck = joinPathUpTo(path, currentIndex); + /* + Proposed algorithm (pseudo-code): + lastGood := path := canonicalized path to be launched + + while path is not a mount point + if path is quarantined and not user-approved then exit loop # Gatekeeper has not cleared this code + if SecStaticCodeCreateWithPath(path) succeeds # used as an “is a code bundle” oracle + then lastGood := path + path := parent directory of path + return lastGood + */ + while(currentIndex) + { + ExtendedAutoFileDesc currFd(pathToCheck); + + if (currFd.isMountPoint() || !currFd.isQuarantined() || !currFd.isUserApproved()) + { + break; + } + + SecStaticCodeRef staticCodeRef = NULL; + + if( SecStaticCodeCreateWithPath(CFTempURL(currFd.getRealPath()), kSecCSDefaultFlags, &staticCodeRef) == errSecSuccess) + { + lastGoodIndex = currentIndex; + CFRelease(staticCodeRef); + } + + currentIndex--; + pathToCheck = joinPathUpTo(path, currentIndex); + } + + return ExtendedAutoFileDesc(joinPathUpTo(path, lastGoodIndex)); +} + +/* Given an fd to a translocated file, build the path to the original file + Throws if the fd isn't in a nullfs mount for the calling user. */ +string getOriginalPath(const ExtendedAutoFileDesc& fd, bool* isDir) +{ + if (!fd.isFileSystemType(NULLFS_FSTYPE) || + isDir == NULL || + !fd.isInPrefixDir(fd.getMountPoint())) + { + Syslog::error("SecTranslocate::getOriginalPath called with invalid params: fs_type = %s, isDir = %p, realPath = %s, mountpoint = %s", + fd.getFsType().c_str(), + isDir, + fd.getRealPath().c_str(), + fd.getMountPoint().c_str()); + UnixError::throwMe(EINVAL); + } + + string translocationBaseDir = translocationDirForUser(); + + if(!fd.isInPrefixDir(translocationBaseDir)) + { + Syslog::error("SecTranslocate::getOriginal path called with path (%s) that doesn't belong to user (%d)", + fd.getRealPath().c_str(), + getuid()); + UnixError::throwMe(EPERM); + } + + *isDir = fd.isA(S_IFDIR); + + vector mountFromPath = splitPath(fd.getMountFromPath()); + vector mountPointPath = splitPath(fd.getMountPoint()); + vector translocatedRealPath = splitPath(fd.getRealPath()); + + if (mountPointPath.size() > translocatedRealPath.size()) + { + Syslog::warning("SecTranslocate: invalid translocated path %s", fd.getRealPath().c_str()); + UnixError::throwMe(EINVAL); + } + + string originalPath = fd.getMountFromPath(); + + int i; + + for( i = 0; i app = splitPath(appPath); // throws if empty or not absolute + vector original = splitPath(originalPath); //throws if empty or not absolute + + if (original.size() == 0) // had to have at least one directory, can't null mount / + { + Syslog::error("SecTranslocate: invalid original path: %s", originalPath.c_str()); + UnixError::throwMe(EINVAL); + } + + if (app.size() >= 3 && //the app path must have at least 3 directories, can't null mount onto / + app.back() == original.back()) //last directory of both match + { + app.pop_back(); + if(app.back() == "d") //last directory of app path is preceded by /d/ + { + app.pop_back(); + result = joinPath(app); + goto end; + } + } + + Syslog::error("SecTranslocate: invalid app path: %s", appPath.c_str()); + UnixError::throwMe(EINVAL); + +end: + return result; +} + +/* Read the mount table and return it in a vector */ +static vector getMountTableSnapshot() +{ + vector mntInfo; + int fs_cnt_first = 0; + int fs_cnt_second = 0; + int retry = 2; + + /*Strategy here is: + 1. check the current mount table size + 2. allocate double the required space + 3. actually read the mount table + 4. if the read actually filled up that double size try again once otherwise we are done + */ + + while(retry) + { + fs_cnt_first = getfsstat(NULL, 0 , MNT_WAIT); + if(fs_cnt_first <= 0) + { + Syslog::warning("SecTranslocate: error(%d) getting mount table info.", errno); + UnixError::throwMe(); + } + + if( fs_cnt_first == fs_cnt_second) + { + /* this path only applies on a retry. If our second attempt to get the size is + the same as what we already read then break. */ + break; + } + + mntInfo.resize(fs_cnt_first*2); + + fs_cnt_second = getfsstat(mntInfo.data(), (int)(mntInfo.size() * sizeof(struct statfs)), MNT_WAIT); + if (fs_cnt_second <= 0) + { + Syslog::warning("SecTranslocate: error(%d) getting mount table info.", errno); + UnixError::throwMe(); + } + + if( fs_cnt_second == mntInfo.size()) + { + retry--; + } + else + { + mntInfo.resize(fs_cnt_second); // trim the vector to what we actually need + break; + } + } + + if( retry == 0) + { + Syslog::warning("SecTranslocate: mount table is growing very quickly"); + } + + return mntInfo; +} + +/* Given the directory where app translocations go for this user, the path to the app to be translocated + and an optional destination mountpoint path. Check the mount table to see if a mount point already + user, for this app. If a destMountPoint is provided, make sure it is for this user, and that + exists for this the mountpoint found in the mount table is the same as the one requested */ +static string mountExistsForUser(const string &translationDirForUser, const string &originalPath, const string &destMountPoint) +{ + string result; // start empty + + if(!destMountPoint.empty()) + { + /* Validate that destMountPoint path is well formed and for this user + well formed means it is === translationDirForUser/<1 directory> + */ + vector splitDestMount = splitPath(destMountPoint); + + if(splitDestMount.size() < 2) //translationDirForUser is never / + { + Syslog::warning("SecTranslocate: invalid destination mount point: %s", + destMountPoint.c_str()); + UnixError::throwMe(EINVAL); + } + + splitDestMount.pop_back(); // knock off one directory + + string destBaseDir = joinPath(splitDestMount)+"/"; //translationDirForUser has a / at the end + + if (translationDirForUser != destBaseDir) + { + Syslog::warning("SecTranslocate: invalid destination mount point for user\n\tExpected: %s\n\tRequested: %s", + translationDirForUser.c_str(), + destBaseDir.c_str()); + /* requested destination isn't valid for the user */ + UnixError::throwMe(EINVAL); + } + } + + vector mntbuf = getMountTableSnapshot(); + + for (auto &i : mntbuf) + { + string mountOnName = i.f_mntonname; + size_t lastNonSlashPos = mountOnName.length() - 1; //start at the end of the string + + /* find the last position of the last non slash character */ + for(; lastNonSlashPos != 0 && mountOnName[lastNonSlashPos] == '/' ; lastNonSlashPos--); + + /* we want an exact match for originalPath and a prefix match for translationDirForUser + also make sure that this is a nullfs mount and that the mount point name is longer than the + translation directory with something other than / */ + + if (i.f_mntfromname == originalPath && //mount is for the requested path + strcmp(i.f_fstypename, NULLFS_FSTYPE) == 0 && // mount is a nullfs mount + lastNonSlashPos > translationDirForUser.length()-1 && // no shenanigans, there must be more directory here than just the translation dir + strncmp(i.f_mntonname, translationDirForUser.c_str(), translationDirForUser.length()) == 0) //mount is inside the translocation dir + { + if(!destMountPoint.empty()) + { + if (mountOnName != destMountPoint) + { + /* a mount exists for this path, but its not the one requested */ + Syslog::warning("SecTranslocate: requested destination doesn't match existing\n\tExpected: %s\n\tRequested: %s", + i.f_mntonname, + destMountPoint.c_str()); + UnixError::throwMe(EEXIST); + } + } + result = mountOnName; + break; + } + } + + return result; +} + +/* Given what we think is a valid mountpoint, perform a sanity check, and clean up if we are wrong */ +static void validateMountpoint(const string &mountpoint, bool owned) +{ + /* Requirements: + 1. can be opened + 2. is a directory + 3. is not already a mountpoint + 4. is an absolute path + */ + bool isDir = false; + bool isMount = false; + bool isEmpty = true; + + try { + /* first make sure this is a directory and that it is empty + (it could be dangerous to mount over a directory that contains something, + unfortunately this is still racy, and mount() is path based so we can't lock + down the directory until the mount succeeds (lock down is because of the entitlement + checks in nullfs))*/ + DIR* dir = opendir(mountpoint.c_str()); + int error = 0; + + if (dir == NULL) + { + error = errno; + Syslog::warning("SecTranslocate: mountpoint is not a directory or doesn't exist: %s", + mountpoint.c_str()); + UnixError::throwMe(error); + } + + isDir = true; + + struct dirent *d; + struct dirent dirbuf; + int cnt = 0; + int err = 0; + while(((err = readdir_r(dir, &dirbuf, &d)) == 0) && + d != NULL) + { + /* skip . and .. but break if there is more than that */ + if(++cnt > 2) + { + isEmpty = false; + break; + } + } + + error = errno; + (void)closedir(dir); + + if(err) + { + Syslog::warning("SecTranslocate: error while checking that mountpoint is empty"); + UnixError::throwMe(error); + } + + if(!isEmpty) + { + Syslog::warning("Sectranslocate: mountpoint is not empty: %s", + mountpoint.c_str()); + UnixError::throwMe(EBUSY); + } + + /* now check that the path is not a mountpoint */ + ExtendedAutoFileDesc fd(mountpoint); + + if(!fd.pathIsAbsolute()) + { + Syslog::warning("SecTranslocate: mountpoint isn't fully resolved\n\tExpected: %s\n\tActual: %s", + fd.getRealPath().c_str(), + mountpoint.c_str()); + UnixError::throwMe(EINVAL); + } + + isMount = fd.isMountPoint(); + + if(isMount) + { + Syslog::warning("SecTranslocate:Translocation failed, new mountpoint is already a mountpoint (%s)", + mountpoint.c_str()); + UnixError::throwMe(EINVAL); + } + } + catch(...) + { + if(owned) + { + if (!isMount) + { + if (isDir) + { + if(isEmpty) + { + rmdir(mountpoint.c_str()); + } + /* Already logged the else case above */ + } + else + { + Syslog::warning("SecTranslocate: unexpected file detected at mountpoint location (%s). Deleting.", + mountpoint.c_str()); + unlink(mountpoint.c_str()); + } + } + } + rethrow_exception(current_exception()); + } +} + +/* Create and validate the directory that we should mount at but don't create the mount yet */ +static string makeNewMountpoint(const string &translationDir) +{ + AutoFileDesc fd(getFDForDirectory(translationDir)); + + string uuid = makeUUID(); + + UnixError::check(mkdirat(fd, uuid.c_str(), 0500)); + + string mountpoint = translationDir+uuid; + + validateMountpoint(mountpoint); + + return mountpoint; +} + +/* If the original path has mountpoint quarantine info, apply it to the new mountpoint*/ +static void setMountPointQuarantineIfNecessary(const string &mountPoint, const string &originalPath) +{ + struct statfs sfsbuf; + int error = 0; + + UnixError::check(statfs(originalPath.c_str(), &sfsbuf)); + qtn_file_t original_attr = qtn_file_alloc(); + + if (original_attr != NULL) + { + if (qtn_file_init_with_mount_point(original_attr, sfsbuf.f_mntonname) == 0) + { + error = qtn_file_apply_to_mount_point(original_attr, mountPoint.c_str()); + } + qtn_file_free(original_attr); + } + else + { + error = errno; + } + + if (error) + { + Syslog::warning("SecTranslocate: Failed to apply quarantine information\n\tMountpoint: %s\n\tOriginal Path: %s", + mountPoint.c_str(), + originalPath.c_str()); + UnixError::throwMe(error); + } +} + +/* Given the path to a new mountpoint and the original path to translocate, calculate the path + to the desired app in the new mountpoint, and sanity check that calculation */ +static string newAppPath (const string &mountPoint, const TranslocationPath &originalPath) +{ + vector original = splitPath(originalPath.getPathToTranslocate()); + + if (original.size() == 0) + { + Syslog::error("SecTranslocate: Invalid originalPath: %s", originalPath.getPathToTranslocate().c_str()); + UnixError::throwMe(EINVAL); + } + + string midPath = mountPoint+"/d"; + string outPath = originalPath.getTranslocatedPathToOriginalPath(midPath+"/"+original.back()); + + /* ExtendedAutoFileDesc will throw if one of these doesn't exist or isn't accessible */ + ExtendedAutoFileDesc mountFd(mountPoint); + ExtendedAutoFileDesc midFd(midPath); + ExtendedAutoFileDesc outFd(outPath); + + if(!outFd.isFileSystemType(NULLFS_FSTYPE) || + !mountFd.isFileSystemType(NULLFS_FSTYPE) || + !midFd.isFileSystemType(NULLFS_FSTYPE)) + { + Syslog::warning("SecTranslocate::App exists at expected translocation path (%s) but isn't a nullfs mount (%s)", + outPath.c_str(), + outFd.getFsType().c_str()); + UnixError::throwMe(EINVAL); + } + + if(!outFd.pathIsAbsolute() || + !mountFd.pathIsAbsolute() || + !midFd.pathIsAbsolute() ) + { + Syslog::warning("SecTranslocate::App path isn't resolved\n\tGot: %s\n\tExpected: %s", + outFd.getRealPath().c_str(), + outPath.c_str()); + UnixError::throwMe(EINVAL); + } + + fsid_t outFsid = outFd.getFsid(); + fsid_t midFsid = midFd.getFsid(); + fsid_t mountFsid = mountFd.getFsid(); + + /* different fsids mean that there is more than one volume between the expected mountpoint and the expected app path */ + if (memcmp(&outFsid, &midFsid, sizeof(fsid_t)) != 0 || + memcmp(&outFsid, &mountFsid, sizeof(fsid_t)) != 0) + { + Syslog::warning("SecTranslocate:: the fsid is not consistent between app, /d/ and mountpoint"); + UnixError::throwMe(EINVAL); + } + + return outFd.getRealPath(); +} + +/* Create an app translocation point given the original path and an optional destination path. + note the destination path can only be an outermost path (where the translocation would happen) and not a path to nested code + synchronize the process on the dispatch queue. */ +string translocatePathForUser(const TranslocationPath &originalPath, const string &destPath) +{ + string newPath; + exception_ptr exception(0); + + string mountpoint; + bool owned = false; + try + { + const string &toTranslocate = originalPath.getPathToTranslocate(); + string baseDirForUser = translocationDirForUser(); //throws + string destMountPoint; + if(!destPath.empty()) + { + destMountPoint = getMountpointFromAppPath(destPath, toTranslocate); //throws or returns a mountpoint + } + + mountpoint = mountExistsForUser(baseDirForUser, toTranslocate, destMountPoint); //throws, detects invalid destMountPoint string + + if (!mountpoint.empty()) + { + /* A mount point exists already so bail*/ + newPath = newAppPath(mountpoint, originalPath); + return newPath; /* exit the block */ + } + if (destMountPoint.empty()) + { + mountpoint = makeNewMountpoint(baseDirForUser); //throws + owned = true; + } + else + { + AutoFileDesc fd(getFDForDirectory(destMountPoint, &owned)); //throws, makes the directory if it doesn't exist + + validateMountpoint(destMountPoint, owned); //throws + mountpoint = destMountPoint; + } + + UnixError::check(mount(NULLFS_FSTYPE, mountpoint.c_str(), MNT_RDONLY, (void*)toTranslocate.c_str())); + + setMountPointQuarantineIfNecessary(mountpoint, toTranslocate); //throws + + newPath = newAppPath(mountpoint, originalPath); //throws + + if (!destPath.empty()) + { + if (newPath != originalPath.getTranslocatedPathToOriginalPath(destPath)) + { + Syslog::warning("SecTranslocate: created app translocation point did not equal requested app translocation point\n\texpected: %s\n\tcreated: %s", + newPath.c_str(), + destPath.c_str()); + /* the app at originalPath didn't match the one at destPath */ + UnixError::throwMe(EINVAL); + } + } + // log that we created a new mountpoint (we don't log when we are re-using) + Syslog::warning("SecTranslocateCreateSecureDirectoryForURL: created %s", + newPath.c_str()); + } + catch (...) + { + exception = current_exception(); + + if (!mountpoint.empty()) + { + if (owned) + { + /* try to unmount/delete (best effort)*/ + unmount(mountpoint.c_str(), 0); + rmdir(mountpoint.c_str()); + } + } + } + + /* rethrow outside the dispatch block */ + if (exception) + { + rethrow_exception(exception); + } + + return newPath; +} + +/* Loop through the directory in the specified user directory and delete any that aren't mountpoints */ +static void cleanupTranslocationDirForUser(const string &userDir) +{ + DIR* translocationDir = opendir(userDir.c_str()); + + if( translocationDir ) + { + struct dirent de; + struct statfs sfbuf; + struct dirent * result = NULL; + + while (readdir_r(translocationDir, &de, &result) == 0 && result) + { + if(result->d_type == DT_DIR) + { + if (result->d_name[0] == '.') + { + if(result->d_namlen == 1 || + (result->d_namlen == 2 && + result->d_name[1] == '.')) + { + /* skip . and .. */ + continue; + } + } + string nextDir = userDir+string(result->d_name); + if (0 == statfs(nextDir.c_str(), &sfbuf) && + nextDir == sfbuf.f_mntonname) + { + /* its a mount point so continue */ + continue; + } + + /* not a mountpoint so delete it */ + if(unlinkat(dirfd(translocationDir), result->d_name, AT_REMOVEDIR)) + { + Syslog::warning("SecTranslocate: failed to delete directory during cleanup (error %d)\n\tUser Dir: %s\n\tDir to delete: %s", + errno, + userDir.c_str(), + result->d_name); + } + } + } + closedir(translocationDir); + } +} + +/* Unmount and delete a directory */ +static int removeMountPoint(const string &mountpoint, bool force) +{ + int error = 0; + + if (0 == unmount(mountpoint.c_str(), force ? MNT_FORCE : 0) && + 0 == rmdir(mountpoint.c_str())) + { + Syslog::warning("SecTranslocate: removed mountpoint: %s", + mountpoint.c_str()); + } + else + { + error = errno; + Syslog::warning("SecTranslocate: failed to unmount/remove mount point (errno: %d): %s", + error, mountpoint.c_str()); + } + + return error; +} + +/* Destroy the specified translocated path, and clean up the user's translocation directory. + It is the caller's responsibility to synchronize the operation on the dispatch queue. */ +bool destroyTranslocatedPathForUser(const string &translocatedPath) +{ + bool result = false; + int error = 0; + /* steps + 1. verify the translocatedPath is for the user + 2. verify it is a nullfs mountpoint (with app path) + 3. unmount it + 4. delete it + 5. loop through all the other directories in the app translation directory looking for directories not mounted on and delete them. + */ + + string baseDirForUser = translocationDirForUser(); // throws + bool shouldUnmount = false; + string translocatedMountpoint; + + { //Use a block to get rid of the file descriptor before we try to unmount. + ExtendedAutoFileDesc fd(translocatedPath); + translocatedMountpoint = fd.getMountPoint(); + /* + To support unmount when nested apps end, just make sure that the requested path is on a translocation + point for this user, not that they asked for a translocation point to be removed. + */ + shouldUnmount = fd.isInPrefixDir(baseDirForUser) && fd.isFileSystemType(NULLFS_FSTYPE); + } + + if (shouldUnmount) + { + error = removeMountPoint(translocatedMountpoint); + result = error == 0; + } + + if (!result && !error) + { + Syslog::warning("SecTranslocate: mountpoint does not belong to user(%d): %s", + getuid(), + translocatedPath.c_str()); + error = EPERM; + } + + cleanupTranslocationDirForUser(baseDirForUser); + + if (error) + { + UnixError::throwMe(error); + } + + return result; +} + +/* Cleanup any translocation directories for this user that are either mounted from the + specified volume or from a volume that doesn't exist anymore. If an empty volumePath + is provided this has the effect of only cleaning up translocation points that point + to volumes that don't exist anymore. + + It is the caller's responsibility to synchronize the operation on the dispatch queue. + */ +bool destroyTranslocatedPathsForUserOnVolume(const string &volumePath) +{ + bool cleanupError = false; + string baseDirForUser = translocationDirForUser(); + vector mountTable = getMountTableSnapshot(); + fsid_t unmountingFsid; + + /* passing in an empty volume here will fail to open */ + ExtendedAutoFileDesc volume(volumePath, O_RDONLY, FileDesc::modeMissingOk); + + if(volume.isOpen()) + { + unmountingFsid = volume.getFsid(); + } + + for (auto &mnt : mountTable) + { + /* + we need to look at each translocation mount and check + 1. is it ours + 2. does its mntfromname still exist, if it doesn't unmount it + 3. if it does, is it the same as the volume we are cleaning up?, if so unmount it. + */ + if (strcmp(mnt.f_fstypename, NULLFS_FSTYPE) == 0 && + strncmp(mnt.f_mntonname, baseDirForUser.c_str(), baseDirForUser.length()) == 0) + { + ExtendedAutoFileDesc volumeToCheck(mnt.f_mntfromname, O_RDONLY, FileDesc::modeMissingOk); + + if (!volumeToCheck.isOpen()) + { + // In this case we are trying to unmount a translocation point that points to nothing. Force it. + // Not forcing it currently hangs in UBC cleanup. + (void)removeMountPoint(mnt.f_mntonname , true); + } + else if (volume.isOpen()) + { + fsid_t toCheckFsid = volumeToCheck.getFsid(); + if( memcmp(&unmountingFsid, &toCheckFsid, sizeof(fsid_t)) == 0) + { + if(removeMountPoint(mnt.f_mntonname) != 0) + { + cleanupError = true; + } + } + } + } + } + + return !cleanupError; +} +/* This is intended to be used periodically to clean up translocation points that aren't used anymore */ +void tryToDestroyUnusedTranslocationMounts() +{ + vector mountTable = getMountTableSnapshot(); + string baseDirForUser = translocationDirForUser(); + + for (auto &mnt : mountTable) + { + if (strcmp(mnt.f_fstypename, NULLFS_FSTYPE) == 0 && + strncmp(mnt.f_mntonname, baseDirForUser.c_str(), baseDirForUser.length()) == 0) + { + ExtendedAutoFileDesc volumeToCheck(mnt.f_mntfromname, O_RDONLY, FileDesc::modeMissingOk); + + // Try to destroy the mount point. If the mirroed volume (volumeToCheck) isn't open then force it. + // Not forcing it currently hangs in UBC cleanup. + (void)removeMountPoint(mnt.f_mntonname , !volumeToCheck.isOpen()); + } + } +} + +} //namespace SecTranslocate +}// namespace Security