summaryrefslogtreecommitdiffstats
path: root/security/sandbox/linux/broker/SandboxBroker.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'security/sandbox/linux/broker/SandboxBroker.cpp')
-rw-r--r--security/sandbox/linux/broker/SandboxBroker.cpp1097
1 files changed, 1097 insertions, 0 deletions
diff --git a/security/sandbox/linux/broker/SandboxBroker.cpp b/security/sandbox/linux/broker/SandboxBroker.cpp
new file mode 100644
index 0000000000..c6f0cdf6ca
--- /dev/null
+++ b/security/sandbox/linux/broker/SandboxBroker.cpp
@@ -0,0 +1,1097 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SandboxBroker.h"
+#include "SandboxInfo.h"
+#include "SandboxLogging.h"
+#include "SandboxBrokerUtils.h"
+
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+#ifdef XP_LINUX
+# include <sys/prctl.h>
+#endif
+
+#include <utility>
+
+#include "GeckoProfiler.h"
+#include "SpecialSystemDirectory.h"
+#include "base/string_util.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/ipc/FileDescriptor.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsThreadUtils.h"
+#include "sandbox/linux/system_headers/linux_syscalls.h"
+
+namespace mozilla {
+
+// Default/fallback temporary directory
+static const nsLiteralCString tempDirPrefix("/tmp");
+
+// This constructor signals failure by setting mFileDesc and aClientFd to -1.
+SandboxBroker::SandboxBroker(UniquePtr<const Policy> aPolicy, int aChildPid,
+ int& aClientFd)
+ : mChildPid(aChildPid), mPolicy(std::move(aPolicy)) {
+ int fds[2];
+ if (0 != socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, fds)) {
+ SANDBOX_LOG_ERRNO("SandboxBroker: socketpair failed");
+ mFileDesc = -1;
+ aClientFd = -1;
+ return;
+ }
+ mFileDesc = fds[0];
+ aClientFd = fds[1];
+
+ if (!PlatformThread::Create(0, this, &mThread)) {
+ SANDBOX_LOG_ERRNO("SandboxBroker: thread creation failed");
+ close(mFileDesc);
+ close(aClientFd);
+ mFileDesc = -1;
+ aClientFd = -1;
+ }
+#if defined(MOZ_CONTENT_TEMP_DIR)
+ nsCOMPtr<nsIFile> tmpDir;
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_CONTENT_PROCESS_TEMP_DIR,
+ getter_AddRefs(tmpDir));
+ if (NS_SUCCEEDED(rv)) {
+ rv = tmpDir->GetNativePath(mContentTempPath);
+ if (NS_FAILED(rv)) {
+ mContentTempPath.Truncate();
+ }
+ }
+#endif
+}
+
+UniquePtr<SandboxBroker> SandboxBroker::Create(
+ UniquePtr<const Policy> aPolicy, int aChildPid,
+ ipc::FileDescriptor& aClientFdOut) {
+ int clientFd;
+ // Can't use MakeUnique here because the constructor is private.
+ UniquePtr<SandboxBroker> rv(
+ new SandboxBroker(std::move(aPolicy), aChildPid, clientFd));
+ if (clientFd < 0) {
+ rv = nullptr;
+ } else {
+ // FileDescriptor can be constructed from an int, but that dup()s
+ // the fd; instead, transfer ownership:
+ aClientFdOut = ipc::FileDescriptor(UniqueFileHandle(clientFd));
+ }
+ return rv;
+}
+
+SandboxBroker::~SandboxBroker() {
+ // If the constructor failed, there's nothing to be done here.
+ if (mFileDesc < 0) {
+ return;
+ }
+
+ shutdown(mFileDesc, SHUT_RD);
+ // The thread will now get EOF even if the client hasn't exited.
+ PlatformThread::Join(mThread);
+ // Now that the thread has exited, the fd will no longer be accessed.
+ close(mFileDesc);
+ // Having ensured that this object outlives the thread, this
+ // destructor can now return.
+}
+
+SandboxBroker::Policy::Policy() = default;
+SandboxBroker::Policy::~Policy() = default;
+
+SandboxBroker::Policy::Policy(const Policy& aOther)
+ : mMap(aOther.mMap.Clone()) {}
+
+// Chromium
+// sandbox/linux/syscall_broker/broker_file_permission.cc
+// Async signal safe
+bool SandboxBroker::Policy::ValidatePath(const char* path) const {
+ if (!path) return false;
+
+ const size_t len = strlen(path);
+ // No empty paths
+ if (len == 0) return false;
+ // Paths must be absolute and not relative
+ if (path[0] != '/') return false;
+ // No trailing / (but "/" is valid)
+ if (len > 1 && path[len - 1] == '/') return false;
+ // No trailing /.
+ if (len >= 2 && path[len - 2] == '/' && path[len - 1] == '.') return false;
+ // No trailing /..
+ if (len >= 3 && path[len - 3] == '/' && path[len - 2] == '.' &&
+ path[len - 1] == '.')
+ return false;
+ // No /../ anywhere
+ for (size_t i = 0; i < len; i++) {
+ if (path[i] == '/' && (len - i) > 3) {
+ if (path[i + 1] == '.' && path[i + 2] == '.' && path[i + 3] == '/') {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+void SandboxBroker::Policy::AddPath(int aPerms, const char* aPath,
+ AddCondition aCond) {
+ nsDependentCString path(aPath);
+ MOZ_ASSERT(path.Length() <= kMaxPathLen);
+ if (aCond == AddIfExistsNow) {
+ struct stat statBuf;
+ if (lstat(aPath, &statBuf) != 0) {
+ return;
+ }
+ }
+ auto& perms = mMap.LookupOrInsert(path, MAY_ACCESS);
+ MOZ_ASSERT(perms & MAY_ACCESS);
+
+ if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) {
+ SANDBOX_LOG("policy for %s: %d -> %d", aPath, perms, perms | aPerms);
+ }
+ perms |= aPerms;
+}
+
+void SandboxBroker::Policy::AddTree(int aPerms, const char* aPath) {
+ struct stat statBuf;
+
+ if (stat(aPath, &statBuf) != 0) {
+ return;
+ }
+ if (!S_ISDIR(statBuf.st_mode)) {
+ AddPath(aPerms, aPath, AddAlways);
+ } else {
+ DIR* dirp = opendir(aPath);
+ if (!dirp) {
+ return;
+ }
+ while (struct dirent* de = readdir(dirp)) {
+ if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) {
+ continue;
+ }
+ // Note: could optimize the string handling.
+ nsAutoCString subPath;
+ subPath.Assign(aPath);
+ subPath.Append('/');
+ subPath.Append(de->d_name);
+ AddTree(aPerms, subPath.get());
+ }
+ closedir(dirp);
+ }
+}
+
+void SandboxBroker::Policy::AddDir(int aPerms, const char* aPath) {
+ struct stat statBuf;
+
+ if (stat(aPath, &statBuf) != 0) {
+ return;
+ }
+
+ if (!S_ISDIR(statBuf.st_mode)) {
+ return;
+ }
+
+ Policy::AddDirInternal(aPerms, aPath);
+}
+
+void SandboxBroker::Policy::AddFutureDir(int aPerms, const char* aPath) {
+ Policy::AddDirInternal(aPerms, aPath);
+}
+
+void SandboxBroker::Policy::AddDirInternal(int aPerms, const char* aPath) {
+ // Add a Prefix permission on things inside the dir.
+ nsDependentCString path(aPath);
+ MOZ_ASSERT(path.Length() <= kMaxPathLen - 1);
+ // Enforce trailing / on aPath
+ if (path.Last() != '/') {
+ path.Append('/');
+ }
+ Policy::AddPrefixInternal(aPerms, path);
+
+ // Add a path permission on the dir itself so it can
+ // be opened. We're guaranteed to have a trailing / now,
+ // so just cut that.
+ path.Truncate(path.Length() - 1);
+ if (!path.IsEmpty()) {
+ Policy::AddPath(aPerms, path.get(), AddAlways);
+ }
+}
+
+void SandboxBroker::Policy::AddPrefix(int aPerms, const char* aPath) {
+ Policy::AddPrefixInternal(aPerms, nsDependentCString(aPath));
+}
+
+void SandboxBroker::Policy::AddPrefixInternal(int aPerms,
+ const nsACString& aPath) {
+ auto& perms = mMap.LookupOrInsert(aPath, MAY_ACCESS);
+ MOZ_ASSERT(perms & MAY_ACCESS);
+
+ int newPerms = perms | aPerms | RECURSIVE;
+ if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) {
+ SANDBOX_LOG("policy for %s: %d -> %d", PromiseFlatCString(aPath).get(),
+ perms, newPerms);
+ }
+ perms = newPerms;
+}
+
+void SandboxBroker::Policy::AddFilePrefix(int aPerms, const char* aDir,
+ const char* aPrefix) {
+ size_t prefixLen = strlen(aPrefix);
+ DIR* dirp = opendir(aDir);
+ struct dirent* de;
+ if (!dirp) {
+ return;
+ }
+ while ((de = readdir(dirp))) {
+ if (strcmp(de->d_name, ".") != 0 && strcmp(de->d_name, "..") != 0 &&
+ strncmp(de->d_name, aPrefix, prefixLen) == 0) {
+ nsAutoCString subPath;
+ subPath.Assign(aDir);
+ subPath.Append('/');
+ subPath.Append(de->d_name);
+ AddPath(aPerms, subPath.get(), AddAlways);
+ }
+ }
+ closedir(dirp);
+}
+
+void SandboxBroker::Policy::AddDynamic(int aPerms, const char* aPath) {
+ struct stat statBuf;
+ bool exists = (stat(aPath, &statBuf) == 0);
+
+ if (!exists) {
+ AddPrefix(aPerms, aPath);
+ } else {
+ size_t len = strlen(aPath);
+ if (!len) return;
+ if (aPath[len - 1] == '/') {
+ AddDir(aPerms, aPath);
+ } else {
+ AddPath(aPerms, aPath);
+ }
+ }
+}
+
+void SandboxBroker::Policy::AddAncestors(const char* aPath, int aPerms) {
+ nsAutoCString path(aPath);
+
+ while (true) {
+ const auto lastSlash = path.RFindCharInSet("/");
+ if (lastSlash <= 0) {
+ MOZ_ASSERT(lastSlash == 0);
+ return;
+ }
+ path.Truncate(lastSlash);
+ AddPath(aPerms, path.get());
+ }
+}
+
+void SandboxBroker::Policy::FixRecursivePermissions() {
+ // This builds an entirely new hashtable in order to avoid iterator
+ // invalidation problems.
+ PathPermissionMap oldMap;
+ mMap.SwapElements(oldMap);
+
+ if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) {
+ SANDBOX_LOG("fixing recursive policy entries");
+ }
+
+ for (const auto& entry : oldMap) {
+ const nsACString& path = entry.GetKey();
+ const int& localPerms = entry.GetData();
+ int inheritedPerms = 0;
+
+ nsAutoCString ancestor(path);
+ // This is slightly different from the loop in AddAncestors: it
+ // leaves the trailing slashes attached so they'll match AddDir
+ // entries.
+ while (true) {
+ // Last() release-asserts that the string is not empty. We
+ // should never have empty keys in the map, and the Truncate()
+ // below will always give us a non-empty string.
+ if (ancestor.Last() == '/') {
+ ancestor.Truncate(ancestor.Length() - 1);
+ }
+ const auto lastSlash = ancestor.RFindCharInSet("/");
+ if (lastSlash < 0) {
+ MOZ_ASSERT(ancestor.IsEmpty());
+ break;
+ }
+ ancestor.Truncate(lastSlash + 1);
+ const int ancestorPerms = oldMap.Get(ancestor);
+ if (ancestorPerms & RECURSIVE) {
+ // if a child is set with FORCE_DENY, do not compute inheritedPerms
+ if ((localPerms & FORCE_DENY) == FORCE_DENY) {
+ if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) {
+ SANDBOX_LOG("skip inheritence policy for %s: %d",
+ PromiseFlatCString(path).get(), localPerms);
+ }
+ } else {
+ inheritedPerms |= ancestorPerms & ~RECURSIVE;
+ }
+ }
+ }
+
+ const int newPerms = localPerms | inheritedPerms;
+ if ((newPerms & ~RECURSIVE) == inheritedPerms) {
+ if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) {
+ SANDBOX_LOG("removing redundant %s: %d -> %d",
+ PromiseFlatCString(path).get(), localPerms, newPerms);
+ }
+ // Skip adding this entry to the new map.
+ continue;
+ }
+ if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) {
+ SANDBOX_LOG("new policy for %s: %d -> %d", PromiseFlatCString(path).get(),
+ localPerms, newPerms);
+ }
+ mMap.InsertOrUpdate(path, newPerms);
+ }
+}
+
+int SandboxBroker::Policy::Lookup(const nsACString& aPath) const {
+ // Early exit for paths explicitly found in the
+ // whitelist.
+ // This means they will not gain extra permissions
+ // from recursive paths.
+ int perms = mMap.Get(aPath);
+ if (perms) {
+ return perms;
+ }
+
+ // Not a legally constructed path
+ if (!ValidatePath(PromiseFlatCString(aPath).get())) return 0;
+
+ // Now it's either an illegal access, or a recursive
+ // directory permission. We'll have to check the entire
+ // whitelist for the best match (slower).
+ int allPerms = 0;
+ for (const auto& entry : mMap) {
+ const nsACString& whiteListPath = entry.GetKey();
+ const int& perms = entry.GetData();
+
+ if (!(perms & RECURSIVE)) continue;
+
+ // passed part starts with something on the whitelist
+ if (StringBeginsWith(aPath, whiteListPath)) {
+ allPerms |= perms;
+ }
+ }
+
+ // Strip away the RECURSIVE flag as it doesn't
+ // necessarily apply to aPath.
+ return allPerms & ~RECURSIVE;
+}
+
+static bool AllowOperation(int aReqFlags, int aPerms) {
+ int needed = 0;
+ if (aReqFlags & R_OK) {
+ needed |= SandboxBroker::MAY_READ;
+ }
+ if (aReqFlags & W_OK) {
+ needed |= SandboxBroker::MAY_WRITE;
+ }
+ // We don't really allow executing anything,
+ // so in true unix tradition we hijack this
+ // for directory access (creation).
+ if (aReqFlags & X_OK) {
+ needed |= SandboxBroker::MAY_CREATE;
+ }
+ return (aPerms & needed) == needed;
+}
+
+static bool AllowAccess(int aReqFlags, int aPerms) {
+ if (aReqFlags & ~(R_OK | W_OK | X_OK | F_OK)) {
+ return false;
+ }
+ int needed = 0;
+ if (aReqFlags & R_OK) {
+ needed |= SandboxBroker::MAY_READ;
+ }
+ if (aReqFlags & W_OK) {
+ needed |= SandboxBroker::MAY_WRITE;
+ }
+ return (aPerms & needed) == needed;
+}
+
+// These flags are added to all opens to prevent possible side-effects
+// on this process. These shouldn't be relevant to the child process
+// in any case due to the sandboxing restrictions on it. (See also
+// the use of MSG_CMSG_CLOEXEC in SandboxBrokerCommon.cpp).
+static const int kRequiredOpenFlags = O_CLOEXEC | O_NOCTTY;
+
+// Linux originally assigned a flag bit to O_SYNC but implemented the
+// semantics standardized as O_DSYNC; later, that bit was renamed and
+// a new bit was assigned to the full O_SYNC, and O_SYNC was redefined
+// to be both bits. As a result, this #define is needed to compensate
+// for outdated kernel headers like Android's.
+#define O_SYNC_NEW 04010000
+static const int kAllowedOpenFlags =
+ O_APPEND | O_DIRECT | O_DIRECTORY | O_EXCL | O_LARGEFILE | O_NOATIME |
+ O_NOCTTY | O_NOFOLLOW | O_NONBLOCK | O_NDELAY | O_SYNC_NEW | O_TRUNC |
+ O_CLOEXEC | O_CREAT;
+#undef O_SYNC_NEW
+
+static bool AllowOpen(int aReqFlags, int aPerms) {
+ if (aReqFlags & ~O_ACCMODE & ~kAllowedOpenFlags) {
+ return false;
+ }
+ int needed;
+ switch (aReqFlags & O_ACCMODE) {
+ case O_RDONLY:
+ needed = SandboxBroker::MAY_READ;
+ break;
+ case O_WRONLY:
+ needed = SandboxBroker::MAY_WRITE;
+ break;
+ case O_RDWR:
+ needed = SandboxBroker::MAY_READ | SandboxBroker::MAY_WRITE;
+ break;
+ default:
+ return false;
+ }
+ if (aReqFlags & O_CREAT) {
+ needed |= SandboxBroker::MAY_CREATE;
+ }
+ // Linux allows O_TRUNC even with O_RDONLY
+ if (aReqFlags & O_TRUNC) {
+ needed |= SandboxBroker::MAY_WRITE;
+ }
+ return (aPerms & needed) == needed;
+}
+
+static int DoStat(const char* aPath, statstruct* aBuff, int aFlags) {
+ if (aFlags & O_NOFOLLOW) {
+ return lstatsyscall(aPath, aBuff);
+ }
+ return statsyscall(aPath, aBuff);
+}
+
+static int DoLink(const char* aPath, const char* aPath2,
+ SandboxBrokerCommon::Operation aOper) {
+ if (aOper == SandboxBrokerCommon::Operation::SANDBOX_FILE_LINK) {
+ return link(aPath, aPath2);
+ }
+ if (aOper == SandboxBrokerCommon::Operation::SANDBOX_FILE_SYMLINK) {
+ return symlink(aPath, aPath2);
+ }
+ MOZ_CRASH("SandboxBroker: Unknown link operation");
+}
+
+static int DoConnect(const char* aPath, size_t aLen, int aType,
+ bool aIsAbstract) {
+ // Deny SOCK_DGRAM for the same reason it's denied for socketpair.
+ if (aType != SOCK_STREAM && aType != SOCK_SEQPACKET) {
+ errno = EACCES;
+ return -1;
+ }
+ // Ensure that the address is a pathname. (An empty string
+ // resulting from an abstract address probably shouldn't have made
+ // it past the policy check, but check explicitly just in case.)
+ if (aPath[0] == '\0') {
+ errno = ENETUNREACH;
+ return -1;
+ }
+
+ // Try to copy the name into a normal-sized sockaddr_un, with
+ // null-termination. Specifically, from man page:
+ //
+ // When the address of an abstract socket is returned, the returned addrlen is
+ // greater than sizeof(sa_family_t) (i.e., greater than 2), and the name of
+ // the socket is contained in the first (addrlen - sizeof(sa_family_t)) bytes
+ // of sun_path.
+ //
+ // As mentionned in `SandboxBrokerClient::Connect()`, `DoCall` expects a
+ // null-terminated string while abstract socket are not. So we receive a copy
+ // here and we have to put things back correctly as a real abstract socket to
+ // perform the brokered `connect()` call.
+ struct sockaddr_un sun;
+ memset(&sun, 0, sizeof(sun));
+ sun.sun_family = AF_UNIX;
+ char* sunPath = sun.sun_path;
+ size_t sunLen = sizeof(sun.sun_path);
+ size_t addrLen = sizeof(sun);
+ if (aIsAbstract) {
+ *sunPath++ = '\0';
+ sunLen--;
+ addrLen = offsetof(struct sockaddr_un, sun_path) + aLen + 1;
+ }
+ if (aLen + 1 > sunLen) {
+ errno = ENAMETOOLONG;
+ return -1;
+ }
+ memcpy(sunPath, aPath, aLen);
+
+ // Finally, the actual socket connection.
+ const int fd = socket(AF_UNIX, aType | SOCK_CLOEXEC, 0);
+ if (fd < 0) {
+ return -1;
+ }
+ if (connect(fd, reinterpret_cast<struct sockaddr*>(&sun), addrLen) < 0) {
+ close(fd);
+ return -1;
+ }
+ return fd;
+}
+
+size_t SandboxBroker::RealPath(char* aPath, size_t aBufSize, size_t aPathLen) {
+ char* result = realpath(aPath, nullptr);
+ if (result != nullptr) {
+ base::strlcpy(aPath, result, aBufSize);
+ free(result);
+ // Size changed, but guaranteed to be 0 terminated
+ aPathLen = strlen(aPath);
+ }
+ return aPathLen;
+}
+
+size_t SandboxBroker::ConvertRelativePath(char* aPath, size_t aBufSize,
+ size_t aPathLen) {
+ if (strstr(aPath, "..") != nullptr) {
+ return RealPath(aPath, aBufSize, aPathLen);
+ }
+ return aPathLen;
+}
+
+#if defined(MOZ_CONTENT_TEMP_DIR)
+size_t SandboxBroker::RemapTempDirs(char* aPath, size_t aBufSize,
+ size_t aPathLen) {
+ nsAutoCString path(aPath);
+
+ size_t prefixLen = 0;
+ if (!mTempPath.IsEmpty() && StringBeginsWith(path, mTempPath)) {
+ prefixLen = mTempPath.Length();
+ } else if (StringBeginsWith(path, tempDirPrefix)) {
+ prefixLen = tempDirPrefix.Length();
+ }
+
+ if (prefixLen) {
+ const nsDependentCSubstring cutPath =
+ Substring(path, prefixLen, path.Length() - prefixLen);
+
+ // Only now try to get the content process temp dir
+ if (!mContentTempPath.IsEmpty()) {
+ nsAutoCString tmpPath;
+ tmpPath.Assign(mContentTempPath);
+ tmpPath.Append(cutPath);
+ base::strlcpy(aPath, tmpPath.get(), aBufSize);
+ return strlen(aPath);
+ }
+ }
+
+ return aPathLen;
+}
+#endif
+
+nsCString SandboxBroker::ReverseSymlinks(const nsACString& aPath) {
+ // Revert any symlinks we previously resolved.
+ int32_t cutLength = aPath.Length();
+ nsCString cutPath(Substring(aPath, 0, cutLength));
+
+ for (;;) {
+ nsCString orig;
+ bool found = mSymlinkMap.Get(cutPath, &orig);
+ if (found) {
+ orig.Append(Substring(aPath, cutLength, aPath.Length() - cutLength));
+ return orig;
+ }
+ // Not found? Remove a path component and try again.
+ int32_t pos = cutPath.RFindChar('/');
+ if (pos == kNotFound || pos <= 0) {
+ // will be empty
+ return orig;
+ } else {
+ // Cut until just before the /
+ cutLength = pos;
+ cutPath.Assign(Substring(cutPath, 0, cutLength));
+ }
+ }
+}
+
+int SandboxBroker::SymlinkPermissions(const char* aPath,
+ const size_t aPathLen) {
+ // Work on a temporary copy, so we can reverse it.
+ // Because we bail on a writable dir, SymlinkPath
+ // might not restore the callers' path exactly.
+ char pathBufSymlink[kMaxPathLen + 1];
+ strcpy(pathBufSymlink, aPath);
+
+ nsCString orig =
+ ReverseSymlinks(nsDependentCString(pathBufSymlink, aPathLen));
+ if (!orig.IsEmpty()) {
+ if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) {
+ SANDBOX_LOG("Reversing %s -> %s", aPath, orig.get());
+ }
+ base::strlcpy(pathBufSymlink, orig.get(), sizeof(pathBufSymlink));
+ }
+
+ int perms = 0;
+ // Resolve relative paths, propagate permissions and
+ // fail if a symlink is in a writable path. The output is in perms.
+ char* result =
+ SandboxBroker::SymlinkPath(mPolicy.get(), pathBufSymlink, NULL, &perms);
+ if (result != NULL) {
+ free(result);
+ // We finished the translation, so we have a usable return in "perms".
+ return perms;
+ } else {
+ // Empty path means we got a writable dir in the chain or tried
+ // to back out of a link target.
+ return 0;
+ }
+}
+
+void SandboxBroker::ThreadMain(void) {
+ // Create a nsThread wrapper for the current platform thread, and register it
+ // with the thread manager.
+ (void)NS_GetCurrentThread();
+
+ char threadName[16];
+ SprintfLiteral(threadName, "FSBroker%d", mChildPid);
+ PlatformThread::SetName(threadName);
+
+ AUTO_PROFILER_REGISTER_THREAD(threadName);
+
+ // Permissive mode can only be enabled through an environment variable,
+ // therefore it is sufficient to fetch the value once
+ // before the main thread loop starts
+ bool permissive = SandboxInfo::Get().Test(SandboxInfo::kPermissive);
+
+#if defined(MOZ_CONTENT_TEMP_DIR)
+ // Find the current temporary directory
+ nsCOMPtr<nsIFile> tmpDir;
+ nsresult rv =
+ GetSpecialSystemDirectory(OS_TemporaryDirectory, getter_AddRefs(tmpDir));
+ if (NS_SUCCEEDED(rv)) {
+ rv = tmpDir->GetNativePath(mTempPath);
+ if (NS_SUCCEEDED(rv)) {
+ // Make sure there's no terminating /
+ if (mTempPath.Last() == '/') {
+ mTempPath.Truncate(mTempPath.Length() - 1);
+ }
+ }
+ }
+ // If we can't find it, we aren't bothered much: we will
+ // always try /tmp anyway in the substitution code
+ if (NS_FAILED(rv) || mTempPath.IsEmpty()) {
+ if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) {
+ SANDBOX_LOG("Tempdir: /tmp");
+ }
+ } else {
+ if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) {
+ SANDBOX_LOG("Tempdir: %s", mTempPath.get());
+ }
+ // If it's /tmp, clear it here so we don't compare against
+ // it twice. Just let the fallback code do the work.
+ if (mTempPath.Equals(tempDirPrefix)) {
+ mTempPath.Truncate();
+ }
+ }
+#endif
+
+ while (true) {
+ struct iovec ios[2];
+ // We will receive the path strings in 1 buffer and split them back up.
+ char recvBuf[2 * (kMaxPathLen + 1)];
+ char pathBuf[kMaxPathLen + 1];
+ char pathBuf2[kMaxPathLen + 1];
+ size_t pathLen = 0;
+ size_t pathLen2 = 0;
+ char respBuf[kMaxPathLen + 1]; // Also serves as struct stat
+ Request req;
+ Response resp;
+ int respfd;
+
+ // Make sure stat responses fit in the response buffer
+ MOZ_ASSERT((kMaxPathLen + 1) > sizeof(struct stat));
+
+ // This makes our string handling below a bit less error prone.
+ memset(recvBuf, 0, sizeof(recvBuf));
+
+ ios[0].iov_base = &req;
+ ios[0].iov_len = sizeof(req);
+ ios[1].iov_base = recvBuf;
+ ios[1].iov_len = sizeof(recvBuf);
+
+ const ssize_t recvd = RecvWithFd(mFileDesc, ios, 2, &respfd);
+ if (recvd == 0) {
+ if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) {
+ SANDBOX_LOG("EOF from pid %d", mChildPid);
+ }
+ break;
+ }
+ // It could be possible to continue after errors and short reads,
+ // at least in some cases, but protocol violation indicates a
+ // hostile client, so terminate the broker instead.
+ if (recvd < 0) {
+ SANDBOX_LOG_ERRNO("bad read from pid %d", mChildPid);
+ shutdown(mFileDesc, SHUT_RD);
+ break;
+ }
+ if (recvd < static_cast<ssize_t>(sizeof(req))) {
+ SANDBOX_LOG("bad read from pid %d (%d < %d)", mChildPid, recvd,
+ sizeof(req));
+ shutdown(mFileDesc, SHUT_RD);
+ break;
+ }
+ if (respfd == -1) {
+ SANDBOX_LOG("no response fd from pid %d", mChildPid);
+ shutdown(mFileDesc, SHUT_RD);
+ break;
+ }
+
+ // Initialize the response with the default failure.
+ memset(&resp, 0, sizeof(resp));
+ memset(&respBuf, 0, sizeof(respBuf));
+ resp.mError = -EACCES;
+ ios[0].iov_base = &resp;
+ ios[0].iov_len = sizeof(resp);
+ ios[1].iov_base = nullptr;
+ ios[1].iov_len = 0;
+ int openedFd = -1;
+
+ // Clear permissions
+ int perms;
+
+ // Find end of first string, make sure the buffer is still
+ // 0 terminated.
+ size_t recvBufLen = static_cast<size_t>(recvd) - sizeof(req);
+ if (recvBufLen > 0 && recvBuf[recvBufLen - 1] != 0) {
+ SANDBOX_LOG("corrupted path buffer from pid %d", mChildPid);
+ shutdown(mFileDesc, SHUT_RD);
+ break;
+ }
+
+ // First path should fit in maximum path length buffer.
+ size_t first_len = strlen(recvBuf);
+ if (first_len <= kMaxPathLen) {
+ strcpy(pathBuf, recvBuf);
+ // Skip right over the terminating 0, and try to copy in the
+ // second path, if any. If there's no path, this will hit a
+ // 0 immediately (we nulled the buffer before receiving).
+ // We do not assume the second path is 0-terminated, this is
+ // enforced below.
+ strncpy(pathBuf2, recvBuf + first_len + 1, kMaxPathLen);
+
+ // First string is guaranteed to be 0-terminated.
+ pathLen = first_len;
+
+ // Look up the first pathname but first translate relative paths.
+ pathLen = ConvertRelativePath(pathBuf, sizeof(pathBuf), pathLen);
+ perms = mPolicy->Lookup(nsDependentCString(pathBuf, pathLen));
+
+ // We don't have permissions on the requested dir.
+#if defined(MOZ_CONTENT_TEMP_DIR)
+ if (!perms) {
+ // Was it a tempdir that we can remap?
+ pathLen = RemapTempDirs(pathBuf, sizeof(pathBuf), pathLen);
+ perms = mPolicy->Lookup(nsDependentCString(pathBuf, pathLen));
+ }
+#endif
+ if (!perms) {
+ // Did we arrive from a symlink in a path that is not writable?
+ // Then try to figure out the original path and see if that is
+ // readable. Work on the original path, this reverses
+ // ConvertRelative above.
+ int symlinkPerms = SymlinkPermissions(recvBuf, first_len);
+ if (symlinkPerms > 0) {
+ perms = symlinkPerms;
+ }
+ }
+ if (!perms) {
+ // Now try the opposite case: translate symlinks to their
+ // actual destination file. Firefox always resolves symlinks,
+ // and in most cases we have whitelisted fixed paths that
+ // libraries will rely on and try to open. So this codepath
+ // is mostly useful for Mesa which had its kernel interface
+ // moved around.
+ pathLen = RealPath(pathBuf, sizeof(pathBuf), pathLen);
+ perms = mPolicy->Lookup(nsDependentCString(pathBuf, pathLen));
+ }
+
+ // Same for the second path.
+ pathLen2 = strnlen(pathBuf2, kMaxPathLen);
+ if (pathLen2 > 0) {
+ // Force 0 termination.
+ pathBuf2[pathLen2] = '\0';
+ pathLen2 = ConvertRelativePath(pathBuf2, sizeof(pathBuf2), pathLen2);
+ int perms2 = mPolicy->Lookup(nsDependentCString(pathBuf2, pathLen2));
+
+ // Take the intersection of the permissions for both paths.
+ perms &= perms2;
+ }
+ } else {
+ // Failed to receive intelligible paths.
+ perms = 0;
+ }
+
+ // And now perform the operation if allowed.
+ if (perms & CRASH_INSTEAD) {
+ // This is somewhat nonmodular, but it works.
+ resp.mError = -ENOSYS;
+ } else if ((perms & FORCE_DENY) == FORCE_DENY) {
+ resp.mError = -EACCES;
+ } else if (permissive || perms & MAY_ACCESS) {
+ // If the operation was only allowed because of permissive mode, log it.
+ if (permissive && !(perms & MAY_ACCESS)) {
+ AuditPermissive(req.mOp, req.mFlags, perms, pathBuf);
+ }
+
+ switch (req.mOp) {
+ case SANDBOX_FILE_OPEN:
+ if (permissive || AllowOpen(req.mFlags, perms)) {
+ // Permissions for O_CREAT hardwired to 0600; if that's
+ // ever a problem we can change the protocol (but really we
+ // should be trying to remove uses of MAY_CREATE, not add
+ // new ones).
+ openedFd = open(pathBuf, req.mFlags | kRequiredOpenFlags, 0600);
+ if (openedFd >= 0) {
+ resp.mError = 0;
+ } else {
+ resp.mError = -errno;
+ }
+ } else {
+ AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+ }
+ break;
+
+ case SANDBOX_FILE_ACCESS:
+ if (permissive || AllowAccess(req.mFlags, perms)) {
+ if (access(pathBuf, req.mFlags) == 0) {
+ resp.mError = 0;
+ } else {
+ resp.mError = -errno;
+ }
+ } else {
+ AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+ }
+ break;
+
+ case SANDBOX_FILE_STAT:
+ MOZ_ASSERT(req.mBufSize == sizeof(statstruct));
+ if (DoStat(pathBuf, (statstruct*)&respBuf, req.mFlags) == 0) {
+ resp.mError = 0;
+ ios[1].iov_base = &respBuf;
+ ios[1].iov_len = sizeof(statstruct);
+ } else {
+ resp.mError = -errno;
+ }
+ break;
+
+ case SANDBOX_FILE_CHMOD:
+ if (permissive || AllowOperation(W_OK, perms)) {
+ if (chmod(pathBuf, req.mFlags) == 0) {
+ resp.mError = 0;
+ } else {
+ resp.mError = -errno;
+ }
+ } else {
+ AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+ }
+ break;
+
+ case SANDBOX_FILE_LINK:
+ case SANDBOX_FILE_SYMLINK:
+ if (permissive || AllowOperation(W_OK | X_OK, perms)) {
+ if (DoLink(pathBuf, pathBuf2, req.mOp) == 0) {
+ resp.mError = 0;
+ } else {
+ resp.mError = -errno;
+ }
+ } else {
+ AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+ }
+ break;
+
+ case SANDBOX_FILE_RENAME:
+ if (permissive || AllowOperation(W_OK | X_OK, perms)) {
+ if (rename(pathBuf, pathBuf2) == 0) {
+ resp.mError = 0;
+ } else {
+ resp.mError = -errno;
+ }
+ } else {
+ AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+ }
+ break;
+
+ case SANDBOX_FILE_MKDIR:
+ if (permissive || AllowOperation(W_OK | X_OK, perms)) {
+ if (mkdir(pathBuf, req.mFlags) == 0) {
+ resp.mError = 0;
+ } else {
+ resp.mError = -errno;
+ }
+ } else {
+ struct stat sb;
+ // This doesn't need an additional policy check because
+ // MAY_ACCESS is required to even enter this switch statement.
+ if (lstat(pathBuf, &sb) == 0) {
+ resp.mError = -EEXIST;
+ } else {
+ AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+ }
+ }
+ break;
+
+ case SANDBOX_FILE_UNLINK:
+ if (permissive || AllowOperation(W_OK | X_OK, perms)) {
+ if (unlink(pathBuf) == 0) {
+ resp.mError = 0;
+ } else {
+ resp.mError = -errno;
+ }
+ } else {
+ AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+ }
+ break;
+
+ case SANDBOX_FILE_RMDIR:
+ if (permissive || AllowOperation(W_OK | X_OK, perms)) {
+ if (rmdir(pathBuf) == 0) {
+ resp.mError = 0;
+ } else {
+ resp.mError = -errno;
+ }
+ } else {
+ AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+ }
+ break;
+
+ case SANDBOX_FILE_READLINK:
+ if (permissive || AllowOperation(R_OK, perms)) {
+ ssize_t respSize =
+ readlink(pathBuf, (char*)&respBuf, sizeof(respBuf));
+ if (respSize >= 0) {
+ if (respSize > 0) {
+ // Record the mapping so we can invert the file to the original
+ // symlink.
+ nsDependentCString orig(pathBuf, pathLen);
+ nsDependentCString xlat(respBuf, respSize);
+ if (!orig.Equals(xlat) && xlat[0] == '/') {
+ if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) {
+ SANDBOX_LOG("Recording mapping %s -> %s", xlat.get(),
+ orig.get());
+ }
+ mSymlinkMap.InsertOrUpdate(xlat, orig);
+ }
+ // Make sure we can invert a fully resolved mapping too. If our
+ // caller is realpath, and there's a relative path involved, the
+ // client side will try to open this one.
+ char* resolvedBuf = realpath(pathBuf, nullptr);
+ if (resolvedBuf) {
+ nsDependentCString resolvedXlat(resolvedBuf);
+ if (!orig.Equals(resolvedXlat) &&
+ !xlat.Equals(resolvedXlat)) {
+ if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) {
+ SANDBOX_LOG("Recording mapping %s -> %s",
+ resolvedXlat.get(), orig.get());
+ }
+ mSymlinkMap.InsertOrUpdate(resolvedXlat, orig);
+ }
+ free(resolvedBuf);
+ }
+ }
+ resp.mError = respSize;
+ ios[1].iov_base = &respBuf;
+ ios[1].iov_len = respSize;
+ } else {
+ resp.mError = -errno;
+ }
+ } else {
+ AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+ }
+ break;
+
+ case SANDBOX_SOCKET_CONNECT:
+ case SANDBOX_SOCKET_CONNECT_ABSTRACT:
+ if (permissive || (perms & MAY_CONNECT) != 0) {
+ openedFd = DoConnect(pathBuf, pathLen, req.mFlags,
+ req.mOp == SANDBOX_SOCKET_CONNECT_ABSTRACT);
+ if (openedFd >= 0) {
+ resp.mError = 0;
+ } else {
+ resp.mError = -errno;
+ }
+ } else {
+ AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+ }
+ break;
+ }
+ } else {
+ MOZ_ASSERT(perms == 0);
+ AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+ }
+
+ const size_t numIO = ios[1].iov_len > 0 ? 2 : 1;
+ const ssize_t sent = SendWithFd(respfd, ios, numIO, openedFd);
+ if (sent < 0) {
+ SANDBOX_LOG_ERRNO("failed to send broker response to pid %d", mChildPid);
+ } else {
+ MOZ_ASSERT(static_cast<size_t>(sent) == ios[0].iov_len + ios[1].iov_len);
+ }
+
+ // Work around Linux kernel bug: recvmsg checks for pending data
+ // and then checks for EOF or shutdown, without synchronization;
+ // if the sendmsg and last close occur between those points, it
+ // will see no pending data (before) and a closed socket (after),
+ // and incorrectly return EOF even though there is a message to be
+ // read. To avoid this, we send an extra message with a reference
+ // to respfd, so the last close can't happen until after the real
+ // response is read.
+ //
+ // See also: https://bugzil.la/1243108#c48
+ const struct Response fakeResp = {-4095};
+ const struct iovec fakeIO = {const_cast<Response*>(&fakeResp),
+ sizeof(fakeResp)};
+ // If the client has already read the real response and closed its
+ // socket then this will fail, but that's fine.
+ if (SendWithFd(respfd, &fakeIO, 1, respfd) < 0) {
+ MOZ_ASSERT(errno == EPIPE || errno == ECONNREFUSED || errno == ENOTCONN);
+ }
+
+ close(respfd);
+
+ if (openedFd >= 0) {
+ close(openedFd);
+ }
+ }
+}
+
+void SandboxBroker::AuditPermissive(int aOp, int aFlags, int aPerms,
+ const char* aPath) {
+ MOZ_RELEASE_ASSERT(SandboxInfo::Get().Test(SandboxInfo::kPermissive));
+
+ struct stat statBuf;
+
+ if (lstat(aPath, &statBuf) == 0) {
+ // Path exists, set errno to 0 to indicate "success".
+ errno = 0;
+ }
+
+ SANDBOX_LOG_ERRNO(
+ "SandboxBroker: would have denied op=%s rflags=%o perms=%d path=%s for "
+ "pid=%d permissive=1; real status",
+ OperationDescription[aOp], aFlags, aPerms, aPath, mChildPid);
+}
+
+void SandboxBroker::AuditDenial(int aOp, int aFlags, int aPerms,
+ const char* aPath) {
+ if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) {
+ SANDBOX_LOG(
+ "SandboxBroker: denied op=%s rflags=%o perms=%d path=%s for pid=%d",
+ OperationDescription[aOp], aFlags, aPerms, aPath, mChildPid);
+ }
+}
+
+} // namespace mozilla