SecTranslocateShared.cpp [plain text]
#include <vector>
#include <string>
#include <exception>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/param.h>
#include <sys/mount.h>
#include <sys/ucred.h>
#include <dispatch/dispatch.h>
#include <string.h>
#include <dirent.h>
#define __APPLE_API_PRIVATE
#include <quarantine.h>
#undef __APPLE_API_PRIVATE
#include <security_utilities/cfutilities.h>
#include <security_utilities/unix++.h>
#include <security_utilities/logging.h>
#include <Security/SecStaticCode.h>
#include "SecTranslocateShared.hpp"
#include "SecTranslocateUtilities.hpp"
namespace Security {
namespace SecTranslocate {
using namespace std;
const char* kSecTranslocateXPCFuncCreate = "create";
const char* kSecTranslocateXPCFuncCheckIn = "check-in";
const char* kSecTranslocateXPCMessageFunction = "function";
const char* kSecTranslocateXPCMessageOriginalPath = "original";
const char* kSecTranslocateXPCMessageDestinationPath = "dest";
const char* kSecTranslocateXPCMessagePid = "pid";
const char* kSecTranslocateXPCReplyError = "error";
const char* kSecTranslocateXPCReplySecurePath = "result";
static void setMountPointQuarantineIfNecessary(const string &mountPoint, const string &originalPath);
static string getMountpointFromAppPath(const string &appPath, const string &originalPath);
static vector<struct statfs> getMountTableSnapshot();
static string mountExistsForUser(const string &translationDirForUser,
const TranslocationPath &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);
TranslocationPath::TranslocationPath(string originalPath)
{
ExtendedAutoFileDesc fd(originalPath);
should = false;
realOriginalPath = fd.getRealPath();
if(!fd.isFileSystemType(NULLFS_FSTYPE) && fd.isQuarantined() && fd.shouldTranslocate() && !fd.isMountPoint())
{
ExtendedAutoFileDesc &&outermost = findOuterMostCodeBundleForFD(fd);
should = outermost.isQuarantined() && outermost.shouldTranslocate();
pathToTranslocate = outermost.getRealPath();
if (should)
{
vector<string> originalComponents = splitPath(realOriginalPath);
vector<string> 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);
}
componentNameToTranslocate = toTranslocateComponents.back();
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
{
if(pathInsideTranslocationPoint.empty())
{
pathInsideTranslocationPoint = originalComponents[cnt];
}
else
{
pathInsideTranslocationPoint += "/" + originalComponents[cnt];
}
}
}
}
}
}
string TranslocationPath::getTranslocatedPathToOriginalPath(const string &translocationPoint) const
{
string seperator = translocationPoint.back() != '/' ? "/" : "";
if (should)
{
if(!pathInsideTranslocationPoint.empty())
{
return translocationPoint + seperator + pathInsideTranslocationPoint;
}
else
{
return translocationPoint;
}
}
else
{
return realOriginalPath;
}
}
ExtendedAutoFileDesc TranslocationPath::findOuterMostCodeBundleForFD(ExtendedAutoFileDesc &fd)
{
if( fd.isMountPoint() || !fd.isQuarantined())
{
return fd;
}
vector<string> path = splitPath(fd.getRealPath());
size_t currentIndex = path.size() - 1;
size_t lastGoodIndex = currentIndex;
string pathToCheck = joinPathUpTo(path, currentIndex);
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));
}
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<string> mountFromPath = splitPath(fd.getMountFromPath());
vector<string> mountPointPath = splitPath(fd.getMountPoint());
vector<string> 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<translocatedRealPath.size(); i++)
{
if( i < mountPointPath.size())
{
if(translocatedRealPath[i] != mountPointPath[i])
{
Syslog::error("SecTranslocate: invalid translocated path %s", fd.getRealPath().c_str());
UnixError::throwMe(EINVAL);
}
}
else if( i == mountPointPath.size())
{
if( translocatedRealPath[i] != "d")
{
Syslog::error("SecTranslocate: invalid translocated path %s", fd.getRealPath().c_str());
UnixError::throwMe(EINVAL);
}
}
else if( i == mountPointPath.size() + 1)
{
if( translocatedRealPath[i] != mountFromPath.back())
{
Syslog::error("SecTranslocate: invalid translocated path %s", fd.getRealPath().c_str());
UnixError::throwMe(EINVAL);
}
}
else
{
originalPath +="/"+translocatedRealPath[i];
}
}
if( i == mountPointPath.size() || i == mountPointPath.size() + 1)
{
Syslog::warning("SecTranslocate: asked for the original path of a virtual directory: %s", fd.getRealPath().c_str());
UnixError::throwMe(ENOENT);
}
ExtendedAutoFileDesc originalFD(originalPath);
if(!originalFD.pathIsAbsolute())
{
Syslog::error("SecTranslocate: Calculated original path contains symlinks:\n\tExpected: %s\n\tRequested: %s",
originalFD.getRealPath().c_str(),
originalPath.c_str());
UnixError::throwMe(EINVAL);
}
return originalPath;
}
static string getMountpointFromAppPath(const string &appPath, const string &originalPath)
{
string result;
vector<string> app = splitPath(appPath); vector<string> original = splitPath(originalPath);
if (original.size() == 0) {
Syslog::error("SecTranslocate: invalid original path: %s", originalPath.c_str());
UnixError::throwMe(EINVAL);
}
if (app.size() >= 3 && app.back() == original.back()) {
app.pop_back();
if(app.back() == "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;
}
static vector<struct statfs> getMountTableSnapshot()
{
vector<struct statfs> mntInfo;
int fs_cnt_first = 0;
int fs_cnt_second = 0;
int retry = 2;
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)
{
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); break;
}
}
if( retry == 0)
{
Syslog::warning("SecTranslocate: mount table is growing very quickly");
}
return mntInfo;
}
static string mountExistsForUser(const string &translationDirForUser, const TranslocationPath &originalPath, const string &destMountPoint)
{
string result;
if(!destMountPoint.empty())
{
vector<string> splitDestMount = splitPath(destMountPoint);
if(splitDestMount.size() < 2) {
Syslog::warning("SecTranslocate: invalid destination mount point: %s",
destMountPoint.c_str());
UnixError::throwMe(EINVAL);
}
splitDestMount.pop_back();
string destBaseDir = joinPath(splitDestMount)+"/";
if (translationDirForUser != destBaseDir)
{
Syslog::warning("SecTranslocate: invalid destination mount point for user\n\tExpected: %s\n\tRequested: %s",
translationDirForUser.c_str(),
destBaseDir.c_str());
UnixError::throwMe(EINVAL);
}
}
vector <struct statfs> mntbuf = getMountTableSnapshot();
ExtendedAutoFileDesc::UnixStat untranslocatedStat;
if (stat(originalPath.getPathToTranslocate().c_str(), &untranslocatedStat))
{
errno_t err = errno;
Syslog::warning("SecTranslocate: failed to stat original path (%d): %s",
err,
originalPath.getPathToTranslocate().c_str());
UnixError::throwMe(err);
}
for (auto &i : mntbuf)
{
string mountOnName = i.f_mntonname;
size_t lastNonSlashPos = mountOnName.length() - 1;
for(; lastNonSlashPos != 0 && mountOnName[lastNonSlashPos] == '/' ; lastNonSlashPos--);
if (i.f_mntfromname == originalPath.getPathToTranslocate() && strcmp(i.f_fstypename, NULLFS_FSTYPE) == 0 && lastNonSlashPos > translationDirForUser.length()-1 && strncmp(i.f_mntonname, translationDirForUser.c_str(), translationDirForUser.length()) == 0) {
if(!destMountPoint.empty())
{
if (mountOnName != destMountPoint)
{
Syslog::warning("SecTranslocate: requested destination doesn't match existing\n\tExpected: %s\n\tRequested: %s",
i.f_mntonname,
destMountPoint.c_str());
UnixError::throwMe(EEXIST);
}
}
string pathToTranslocatedApp = mountOnName+"/d/"+originalPath.getComponentNameToTranslocate();
ExtendedAutoFileDesc::UnixStat oldTranslocatedStat;
if (stat(pathToTranslocatedApp.c_str(), &oldTranslocatedStat))
{
errno_t err = errno;
Syslog::warning("SecTranslocate: expected app not inside mountpoint: %s (error: %d)", pathToTranslocatedApp.c_str(), err);
UnixError::throwMe(err);
}
if(untranslocatedStat.st_ino != oldTranslocatedStat.st_ino)
{
destroyTranslocatedPathForUser(pathToTranslocatedApp);
continue;
}
result = mountOnName;
break;
}
}
return result;
}
static void validateMountpoint(const string &mountpoint, bool owned)
{
bool isDir = false;
bool isMount = false;
bool isEmpty = true;
try {
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)
{
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);
}
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());
}
}
else
{
Syslog::warning("SecTranslocate: unexpected file detected at mountpoint location (%s). Deleting.",
mountpoint.c_str());
unlink(mountpoint.c_str());
}
}
}
rethrow_exception(current_exception());
}
}
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;
}
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);
}
}
static string newAppPath (const string &mountPoint, const TranslocationPath &originalPath)
{
string midPath = mountPoint+"/d";
string outPath = originalPath.getTranslocatedPathToOriginalPath(midPath+"/"+originalPath.getComponentNameToTranslocate());
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();
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();
}
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(); string destMountPoint;
if(!destPath.empty())
{
destMountPoint = getMountpointFromAppPath(destPath, toTranslocate); }
mountpoint = mountExistsForUser(baseDirForUser, originalPath, destMountPoint);
if (!mountpoint.empty())
{
newPath = newAppPath(mountpoint, originalPath);
return newPath;
}
if (destMountPoint.empty())
{
mountpoint = makeNewMountpoint(baseDirForUser); owned = true;
}
else
{
AutoFileDesc fd(getFDForDirectory(destMountPoint, &owned));
validateMountpoint(destMountPoint, owned); mountpoint = destMountPoint;
}
UnixError::check(mount(NULLFS_FSTYPE, mountpoint.c_str(), MNT_RDONLY, (void*)toTranslocate.c_str()));
setMountPointQuarantineIfNecessary(mountpoint, toTranslocate);
newPath = newAppPath(mountpoint, originalPath);
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());
UnixError::throwMe(EINVAL);
}
}
Syslog::warning("SecTranslocateCreateSecureDirectoryForURL: created %s",
newPath.c_str());
}
catch (...)
{
exception = current_exception();
if (!mountpoint.empty())
{
if (owned)
{
unmount(mountpoint.c_str(), 0);
rmdir(mountpoint.c_str());
}
}
}
if (exception)
{
rethrow_exception(exception);
}
return newPath;
}
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] == '.'))
{
continue;
}
}
string nextDir = userDir+string(result->d_name);
if (0 == statfs(nextDir.c_str(), &sfbuf) &&
nextDir == sfbuf.f_mntonname)
{
continue;
}
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);
}
}
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;
}
bool destroyTranslocatedPathForUser(const string &translocatedPath)
{
bool result = false;
int error = 0;
string baseDirForUser = translocationDirForUser(); bool shouldUnmount = false;
string translocatedMountpoint;
{ ExtendedAutoFileDesc fd(translocatedPath);
translocatedMountpoint = fd.getMountPoint();
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;
}
bool destroyTranslocatedPathsForUserOnVolume(const string &volumePath)
{
bool cleanupError = false;
string baseDirForUser = translocationDirForUser();
vector <struct statfs> mountTable = getMountTableSnapshot();
struct statfs sb;
fsid_t unmountingFsid;
int haveUnmountingFsid = statfs(volumePath.c_str(), &sb);
int haveMntFromState = 0;
memset(&unmountingFsid, 0, sizeof(unmountingFsid));
if(haveUnmountingFsid == 0) {
unmountingFsid = sb.f_fsid;
}
for (auto &mnt : mountTable)
{
if (strcmp(mnt.f_fstypename, NULLFS_FSTYPE) == 0 &&
strncmp(mnt.f_mntonname, baseDirForUser.c_str(), baseDirForUser.length()) == 0)
{
haveMntFromState = statfs(mnt.f_mntfromname, &sb);
if (haveMntFromState != 0)
{
(void)removeMountPoint(mnt.f_mntonname , true);
}
else if (haveUnmountingFsid == 0)
{
fsid_t toCheckFsid = sb.f_fsid;
if( memcmp(&unmountingFsid, &toCheckFsid, sizeof(fsid_t)) == 0)
{
if(removeMountPoint(mnt.f_mntonname) != 0)
{
cleanupError = true;
}
}
}
}
}
return !cleanupError;
}
void tryToDestroyUnusedTranslocationMounts()
{
vector <struct statfs> 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);
(void)removeMountPoint(mnt.f_mntonname , !volumeToCheck.isOpen());
}
}
}
} }