#include <vector>
#include <string>
#include <exception>
+#include <memory>
#include <sys/stat.h>
#include <unistd.h>
#include "SecTranslocateShared.hpp"
#include "SecTranslocateUtilities.hpp"
+#include "SecTranslocateEnumUtils.hpp"
+
+#define NULLM_UNVEIL 0x1ULL << 2
+struct null_mount_conf {
+ uint64_t flags;
+};
namespace Security {
const char* kSecTranslocateXPCMessageFunction = "function";
const char* kSecTranslocateXPCMessageOriginalPath = "original";
const char* kSecTranslocateXPCMessageDestinationPath = "dest";
+const char* kSecTranslocateXPCMessageOptions= "opts";
const char* kSecTranslocateXPCMessagePid = "pid";
/*XPC message reply keys */
static string getMountpointFromAppPath(const string &appPath, const string &originalPath);
static vector<struct statfs> getMountTableSnapshot();
-static string mountExistsForUser(const string &translationDirForUser, const string &originalPath, const string &destMount);
+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 int removeMountPoint(const string &mountpoint, bool force = false);
/* calculate whether a translocation should occur and where from */
-TranslocationPath::TranslocationPath(string originalPath)
+TranslocationPath::TranslocationPath(string originalPath, TranslocationOptions opts)
{
/* To support testing of translocation the policy is as follows:
*/
ExtendedAutoFileDesc fd(originalPath);
-
+
+ options = opts;
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())
+ /* Nullfs can't translocate other mount's roots so abort if its a mountpoint */
+ if(!fd.isFileSystemType(NULLFS_FSTYPE) && fd.isQuarantined() && fd.shouldTranslocate() && !fd.isMountPoint())
{
ExtendedAutoFileDesc &&outermost = findOuterMostCodeBundleForFD(fd);
UnixError::throwMe(EINVAL);
}
+ componentNameToTranslocate = toTranslocateComponents.back();
+
for(size_t cnt = 0; cnt < originalComponents.size(); cnt++)
{
if (cnt < toTranslocateComponents.size())
return ExtendedAutoFileDesc(joinPathUpTo(path, lastGoodIndex));
}
+GenericTranslocationPath::GenericTranslocationPath(const string& path, TranslocationOptions opts) {
+ ExtendedAutoFileDesc fd(path);
+ realOriginalPath = fd.getRealPath();
+ should = false;
+ options = opts;
+
+ /* don't translocate if it already is */
+ /* Nullfs can't translocate other mount's roots so abort if its a mountpoint */
+ if(fd.isFileSystemType(NULLFS_FSTYPE) || fd.isMountPoint()) {
+ return;
+ }
+
+ componentNameToTranslocate = splitPath(path).back();
+
+ should = true;
+}
+
/* 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. */
+ Throws if the fd isn't in a nullfs mount. */
string getOriginalPath(const ExtendedAutoFileDesc& fd, bool* isDir)
{
if (!fd.isFileSystemType(NULLFS_FSTYPE) ||
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());
return mntInfo;
}
+static bool pathExistsInMountTable(const GenericTranslocationPath& path, const string& mountpoint)
+{
+ vector <struct statfs> mntbuf = getMountTableSnapshot();
+
+ /* Save the untranslocated inode number*/
+ ExtendedAutoFileDesc::UnixStat untranslocatedStat;
+
+ if (stat(path.getOriginalRealPath().c_str(), &untranslocatedStat))
+ {
+ errno_t err = errno;
+ Syslog::warning("SecTranslocate: failed to stat original path (%d): %s",
+ err,
+ path.getOriginalRealPath().c_str());
+ UnixError::throwMe(err);
+ }
+
+ for (auto &i : mntbuf)
+ {
+ string mountOnName = i.f_mntonname;
+
+ if (path.getOriginalRealPath() == i.f_mntfromname && //mount is for the requested path
+ mountpoint == mountOnName && //mount to is the same
+ strcmp(i.f_fstypename, NULLFS_FSTYPE) == 0 // mount is a nullfs mount
+ )
+ {
+ /*
+ find the inode number for mountOnName
+ */
+ string pathToTranslocatedApp = mountOnName+"/d/"+path.getComponentNameToTranslocate();
+
+ ExtendedAutoFileDesc::UnixStat oldTranslocatedStat;
+
+ if (stat(pathToTranslocatedApp.c_str(), &oldTranslocatedStat))
+ {
+ /* We should have access to this path and it should be real so complain if thats not true. */
+ 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)
+ {
+ /* We have two Apps with the same name at the same path but different inodes. This means that the
+ translocated path is broken and should be removed */
+ destroyTranslocatedPathForUser(pathToTranslocatedApp);
+ continue;
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
/* 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)
+static string mountExistsForUser(const string &translationDirForUser, const TranslocationPath &originalPath, const string &destMountPoint)
{
string result; // start empty
vector <struct statfs> mntbuf = getMountTableSnapshot();
+ /* Save the untranslocated inode number*/
+ 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;
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
+ if (i.f_mntfromname == originalPath.getPathToTranslocate() && //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
UnixError::throwMe(EEXIST);
}
}
+ /*
+ find the inode number for mountOnName+/d/appname
+ */
+ string pathToTranslocatedApp = mountOnName+"/d/"+originalPath.getComponentNameToTranslocate();
+
+ ExtendedAutoFileDesc::UnixStat oldTranslocatedStat;
+
+ if (stat(pathToTranslocatedApp.c_str(), &oldTranslocatedStat))
+ {
+ /* We should have access to this path and it should be real so complain if thats not true. */
+ 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)
+ {
+ /* We have two Apps with the same name at the same path but different inodes. This means that the
+ translocated path is broken and should be removed */
+ destroyTranslocatedPathForUser(pathToTranslocatedApp);
+ continue;
+ }
+
result = mountOnName;
break;
}
}
}
-/* 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)
+static string newAppPathFrom (const string &mountPoint, const string &outPath)
{
- vector<string> 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);
return outFd.getRealPath();
}
+
+static string newAppPath (const string &mountPoint, const GenericTranslocationPath &originalPath)
+{
+ string outPath = mountPoint+"/d/"+originalPath.getComponentNameToTranslocate();
+ return newAppPathFrom(mountPoint, outPath);
+}
+
+/* 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)
+{
+ string outPath = originalPath.getTranslocatedPathToOriginalPath(mountPoint+"/d/"+originalPath.getComponentNameToTranslocate());
+ return newAppPathFrom(mountPoint, outPath);
+}
+
+static std::vector<char> getMountData(const string& toTranslocate, TranslocationOptions opts) {
+ std::vector<char> data;
+ data.reserve(sizeof(null_mount_conf) + toTranslocate.size() + 1);
+ null_mount_conf conf = {0};
+ if ((opts & TranslocationOptions::Unveil) == TranslocationOptions::Unveil) {
+ conf.flags = NULLM_UNVEIL;
+ }
+ data.insert(data.end(), reinterpret_cast<const char*>(&conf), reinterpret_cast<const char*>(&conf + 1));
+ data.insert(data.end(), toTranslocate.c_str(), toTranslocate.c_str() + toTranslocate.size());
+ data.push_back('\0');
+ return data;
+}
+
/* 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. */
destMountPoint = getMountpointFromAppPath(destPath, toTranslocate); //throws or returns a mountpoint
}
- mountpoint = mountExistsForUser(baseDirForUser, toTranslocate, destMountPoint); //throws, detects invalid destMountPoint string
+ mountpoint = mountExistsForUser(baseDirForUser, originalPath, destMountPoint); //throws, detects invalid destMountPoint string
if (!mountpoint.empty())
{
mountpoint = destMountPoint;
}
- UnixError::check(mount(NULLFS_FSTYPE, mountpoint.c_str(), MNT_RDONLY, (void*)toTranslocate.c_str()));
+ auto mount_data = getMountData(toTranslocate, originalPath.getOptions());
+ UnixError::check(mount(NULLFS_FSTYPE, mountpoint.c_str(), MNT_RDONLY, mount_data.data()));
setMountPointQuarantineIfNecessary(mountpoint, toTranslocate); //throws
return newPath;
}
+string translocatePathForUser(const GenericTranslocationPath &originalPath, const string &destPath)
+{
+ string newPath;
+ exception_ptr exception(0);
+
+ string mountpoint = destPath;
+ bool owned = false;
+ try
+ {
+ const string &toTranslocate = originalPath.getOriginalRealPath();
+ if (pathExistsInMountTable(originalPath, destPath))
+ {
+ /* A mount point exists already so bail*/
+ newPath = newAppPath(mountpoint, originalPath);
+ return newPath; /* exit the block */
+ }
+
+ AutoFileDesc fd(getFDForDirectory(mountpoint, &owned)); //throws, makes the directory if it doesn't exist
+
+ validateMountpoint(mountpoint, owned); //throws
+
+ auto mount_data = getMountData(toTranslocate, originalPath.getOptions());
+ UnixError::check(mount(NULLFS_FSTYPE, mountpoint.c_str(), MNT_RDONLY, mount_data.data()));
+
+ setMountPointQuarantineIfNecessary(mountpoint, toTranslocate); //throws
+
+ newPath = newAppPath(mountpoint, originalPath); //throws
+
+ // log that we created a new mountpoint (we don't log when we are re-using)
+ Syslog::warning("SecTranslocateCreateGeneric: 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)
{
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.
+ 1. verify it is a nullfs mountpoint (with app path)
+ 2. unmount it
+ 3. delete it
+ 4. loop through all the other directories in the app translation directory looking for directories not mounted on and delete them.
*/
string baseDirForUser = translocationDirForUser(); // throws
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);
+ shouldUnmount = fd.isFileSystemType(NULLFS_FSTYPE);
}
if (shouldUnmount)
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;
- /* passing in an empty volume here will fail to open */
- ExtendedAutoFileDesc volume(volumePath, O_RDONLY, FileDesc::modeMissingOk);
+ memset(&unmountingFsid, 0, sizeof(unmountingFsid));
- if(volume.isOpen())
- {
- unmountingFsid = volume.getFsid();
+ 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)
{
- ExtendedAutoFileDesc volumeToCheck(mnt.f_mntfromname, O_RDONLY, FileDesc::modeMissingOk);
+ haveMntFromState = statfs(mnt.f_mntfromname, &sb);
- if (!volumeToCheck.isOpen())
+ if (haveMntFromState != 0)
{
// 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())
+ else if (haveUnmountingFsid == 0)
{
- fsid_t toCheckFsid = volumeToCheck.getFsid();
+ fsid_t toCheckFsid = sb.f_fsid;
if( memcmp(&unmountingFsid, &toCheckFsid, sizeof(fsid_t)) == 0)
{
if(removeMountPoint(mnt.f_mntonname) != 0)
return !cleanupError;
}
+
/* This is intended to be used periodically to clean up translocation points that aren't used anymore */
void tryToDestroyUnusedTranslocationMounts()
{