summaryrefslogtreecommitdiffstats
path: root/security/sandbox/linux/broker
diff options
context:
space:
mode:
Diffstat (limited to 'security/sandbox/linux/broker')
-rw-r--r--security/sandbox/linux/broker/SandboxBroker.cpp1097
-rw-r--r--security/sandbox/linux/broker/SandboxBroker.h180
-rw-r--r--security/sandbox/linux/broker/SandboxBrokerCommon.cpp155
-rw-r--r--security/sandbox/linux/broker/SandboxBrokerCommon.h77
-rw-r--r--security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp1017
-rw-r--r--security/sandbox/linux/broker/SandboxBrokerPolicyFactory.h36
-rw-r--r--security/sandbox/linux/broker/SandboxBrokerRealpath.cpp277
-rw-r--r--security/sandbox/linux/broker/SandboxBrokerUtils.h32
-rw-r--r--security/sandbox/linux/broker/moz.build37
9 files changed, 2908 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
diff --git a/security/sandbox/linux/broker/SandboxBroker.h b/security/sandbox/linux/broker/SandboxBroker.h
new file mode 100644
index 0000000000..ad3d4b7d49
--- /dev/null
+++ b/security/sandbox/linux/broker/SandboxBroker.h
@@ -0,0 +1,180 @@
+/* -*- 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/. */
+
+#ifndef mozilla_SandboxBroker_h
+#define mozilla_SandboxBroker_h
+
+#include "mozilla/SandboxBrokerCommon.h"
+
+#include "base/platform_thread.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/UniquePtr.h"
+#include "nsTHashMap.h"
+#include "nsHashKeys.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+namespace ipc {
+class FileDescriptor;
+}
+
+// This class implements a broker for filesystem operations requested
+// by a sandboxed child process -- opening files and accessing their
+// metadata. (This is necessary in order to restrict access by path;
+// seccomp-bpf can filter only on argument register values, not
+// parameters passed in memory like pathnames.)
+//
+// The broker currently runs on a thread in the parent process (with
+// effective uid changed on B2G), which is for memory efficiency
+// (compared to forking a process) and simplicity (compared to having
+// a separate executable and serializing/deserializing the policy).
+//
+// See also ../SandboxBrokerClient.h for the corresponding client.
+
+class SandboxBroker final : private SandboxBrokerCommon,
+ public PlatformThread::Delegate {
+ public:
+ enum Perms {
+ MAY_ACCESS = 1 << 0,
+ MAY_READ = 1 << 1,
+ MAY_WRITE = 1 << 2,
+ MAY_CREATE = 1 << 3,
+ // This flag is for testing policy changes -- when the client is
+ // used with the seccomp-bpf integration, an access to this file
+ // will invoke a crash dump with the context of the syscall.
+ // (This overrides all other flags.)
+ CRASH_INSTEAD = 1 << 4,
+ // Applies to everything below this path, including subdirs created
+ // at runtime
+ RECURSIVE = 1 << 5,
+ // Allow Unix-domain socket connections to a path
+ MAY_CONNECT = 1 << 6,
+ // This flag is for adding a deny rule, so that we can e.g., allow read
+ // access to ~/.config/ but still deny access to ~/.config/mozilla/.
+ // It will bypass other checks.
+ FORCE_DENY = 1 << 7,
+ };
+ // Bitwise operations on enum values return ints, so just use int in
+ // the hash table type (and below) to avoid cluttering code with casts.
+ typedef nsTHashMap<nsCStringHashKey, int> PathPermissionMap;
+
+ class Policy {
+ PathPermissionMap mMap;
+
+ public:
+ Policy();
+ Policy(const Policy& aOther);
+ ~Policy();
+
+ // Add permissions from AddDir/AddDynamic rules to any rules that
+ // exist for their descendents, and remove any descendent rules
+ // made redundant by this process.
+ //
+ // Call this after adding rules and before using the policy to
+ // prevent the descendent rules from shadowing the ancestor rules
+ // and removing permissions that we expect the file to have.
+ void FixRecursivePermissions();
+
+ enum AddCondition {
+ AddIfExistsNow,
+ AddAlways,
+ };
+ // Typically, files that don't exist at policy creation time don't
+ // need to be whitelisted, but this allows adding entries for
+ // them if they'll exist later. See also the overload below.
+ void AddPath(int aPerms, const char* aPath, AddCondition aCond);
+ // This adds all regular files (not directories) in the tree
+ // rooted at the given path.
+ void AddTree(int aPerms, const char* aPath);
+ // A directory, and all files and directories under it, even those
+ // added after creation (the dir itself must exist).
+ void AddDir(int aPerms, const char* aPath);
+ // A directory, and all files and directories under it, even those
+ // added after creation (the dir itself may not exist).
+ void AddFutureDir(int aPerms, const char* aPath);
+ // All files in a directory with a given prefix; useful for devices.
+ void AddFilePrefix(int aPerms, const char* aDir, const char* aPrefix);
+ // Everything starting with the given path, even those files/dirs
+ // added after creation. The file or directory may or may not exist.
+ void AddPrefix(int aPerms, const char* aPath);
+ // Adds a file or dir (end with /) if it exists, and a prefix otherwhise.
+ void AddDynamic(int aPerms, const char* aPath);
+ // Adds permissions on all ancestors of a path. (This doesn't
+ // include the root directory, but if the path is given with a
+ // trailing slash it includes the path without the slash.)
+ void AddAncestors(const char* aPath, int aPerms = MAY_ACCESS);
+ // Default: add file if it exists when creating policy or if we're
+ // conferring permission to create it (log files, etc.).
+ void AddPath(int aPerms, const char* aPath) {
+ AddPath(aPerms, aPath,
+ (aPerms & MAY_CREATE) ? AddAlways : AddIfExistsNow);
+ }
+ int Lookup(const nsACString& aPath) const;
+ int Lookup(const char* aPath) const {
+ return Lookup(nsDependentCString(aPath));
+ }
+
+ bool IsEmpty() const { return mMap.Count() == 0; }
+
+ private:
+ // ValidatePath checks |path| and returns true if these conditions are met
+ // * Greater than 0 length
+ // * Is an absolute path
+ // * No trailing slash
+ // * No /../ path traversal
+ bool ValidatePath(const char* path) const;
+ void AddPrefixInternal(int aPerms, const nsACString& aPath);
+ void AddDirInternal(int aPerms, const char* aPath);
+ };
+
+ // Constructing a broker involves creating a socketpair and a
+ // background thread to handle requests, so it can fail. If this
+ // returns nullptr, do not use the value of aClientFdOut.
+ static UniquePtr<SandboxBroker> Create(UniquePtr<const Policy> aPolicy,
+ int aChildPid,
+ ipc::FileDescriptor& aClientFdOut);
+ virtual ~SandboxBroker();
+
+ private:
+ PlatformThreadHandle mThread;
+ int mFileDesc;
+ const int mChildPid;
+ const UniquePtr<const Policy> mPolicy;
+#if defined(MOZ_CONTENT_TEMP_DIR)
+ nsCString mTempPath;
+ nsCString mContentTempPath;
+#endif
+
+ typedef nsTHashMap<nsCStringHashKey, nsCString> PathMap;
+ PathMap mSymlinkMap;
+
+ SandboxBroker(UniquePtr<const Policy> aPolicy, int aChildPid, int& aClientFd);
+ void ThreadMain(void) override;
+ void AuditPermissive(int aOp, int aFlags, int aPerms, const char* aPath);
+ void AuditDenial(int aOp, int aFlags, int aPerms, const char* aPath);
+ // Remap relative paths to absolute paths.
+ size_t ConvertRelativePath(char* aPath, size_t aBufSize, size_t aPathLen);
+ size_t RealPath(char* aPath, size_t aBufSize, size_t aPathLen);
+#if defined(MOZ_CONTENT_TEMP_DIR)
+ // Remap references to /tmp and friends to the content process tempdir
+ size_t RemapTempDirs(char* aPath, size_t aBufSize, size_t aPathLen);
+#endif
+ nsCString ReverseSymlinks(const nsACString& aPath);
+ // Retrieves permissions for the path the original symlink sits in.
+ int SymlinkPermissions(const char* aPath, const size_t aPathLen);
+ // In SandboxBrokerRealPath.cpp
+ char* SymlinkPath(const Policy* aPolicy, const char* __restrict aPath,
+ char* __restrict aResolved, int* aPermission);
+
+ // Holding a UniquePtr should disallow copying, but to make that explicit:
+ SandboxBroker(const SandboxBroker&) = delete;
+ void operator=(const SandboxBroker&) = delete;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_SandboxBroker_h
diff --git a/security/sandbox/linux/broker/SandboxBrokerCommon.cpp b/security/sandbox/linux/broker/SandboxBrokerCommon.cpp
new file mode 100644
index 0000000000..2a9dcfff40
--- /dev/null
+++ b/security/sandbox/linux/broker/SandboxBrokerCommon.cpp
@@ -0,0 +1,155 @@
+/* -*- 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 "SandboxBrokerCommon.h"
+
+#include "mozilla/Assertions.h"
+
+// This file is built both within libxul and as a separate libmozsandbox
+// library. We can only use profiler annotations within libxul.
+#ifdef MOZILLA_INTERNAL_API
+# include "mozilla/ProfilerThreadSleep.h"
+#else
+# define AUTO_PROFILER_THREAD_SLEEP
+#endif
+
+#include <errno.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <string.h>
+
+#ifndef MSG_CMSG_CLOEXEC
+# ifdef XP_LINUX
+// As always, Android's kernel headers are somewhat old.
+# define MSG_CMSG_CLOEXEC 0x40000000
+# else
+// Most of this code can support other POSIX OSes, but being able to
+// receive fds and atomically make them close-on-exec is important,
+// because this is running in a multithreaded process that can fork.
+// In the future, if the broker becomes a dedicated executable, this
+// can change.
+# error "No MSG_CMSG_CLOEXEC?"
+# endif // XP_LINUX
+#endif // MSG_CMSG_CLOEXEC
+
+namespace mozilla {
+
+const char* SandboxBrokerCommon::OperationDescription[] = {
+ "open",
+ "access",
+ "stat",
+ "chmod",
+ "link",
+ "symlink",
+ "mkdir",
+ "rename",
+ "rmdir",
+ "unlink",
+ "readlink",
+ "connect",
+ "connect-abstract",
+};
+
+/* static */
+ssize_t SandboxBrokerCommon::RecvWithFd(int aFd, const iovec* aIO,
+ size_t aNumIO, int* aPassedFdPtr) {
+ struct msghdr msg = {};
+ msg.msg_iov = const_cast<iovec*>(aIO);
+ msg.msg_iovlen = aNumIO;
+
+ char cmsg_buf[CMSG_SPACE(sizeof(int))];
+ if (aPassedFdPtr) {
+ msg.msg_control = cmsg_buf;
+ msg.msg_controllen = sizeof(cmsg_buf);
+ *aPassedFdPtr = -1;
+ }
+
+ ssize_t rv;
+ do {
+ // MSG_CMSG_CLOEXEC is needed to prevent the parent process from
+ // accidentally leaking a copy of the child's response socket to a
+ // new child process. (The child won't be able to exec, so this
+ // doesn't matter as much for that direction.)
+ AUTO_PROFILER_THREAD_SLEEP;
+ rv = recvmsg(aFd, &msg, MSG_CMSG_CLOEXEC);
+ } while (rv < 0 && errno == EINTR);
+
+ if (rv <= 0) {
+ return rv;
+ }
+ if (msg.msg_controllen > 0) {
+ MOZ_ASSERT(aPassedFdPtr);
+ struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msg);
+ if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) {
+ int* fds = reinterpret_cast<int*>(CMSG_DATA(cmsg));
+ if (cmsg->cmsg_len != CMSG_LEN(sizeof(int))) {
+ // A client could, for example, send an extra 32-bit int if
+ // CMSG_SPACE pads to 64-bit size_t alignment. If so, treat
+ // it as an error, but also don't leak the fds.
+ for (size_t i = 0; CMSG_LEN(sizeof(int) * i) < cmsg->cmsg_len; ++i) {
+ close(fds[i]);
+ }
+ // In theory, the kernel should delete the message instead of
+ // giving us an empty one, if errors prevent transferring the
+ // fd.
+ MOZ_DIAGNOSTIC_ASSERT(cmsg->cmsg_len != 0);
+ errno = EMSGSIZE;
+ return -1;
+ }
+ *aPassedFdPtr = fds[0];
+ } else {
+ errno = EPROTO;
+ return -1;
+ }
+ }
+ if (msg.msg_flags & (MSG_TRUNC | MSG_CTRUNC)) {
+ if (aPassedFdPtr && *aPassedFdPtr >= 0) {
+ close(*aPassedFdPtr);
+ *aPassedFdPtr = -1;
+ }
+ // MSG_CTRUNC usually means the fd was dropped due to fd
+ // exhaustion in the receiving process, so map that to `EMFILE`.
+ // MSG_TRUNC (truncation of the data part) shouldn't ever happen.
+ // (If the sender is malicious it can send too many bytes or fds,
+ // but this is about getting an accurate error message in genuine
+ // error cases.)
+ MOZ_DIAGNOSTIC_ASSERT((msg.msg_flags & MSG_TRUNC) == 0);
+ errno = EMFILE;
+ return -1;
+ }
+
+ return rv;
+}
+
+/* static */
+ssize_t SandboxBrokerCommon::SendWithFd(int aFd, const iovec* aIO,
+ size_t aNumIO, int aPassedFd) {
+ struct msghdr msg = {};
+ msg.msg_iov = const_cast<iovec*>(aIO);
+ msg.msg_iovlen = aNumIO;
+
+ char cmsg_buf[CMSG_SPACE(sizeof(int))];
+ memset(cmsg_buf, 0, sizeof(cmsg_buf));
+ if (aPassedFd != -1) {
+ msg.msg_control = cmsg_buf;
+ msg.msg_controllen = sizeof(cmsg_buf);
+ struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msg);
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(sizeof(int));
+ *reinterpret_cast<int*>(CMSG_DATA(cmsg)) = aPassedFd;
+ }
+
+ ssize_t rv;
+ do {
+ rv = sendmsg(aFd, &msg, MSG_NOSIGNAL);
+ } while (rv < 0 && errno == EINTR);
+
+ return rv;
+}
+
+} // namespace mozilla
diff --git a/security/sandbox/linux/broker/SandboxBrokerCommon.h b/security/sandbox/linux/broker/SandboxBrokerCommon.h
new file mode 100644
index 0000000000..b6b69e2a36
--- /dev/null
+++ b/security/sandbox/linux/broker/SandboxBrokerCommon.h
@@ -0,0 +1,77 @@
+/* -*- 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/. */
+
+#ifndef mozilla_SandboxBrokerCommon_h
+#define mozilla_SandboxBrokerCommon_h
+
+#include <sys/types.h>
+
+struct iovec;
+
+// This file defines the protocol between the filesystem broker,
+// described in SandboxBroker.h, and its client, described in
+// ../SandboxBrokerClient.h; and it defines some utility functions
+// used by both.
+//
+// In order to keep the client simple while allowing it to be thread
+// safe and async signal safe, the main broker socket is used only for
+// requests; responses arrive on a per-request socketpair sent with
+// the request. (This technique is also used by Chromium and Breakpad.)
+
+namespace mozilla {
+
+class SandboxBrokerCommon {
+ public:
+ enum Operation {
+ SANDBOX_FILE_OPEN,
+ SANDBOX_FILE_ACCESS,
+ SANDBOX_FILE_STAT,
+ SANDBOX_FILE_CHMOD,
+ SANDBOX_FILE_LINK,
+ SANDBOX_FILE_SYMLINK,
+ SANDBOX_FILE_MKDIR,
+ SANDBOX_FILE_RENAME,
+ SANDBOX_FILE_RMDIR,
+ SANDBOX_FILE_UNLINK,
+ SANDBOX_FILE_READLINK,
+ SANDBOX_SOCKET_CONNECT,
+ SANDBOX_SOCKET_CONNECT_ABSTRACT,
+ };
+ // String versions of the above
+ static const char* OperationDescription[];
+
+ struct Request {
+ Operation mOp;
+ // For open, flags; for access, "mode"; for stat, O_NOFOLLOW for lstat.
+ // For connect, the socket type.
+ int mFlags;
+ // Size of return value buffer, if any
+ size_t mBufSize;
+ // The rest of the packet is the pathname.
+ // SCM_RIGHTS for response socket attached.
+ };
+
+ struct Response {
+ // Syscall result, -errno if failure, or 0 for no error
+ int mError;
+ // Followed by struct stat for stat/lstat.
+ // SCM_RIGHTS attached for successful open.
+ };
+
+ // This doesn't need to be the system's maximum path length, just
+ // the largest path that would be allowed by any policy. (It's used
+ // to size a stack-allocated buffer.)
+ static const size_t kMaxPathLen = 4096;
+
+ static ssize_t RecvWithFd(int aFd, const iovec* aIO, size_t aNumIO,
+ int* aPassedFdPtr);
+ static ssize_t SendWithFd(int aFd, const iovec* aIO, size_t aNumIO,
+ int aPassedFd);
+};
+
+} // namespace mozilla
+
+#endif // mozilla_SandboxBrokerCommon_h
diff --git a/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp b/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp
new file mode 100644
index 0000000000..fe7ce56c7e
--- /dev/null
+++ b/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp
@@ -0,0 +1,1017 @@
+/* -*- 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 "SandboxBrokerPolicyFactory.h"
+#include "SandboxInfo.h"
+#include "SandboxLogging.h"
+
+#include "base/shared_memory.h"
+#include "mozilla/Array.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Omnijar.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/SandboxLaunch.h"
+#include "mozilla/SandboxSettings.h"
+#include "mozilla/StaticPrefs_security.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/UniquePtrExtensions.h"
+#include "nsComponentManagerUtils.h"
+#include "nsPrintfCString.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+#include "nsXULAppAPI.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "SpecialSystemDirectory.h"
+#include "nsReadableUtils.h"
+#include "nsIFileStreams.h"
+#include "nsILineInputStream.h"
+#include "nsIFile.h"
+
+#include "nsNetCID.h"
+#include "prenv.h"
+
+#ifdef ANDROID
+# include "cutils/properties.h"
+#endif
+
+#ifdef MOZ_WIDGET_GTK
+# include "mozilla/WidgetUtilsGtk.h"
+# include <glib.h>
+#endif
+
+#include <dirent.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>
+#include <sys/types.h>
+#ifndef ANDROID
+# include <glob.h>
+#endif
+
+namespace mozilla {
+
+namespace {
+static const int rdonly = SandboxBroker::MAY_READ;
+static const int wronly = SandboxBroker::MAY_WRITE;
+static const int rdwr = rdonly | wronly;
+static const int rdwrcr = rdwr | SandboxBroker::MAY_CREATE;
+static const int access = SandboxBroker::MAY_ACCESS;
+static const int deny = SandboxBroker::FORCE_DENY;
+} // namespace
+
+using CacheE = std::pair<nsCString, int>;
+using FileCacheT = nsTArray<CacheE>;
+
+static void AddDriPaths(SandboxBroker::Policy* aPolicy) {
+ // Bug 1401666: Mesa driver loader part 2: Mesa <= 12 using libudev
+ // Used by libdrm, which is used by Mesa, and
+ // Intel(R) Media Driver for VAAPI.
+ if (auto dir = opendir("/dev/dri")) {
+ while (auto entry = readdir(dir)) {
+ if (entry->d_name[0] != '.') {
+ nsPrintfCString devPath("/dev/dri/%s", entry->d_name);
+ struct stat sb;
+ if (stat(devPath.get(), &sb) == 0 && S_ISCHR(sb.st_mode)) {
+ // For both the DRI node and its parent (the physical
+ // device), allow reading the "uevent" file.
+ static const Array<nsCString, 2> kSuffixes = {""_ns, "/device"_ns};
+ nsPrintfCString prefix("/sys/dev/char/%u:%u", major(sb.st_rdev),
+ minor(sb.st_rdev));
+ for (const auto& suffix : kSuffixes) {
+ nsCString sysPath(prefix + suffix);
+
+ // libudev will expand the symlink but not do full
+ // canonicalization, so it will leave in ".." path
+ // components that will be realpath()ed in the
+ // broker. To match this, allow the canonical paths.
+ UniqueFreePtr<char[]> realSysPath(realpath(sysPath.get(), nullptr));
+ if (realSysPath) {
+ // https://gitlab.freedesktop.org/mesa/drm/-/commit/3988580e4c0f4b3647a0c6af138a3825453fe6e0
+ // > term = strrchr(real_path, '/');
+ // > if (term && strncmp(term, "/virtio", 7) == 0)
+ // > *term = 0;
+ char* term = strrchr(realSysPath.get(), '/');
+ if (term && strncmp(term, "/virtio", 7) == 0) {
+ *term = 0;
+ }
+
+ aPolicy->AddFilePrefix(rdonly, realSysPath.get(), "");
+ // Allowing stat-ing and readlink-ing the parent dirs
+ nsPrintfCString basePath("%s/", realSysPath.get());
+ aPolicy->AddAncestors(basePath.get(), rdonly);
+ }
+ }
+
+ // https://gitlab.freedesktop.org/mesa/drm/-/commit/a02900133b32dd4a7d6da4966f455ab337e80dfc
+ // > strncpy(path, device_path, PATH_MAX);
+ // > strncat(path, "/subsystem", PATH_MAX);
+ // >
+ // > if (readlink(path, link, PATH_MAX) < 0)
+ // > return -errno;
+ nsCString subsystemPath(prefix + "/device/subsystem"_ns);
+ aPolicy->AddPath(rdonly, subsystemPath.get());
+ aPolicy->AddAncestors(subsystemPath.get(), rdonly);
+ }
+ }
+ }
+ closedir(dir);
+ }
+
+ // https://gitlab.freedesktop.org/mesa/mesa/-/commit/04bdbbcab3c4862bf3f54ce60fcc1d2007776f80
+ aPolicy->AddPath(rdonly, "/usr/share/drirc.d");
+
+ // https://dri.freedesktop.org/wiki/ConfigurationInfrastructure/
+ aPolicy->AddPath(rdonly, "/etc/drirc");
+
+ nsCOMPtr<nsIFile> drirc;
+ nsresult rv =
+ GetSpecialSystemDirectory(Unix_HomeDirectory, getter_AddRefs(drirc));
+ if (NS_SUCCEEDED(rv)) {
+ rv = drirc->AppendNative(".drirc"_ns);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString tmpPath;
+ rv = drirc->GetNativePath(tmpPath);
+ if (NS_SUCCEEDED(rv)) {
+ aPolicy->AddPath(rdonly, tmpPath.get());
+ }
+ }
+ }
+}
+
+static void JoinPathIfRelative(const nsACString& aCwd, const nsACString& inPath,
+ nsACString& outPath) {
+ if (inPath.Length() < 1) {
+ outPath.Assign(aCwd);
+ SANDBOX_LOG("Unjoinable path: %s", PromiseFlatCString(aCwd).get());
+ return;
+ }
+ const char* startChar = inPath.BeginReading();
+ if (*startChar != '/') {
+ // Relative path, copy basepath in front
+ outPath.Assign(aCwd);
+ outPath.Append("/");
+ outPath.Append(inPath);
+ } else {
+ // Absolute path, it's ok like this
+ outPath.Assign(inPath);
+ }
+}
+
+static void CachePathsFromFile(FileCacheT& aCache, const nsACString& aPath);
+
+static void CachePathsFromFileInternal(FileCacheT& aCache,
+ const nsACString& aCwd,
+ const nsACString& aPath) {
+ nsresult rv;
+ nsCOMPtr<nsIFile> ldconfig(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ rv = ldconfig->InitWithNativePath(aPath);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ nsCOMPtr<nsIFileInputStream> fileStream(
+ do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ rv = fileStream->Init(ldconfig, -1, -1, 0);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ nsCOMPtr<nsILineInputStream> lineStream(do_QueryInterface(fileStream, &rv));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ nsAutoCString line;
+ bool more = true;
+ do {
+ rv = lineStream->ReadLine(line, &more);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ // Cut off any comments at the end of the line, also catches lines
+ // that are entirely a comment
+ int32_t hash = line.FindChar('#');
+ if (hash >= 0) {
+ line = Substring(line, 0, hash);
+ }
+ // Simplify our following parsing by trimming whitespace
+ line.CompressWhitespace(true, true);
+ if (line.IsEmpty()) {
+ // Skip comment lines
+ continue;
+ }
+ // Check for any included files and recursively process
+ nsACString::const_iterator start, end, token_end;
+
+ line.BeginReading(start);
+ line.EndReading(end);
+ token_end = end;
+
+ if (FindInReadable("include "_ns, start, token_end)) {
+ nsAutoCString includes(Substring(token_end, end));
+ for (const nsACString& includeGlob : includes.Split(' ')) {
+ // Glob path might be relative, so add cwd if so.
+ nsAutoCString includeFile;
+ JoinPathIfRelative(aCwd, includeGlob, includeFile);
+ glob_t globbuf;
+ if (!glob(PromiseFlatCString(includeFile).get(), GLOB_NOSORT, nullptr,
+ &globbuf)) {
+ for (size_t fileIdx = 0; fileIdx < globbuf.gl_pathc; fileIdx++) {
+ nsAutoCString filePath(globbuf.gl_pathv[fileIdx]);
+ CachePathsFromFile(aCache, filePath);
+ }
+ globfree(&globbuf);
+ }
+ }
+ }
+
+ // Cut off anything behind an = sign, used by dirname=TYPE directives
+ int32_t equals = line.FindChar('=');
+ if (equals >= 0) {
+ line = Substring(line, 0, equals);
+ }
+ char* resolvedPath = realpath(line.get(), nullptr);
+ if (resolvedPath) {
+ aCache.AppendElement(std::make_pair(nsCString(resolvedPath), rdonly));
+ free(resolvedPath);
+ }
+ } while (more);
+}
+
+static void CachePathsFromFile(FileCacheT& aCache, const nsACString& aPath) {
+ // Find the new base path where that file sits in.
+ nsresult rv;
+ nsCOMPtr<nsIFile> includeFile(
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ rv = includeFile->InitWithNativePath(aPath);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) {
+ SANDBOX_LOG("Adding paths from %s to policy.",
+ PromiseFlatCString(aPath).get());
+ }
+
+ // Find the parent dir where this file sits in.
+ nsCOMPtr<nsIFile> parentDir;
+ rv = includeFile->GetParent(getter_AddRefs(parentDir));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ nsAutoCString parentPath;
+ rv = parentDir->GetNativePath(parentPath);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) {
+ SANDBOX_LOG("Parent path is %s", PromiseFlatCString(parentPath).get());
+ }
+ CachePathsFromFileInternal(aCache, parentPath, aPath);
+}
+
+static void AddLdconfigPaths(SandboxBroker::Policy* aPolicy) {
+ static StaticMutex sMutex;
+ StaticMutexAutoLock lock(sMutex);
+
+ static FileCacheT ldConfigCache{};
+ static bool ldConfigCachePopulated = false;
+ if (!ldConfigCachePopulated) {
+ CachePathsFromFile(ldConfigCache, "/etc/ld.so.conf"_ns);
+ ldConfigCachePopulated = true;
+ RunOnShutdown([&] {
+ ldConfigCache.Clear();
+ MOZ_ASSERT(ldConfigCache.IsEmpty(), "ldconfig cache should be empty");
+ });
+ }
+ for (const CacheE& e : ldConfigCache) {
+ aPolicy->AddDir(e.second, e.first.get());
+ }
+}
+
+static void AddLdLibraryEnvPaths(SandboxBroker::Policy* aPolicy) {
+ nsAutoCString LdLibraryEnv(PR_GetEnv("LD_LIBRARY_PATH"));
+ // The items in LD_LIBRARY_PATH can be separated by either colons or
+ // semicolons, according to the ld.so(8) man page, and empirically it
+ // seems to be allowed to mix them (i.e., a:b;c is a list with 3 elements).
+ // There is no support for escaping the delimiters, fortunately (for us).
+ LdLibraryEnv.ReplaceChar(';', ':');
+ for (const nsACString& libPath : LdLibraryEnv.Split(':')) {
+ char* resolvedPath = realpath(PromiseFlatCString(libPath).get(), nullptr);
+ if (resolvedPath) {
+ aPolicy->AddDir(rdonly, resolvedPath);
+ free(resolvedPath);
+ }
+ }
+}
+
+static void AddSharedMemoryPaths(SandboxBroker::Policy* aPolicy, pid_t aPid) {
+ std::string shmPath("/dev/shm");
+ if (base::SharedMemory::AppendPosixShmPrefix(&shmPath, aPid)) {
+ aPolicy->AddPrefix(rdwrcr, shmPath.c_str());
+ }
+}
+
+static void AddMemoryReporting(SandboxBroker::Policy* aPolicy, pid_t aPid) {
+ // Bug 1198552: memory reporting.
+ // Bug 1647957: memory reporting.
+ aPolicy->AddPath(rdonly, nsPrintfCString("/proc/%d/statm", aPid).get());
+ aPolicy->AddPath(rdonly, nsPrintfCString("/proc/%d/smaps", aPid).get());
+}
+
+static void AddDynamicPathList(SandboxBroker::Policy* policy,
+ const char* aPathListPref, int perms) {
+ nsAutoCString pathList;
+ nsresult rv = Preferences::GetCString(aPathListPref, pathList);
+ if (NS_SUCCEEDED(rv)) {
+ for (const nsACString& path : pathList.Split(',')) {
+ nsCString trimPath(path);
+ trimPath.Trim(" ", true, true);
+ policy->AddDynamic(perms, trimPath.get());
+ }
+ }
+}
+
+static void AddX11Dependencies(SandboxBroker::Policy* policy) {
+ // Allow Primus to contact the Bumblebee daemon to manage GPU
+ // switching on NVIDIA Optimus systems.
+ const char* bumblebeeSocket = PR_GetEnv("BUMBLEBEE_SOCKET");
+ if (bumblebeeSocket == nullptr) {
+ bumblebeeSocket = "/var/run/bumblebee.socket";
+ }
+ policy->AddPath(SandboxBroker::MAY_CONNECT, bumblebeeSocket);
+
+#if defined(MOZ_WIDGET_GTK) && defined(MOZ_X11)
+ // Allow local X11 connections, for several purposes:
+ //
+ // * for content processes to use WebGL when the browser is in headless
+ // mode, by opening the X display if/when needed
+ //
+ // * if Primus or VirtualGL is used, to contact the secondary X server
+ static const bool kIsX11 =
+ !mozilla::widget::GdkIsWaylandDisplay() && PR_GetEnv("DISPLAY");
+ if (kIsX11) {
+ policy->AddPrefix(SandboxBroker::MAY_CONNECT, "/tmp/.X11-unix/X");
+ if (auto* const xauth = PR_GetEnv("XAUTHORITY")) {
+ policy->AddPath(rdonly, xauth);
+ } else if (auto* const home = PR_GetEnv("HOME")) {
+ // This follows the logic in libXau: append "/.Xauthority",
+ // even if $HOME ends in a slash, except in the special case
+ // where HOME=/ because POSIX allows implementations to treat
+ // an initial double slash specially.
+ nsAutoCString xauth(home);
+ if (xauth != "/"_ns) {
+ xauth.Append('/');
+ }
+ xauth.AppendLiteral(".Xauthority");
+ policy->AddPath(rdonly, xauth.get());
+ }
+ }
+#endif
+}
+
+static void AddGLDependencies(SandboxBroker::Policy* policy) {
+ // Devices
+ policy->AddDir(rdwr, "/dev/dri");
+ policy->AddFilePrefix(rdwr, "/dev", "nvidia");
+
+ // Hardware info
+ AddDriPaths(policy);
+
+ // /etc and /usr/share (glvnd, libdrm, drirc, ...?)
+ policy->AddDir(rdonly, "/etc");
+ policy->AddDir(rdonly, "/usr/share");
+ policy->AddDir(rdonly, "/usr/local/share");
+
+ // Snap puts the usual /usr/share things in a different place, and
+ // we'll fail to load the library if we don't have (at least) the
+ // glvnd config:
+ if (const char* snapDesktopDir = PR_GetEnv("SNAP_DESKTOP_RUNTIME")) {
+ nsAutoCString snapDesktopShare(snapDesktopDir);
+ snapDesktopShare.AppendLiteral("/usr/share");
+ policy->AddDir(rdonly, snapDesktopShare.get());
+ }
+
+ // Note: This function doesn't do anything about Mesa's shader
+ // cache, because the details can vary by process type, including
+ // whether caching is enabled.
+
+ // This also doesn't include permissions for connecting to a display
+ // server, because headless GL (e.g., Mesa GBM) may not need it.
+}
+
+void SandboxBrokerPolicyFactory::InitContentPolicy() {
+ const bool headless =
+ StaticPrefs::security_sandbox_content_headless_AtStartup();
+
+ // Policy entries that are the same in every process go here, and
+ // are cached over the lifetime of the factory.
+ SandboxBroker::Policy* policy = new SandboxBroker::Policy;
+ // Write permssions
+
+ // Bug 1575985: WASM library sandbox needs RW access to /dev/null
+ policy->AddPath(rdwr, "/dev/null");
+
+ if (!headless) {
+ AddGLDependencies(policy);
+ AddX11Dependencies(policy);
+ }
+
+ // Read permissions
+ policy->AddPath(rdonly, "/dev/urandom");
+ policy->AddPath(rdonly, "/dev/random");
+ policy->AddPath(rdonly, "/proc/sys/crypto/fips_enabled");
+ policy->AddPath(rdonly, "/proc/cpuinfo");
+ policy->AddPath(rdonly, "/proc/meminfo");
+ policy->AddDir(rdonly, "/sys/devices/cpu");
+ policy->AddDir(rdonly, "/sys/devices/system/cpu");
+ policy->AddDir(rdonly, "/lib");
+ policy->AddDir(rdonly, "/lib64");
+ policy->AddDir(rdonly, "/usr/lib");
+ policy->AddDir(rdonly, "/usr/lib32");
+ policy->AddDir(rdonly, "/usr/lib64");
+ policy->AddDir(rdonly, "/etc");
+ policy->AddDir(rdonly, "/usr/share");
+ policy->AddDir(rdonly, "/usr/local/share");
+ // Various places where fonts reside
+ policy->AddDir(rdonly, "/usr/X11R6/lib/X11/fonts");
+ policy->AddDir(rdonly, "/nix/store");
+ // https://gitlab.com/freedesktop-sdk/freedesktop-sdk/-/blob/e434e680d22260f277f4a30ec4660ed32b591d16/files/fontconfig-flatpak.conf
+ policy->AddDir(rdonly, "/run/host/fonts");
+ policy->AddDir(rdonly, "/run/host/user-fonts");
+ policy->AddDir(rdonly, "/run/host/local-fonts");
+ policy->AddDir(rdonly, "/var/cache/fontconfig");
+
+ // Bug 1848615
+ policy->AddPath(rdonly, "/usr");
+ policy->AddPath(rdonly, "/nix");
+
+ AddLdconfigPaths(policy);
+ AddLdLibraryEnvPaths(policy);
+
+ if (!headless) {
+ // Bug 1385715: NVIDIA PRIME support
+ policy->AddPath(rdonly, "/proc/modules");
+ }
+
+ // XDG directories might be non existent according to specs:
+ // https://specifications.freedesktop.org/basedir-spec/0.8/ar01s04.html
+ //
+ // > If, when attempting to write a file, the destination directory is
+ // > non-existent an attempt should be made to create it with permission 0700.
+ //
+ // For that we use AddPath(, SandboxBroker::Policy::AddCondition::AddAlways).
+ //
+ // Allow access to XDG_CONFIG_HOME and XDG_CONFIG_DIRS
+ nsAutoCString xdgConfigHome(PR_GetEnv("XDG_CONFIG_HOME"));
+ if (!xdgConfigHome.IsEmpty()) { // AddPath will fail on empty strings
+ policy->AddFutureDir(rdonly, xdgConfigHome.get());
+ }
+
+ nsAutoCString xdgConfigDirs(PR_GetEnv("XDG_CONFIG_DIRS"));
+ for (const auto& path : xdgConfigDirs.Split(':')) {
+ if (!path.IsEmpty()) { // AddPath will fail on empty strings
+ policy->AddFutureDir(rdonly, PromiseFlatCString(path).get());
+ }
+ }
+
+ // Allow fonts subdir in XDG_DATA_HOME
+ nsAutoCString xdgDataHome(PR_GetEnv("XDG_DATA_HOME"));
+ if (!xdgDataHome.IsEmpty()) {
+ nsAutoCString fontPath(xdgDataHome);
+ fontPath.Append("/fonts");
+ policy->AddFutureDir(rdonly, PromiseFlatCString(fontPath).get());
+ }
+
+ // Any font subdirs in XDG_DATA_DIRS
+ nsAutoCString xdgDataDirs(PR_GetEnv("XDG_DATA_DIRS"));
+ for (const auto& path : xdgDataDirs.Split(':')) {
+ nsAutoCString fontPath(path);
+ fontPath.Append("/fonts");
+ policy->AddFutureDir(rdonly, PromiseFlatCString(fontPath).get());
+ }
+
+ // Extra configuration/cache dirs in the homedir that we want to allow read
+ // access to.
+ std::vector<const char*> extraConfDirsAllow = {
+ ".themes",
+ ".fonts",
+ ".cache/fontconfig",
+ };
+
+ // Fallback if XDG_CONFIG_HOME isn't set
+ if (xdgConfigHome.IsEmpty()) {
+ extraConfDirsAllow.emplace_back(".config");
+ }
+
+ nsCOMPtr<nsIFile> homeDir;
+ nsresult rv =
+ GetSpecialSystemDirectory(Unix_HomeDirectory, getter_AddRefs(homeDir));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIFile> confDir;
+
+ for (const auto& dir : extraConfDirsAllow) {
+ rv = homeDir->Clone(getter_AddRefs(confDir));
+ if (NS_SUCCEEDED(rv)) {
+ rv = confDir->AppendRelativeNativePath(nsDependentCString(dir));
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString tmpPath;
+ rv = confDir->GetNativePath(tmpPath);
+ if (NS_SUCCEEDED(rv)) {
+ policy->AddDir(rdonly, tmpPath.get());
+ }
+ }
+ }
+ }
+
+ // ~/.config/mozilla/ needs to be manually blocked, because the previous
+ // loop will allow for ~/.config/ access.
+ {
+ // If $XDG_CONFIG_HOME is set, we need to account for it.
+ // FIXME: Bug 1722272: Maybe this should just be handled with
+ // GetSpecialSystemDirectory(Unix_XDG_ConfigHome) ?
+ nsCOMPtr<nsIFile> confDirOrXDGConfigHomeDir;
+ if (!xdgConfigHome.IsEmpty()) {
+ rv = NS_NewNativeLocalFile(xdgConfigHome, true,
+ getter_AddRefs(confDirOrXDGConfigHomeDir));
+ // confDirOrXDGConfigHomeDir = nsIFile($XDG_CONFIG_HOME)
+ } else {
+ rv = homeDir->Clone(getter_AddRefs(confDirOrXDGConfigHomeDir));
+ if (NS_SUCCEEDED(rv)) {
+ // since we will use that later, we dont need to care about trailing
+ // slash
+ rv = confDirOrXDGConfigHomeDir->AppendNative(".config"_ns);
+ // confDirOrXDGConfigHomeDir = nsIFile($HOME/.config/)
+ }
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ rv = confDirOrXDGConfigHomeDir->AppendNative("mozilla"_ns);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString tmpPath;
+ rv = confDirOrXDGConfigHomeDir->GetNativePath(tmpPath);
+ if (NS_SUCCEEDED(rv)) {
+ policy->AddFutureDir(deny, tmpPath.get());
+ }
+ }
+ }
+ }
+
+ // ~/.local/share (for themes)
+ rv = homeDir->Clone(getter_AddRefs(confDir));
+ if (NS_SUCCEEDED(rv)) {
+ rv = confDir->AppendNative(".local"_ns);
+ if (NS_SUCCEEDED(rv)) {
+ rv = confDir->AppendNative("share"_ns);
+ }
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString tmpPath;
+ rv = confDir->GetNativePath(tmpPath);
+ if (NS_SUCCEEDED(rv)) {
+ policy->AddDir(rdonly, tmpPath.get());
+ }
+ }
+ }
+
+ // ~/.fonts.conf (Fontconfig)
+ rv = homeDir->Clone(getter_AddRefs(confDir));
+ if (NS_SUCCEEDED(rv)) {
+ rv = confDir->AppendNative(".fonts.conf"_ns);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString tmpPath;
+ rv = confDir->GetNativePath(tmpPath);
+ if (NS_SUCCEEDED(rv)) {
+ policy->AddPath(rdonly, tmpPath.get());
+ }
+ }
+ }
+
+ // .pangorc
+ rv = homeDir->Clone(getter_AddRefs(confDir));
+ if (NS_SUCCEEDED(rv)) {
+ rv = confDir->AppendNative(".pangorc"_ns);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString tmpPath;
+ rv = confDir->GetNativePath(tmpPath);
+ if (NS_SUCCEEDED(rv)) {
+ policy->AddPath(rdonly, tmpPath.get());
+ }
+ }
+ }
+ }
+
+ // Firefox binary dir.
+ // Note that unlike the previous cases, we use NS_GetSpecialDirectory
+ // instead of GetSpecialSystemDirectory. The former requires a working XPCOM
+ // system, which may not be the case for some tests. For querying for the
+ // location of XPCOM things, we can use it anyway.
+ nsCOMPtr<nsIFile> ffDir;
+ rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(ffDir));
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString tmpPath;
+ rv = ffDir->GetNativePath(tmpPath);
+ if (NS_SUCCEEDED(rv)) {
+ policy->AddDir(rdonly, tmpPath.get());
+ }
+ }
+
+ if (!mozilla::IsPackagedBuild()) {
+ // If this is not a packaged build the resources are likely symlinks to
+ // outside the binary dir. Therefore in non-release builds we allow reads
+ // from the whole repository. MOZ_DEVELOPER_REPO_DIR is set by mach run.
+ const char* developer_repo_dir = PR_GetEnv("MOZ_DEVELOPER_REPO_DIR");
+ if (developer_repo_dir) {
+ policy->AddDir(rdonly, developer_repo_dir);
+ }
+ }
+
+#ifdef DEBUG
+ char* bloatLog = PR_GetEnv("XPCOM_MEM_BLOAT_LOG");
+ // XPCOM_MEM_BLOAT_LOG has the format
+ // /tmp/tmpd0YzFZ.mozrunner/runtests_leaks.log
+ // but stores into /tmp/tmpd0YzFZ.mozrunner/runtests_leaks_tab_pid3411.log
+ // So cut the .log part and whitelist the prefix.
+ if (bloatLog != nullptr) {
+ size_t bloatLen = strlen(bloatLog);
+ if (bloatLen >= 4) {
+ nsAutoCString bloatStr(bloatLog);
+ bloatStr.Truncate(bloatLen - 4);
+ policy->AddPrefix(rdwrcr, bloatStr.get());
+ }
+ }
+#endif
+
+ if (!headless) {
+ AddX11Dependencies(policy);
+ }
+
+ // Bug 1732580: when packaged as a strictly confined snap, may need
+ // read-access to configuration files under $SNAP/.
+ const char* snap = PR_GetEnv("SNAP");
+ if (snap) {
+ // When running as a snap, the directory pointed to by $SNAP is guaranteed
+ // to exist before the app is launched, but unit tests need to create it
+ // dynamically, hence the use of AddFutureDir().
+ policy->AddDir(rdonly, snap);
+ }
+
+ // Read any extra paths that will get write permissions,
+ // configured by the user or distro
+ AddDynamicPathList(policy, "security.sandbox.content.write_path_whitelist",
+ rdwr);
+
+ // Whitelisted for reading by the user/distro
+ AddDynamicPathList(policy, "security.sandbox.content.read_path_whitelist",
+ rdonly);
+
+#if defined(MOZ_CONTENT_TEMP_DIR)
+ // Add write permissions on the content process specific temporary dir.
+ nsCOMPtr<nsIFile> tmpDir;
+ rv = NS_GetSpecialDirectory(NS_APP_CONTENT_PROCESS_TEMP_DIR,
+ getter_AddRefs(tmpDir));
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString tmpPath;
+ rv = tmpDir->GetNativePath(tmpPath);
+ if (NS_SUCCEEDED(rv)) {
+ policy->AddDir(rdwrcr, tmpPath.get());
+ }
+ }
+#endif
+
+ // userContent.css and the extensions dir sit in the profile, which is
+ // normally blocked.
+ nsCOMPtr<nsIFile> profileDir;
+ rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(profileDir));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIFile> workDir;
+ rv = profileDir->Clone(getter_AddRefs(workDir));
+ if (NS_SUCCEEDED(rv)) {
+ rv = workDir->AppendNative("chrome"_ns);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString tmpPath;
+ rv = workDir->GetNativePath(tmpPath);
+ if (NS_SUCCEEDED(rv)) {
+ policy->AddDir(rdonly, tmpPath.get());
+ }
+ }
+ }
+ rv = profileDir->Clone(getter_AddRefs(workDir));
+ if (NS_SUCCEEDED(rv)) {
+ rv = workDir->AppendNative("extensions"_ns);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString tmpPath;
+ rv = workDir->GetNativePath(tmpPath);
+ if (NS_SUCCEEDED(rv)) {
+ bool exists;
+ rv = workDir->Exists(&exists);
+ if (NS_SUCCEEDED(rv)) {
+ if (!exists) {
+ policy->AddPrefix(rdonly, tmpPath.get());
+ policy->AddPath(rdonly, tmpPath.get());
+ } else {
+ policy->AddDir(rdonly, tmpPath.get());
+ }
+ }
+ }
+ }
+ }
+ }
+
+ const int level = GetEffectiveContentSandboxLevel();
+ bool allowPulse = false;
+ bool allowAlsa = false;
+ if (level < 4) {
+#ifdef MOZ_PULSEAUDIO
+ allowPulse = true;
+#endif
+#ifdef MOZ_ALSA
+ allowAlsa = true;
+#endif
+ }
+
+ if (allowAlsa) {
+ // Bug 1309098: ALSA support
+ policy->AddDir(rdwr, "/dev/snd");
+ }
+
+ if (allowPulse) {
+ policy->AddDir(rdwrcr, "/dev/shm");
+ }
+
+#ifdef MOZ_WIDGET_GTK
+ if (const auto userDir = g_get_user_runtime_dir()) {
+ // Bug 1321134: DConf's single bit of shared memory
+ // The leaf filename is "user" by default, but is configurable.
+ nsPrintfCString shmPath("%s/dconf/", userDir);
+ policy->AddPrefix(rdwrcr, shmPath.get());
+ policy->AddAncestors(shmPath.get());
+ if (allowPulse) {
+ // PulseAudio, if it can't get server info from X11, will break
+ // unless it can open this directory (or create it, but in our use
+ // case we know it already exists). See bug 1335329.
+ nsPrintfCString pulsePath("%s/pulse", userDir);
+ policy->AddPath(rdonly, pulsePath.get());
+ }
+ }
+#endif // MOZ_WIDGET_GTK
+
+ if (allowPulse) {
+ // PulseAudio also needs access to read the $XAUTHORITY file (see
+ // bug 1384986 comment #1), but that's already allowed for hybrid
+ // GPU drivers (see above).
+ policy->AddPath(rdonly, "/var/lib/dbus/machine-id");
+ }
+
+ // Bug 1434711 - AMDGPU-PRO crashes if it can't read it's marketing ids
+ // and various other things
+ if (!headless && HasAtiDrivers()) {
+ policy->AddDir(rdonly, "/opt/amdgpu/share");
+ policy->AddPath(rdonly, "/sys/module/amdgpu");
+ }
+
+ mCommonContentPolicy.reset(policy);
+}
+
+UniquePtr<SandboxBroker::Policy> SandboxBrokerPolicyFactory::GetContentPolicy(
+ int aPid, bool aFileProcess) {
+ // Policy entries that vary per-process (because they depend on the
+ // pid or content subtype) are added here.
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ const int level = GetEffectiveContentSandboxLevel();
+ // The file broker is used at level 2 and up.
+ if (level <= 1) {
+ // Level 1 has been removed.
+ MOZ_ASSERT(level == 0);
+ return nullptr;
+ }
+
+ std::call_once(mContentInited, [this] { InitContentPolicy(); });
+ MOZ_ASSERT(mCommonContentPolicy);
+ UniquePtr<SandboxBroker::Policy> policy(
+ new SandboxBroker::Policy(*mCommonContentPolicy));
+
+ // No read blocking at level 2 and below.
+ // file:// processes also get global read permissions
+ if (level <= 2 || aFileProcess) {
+ policy->AddDir(rdonly, "/");
+ // Any other read-only rules will be removed as redundant by
+ // Policy::FixRecursivePermissions, so there's no need to
+ // early-return here.
+ }
+
+ // Access to /dev/shm is restricted to a per-process prefix to
+ // prevent interfering with other processes or with services outside
+ // the browser (e.g., PulseAudio).
+ AddSharedMemoryPaths(policy.get(), aPid);
+
+ // Bug 1198550: the profiler's replacement for dl_iterate_phdr
+ policy->AddPath(rdonly, nsPrintfCString("/proc/%d/maps", aPid).get());
+
+ // Bug 1736040: CPU use telemetry
+ policy->AddPath(rdonly, nsPrintfCString("/proc/%d/stat", aPid).get());
+
+ // Bug 1198552: memory reporting.
+ AddMemoryReporting(policy.get(), aPid);
+
+ // Bug 1384804, notably comment 15
+ // Used by libnuma, included by x265/ffmpeg, who falls back
+ // to get_mempolicy if this fails
+ policy->AddPath(rdonly, nsPrintfCString("/proc/%d/status", aPid).get());
+
+ // Finalize the policy.
+ policy->FixRecursivePermissions();
+ return policy;
+}
+
+/* static */ UniquePtr<SandboxBroker::Policy>
+SandboxBrokerPolicyFactory::GetRDDPolicy(int aPid) {
+ auto policy = MakeUnique<SandboxBroker::Policy>();
+
+ AddSharedMemoryPaths(policy.get(), aPid);
+
+ policy->AddPath(rdonly, "/dev/urandom");
+ // FIXME (bug 1662321): we should fix nsSystemInfo so that every
+ // child process doesn't need to re-read these files to get the info
+ // the parent process already has.
+ policy->AddPath(rdonly, "/proc/cpuinfo");
+ policy->AddPath(rdonly,
+ "/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq");
+ policy->AddPath(rdonly, "/sys/devices/system/cpu/cpu0/cache/index2/size");
+ policy->AddPath(rdonly, "/sys/devices/system/cpu/cpu0/cache/index3/size");
+ policy->AddDir(rdonly, "/sys/devices/cpu");
+ policy->AddDir(rdonly, "/sys/devices/system/cpu");
+ policy->AddDir(rdonly, "/sys/devices/system/node");
+ policy->AddDir(rdonly, "/lib");
+ policy->AddDir(rdonly, "/lib64");
+ policy->AddDir(rdonly, "/usr/lib");
+ policy->AddDir(rdonly, "/usr/lib32");
+ policy->AddDir(rdonly, "/usr/lib64");
+ policy->AddDir(rdonly, "/run/opengl-driver/lib");
+ policy->AddDir(rdonly, "/nix/store");
+
+ // Bug 1647957: memory reporting.
+ AddMemoryReporting(policy.get(), aPid);
+
+ // Firefox binary dir.
+ // Note that unlike the previous cases, we use NS_GetSpecialDirectory
+ // instead of GetSpecialSystemDirectory. The former requires a working XPCOM
+ // system, which may not be the case for some tests. For querying for the
+ // location of XPCOM things, we can use it anyway.
+ nsCOMPtr<nsIFile> ffDir;
+ nsresult rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(ffDir));
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString tmpPath;
+ rv = ffDir->GetNativePath(tmpPath);
+ if (NS_SUCCEEDED(rv)) {
+ policy->AddDir(rdonly, tmpPath.get());
+ }
+ }
+
+ if (!mozilla::IsPackagedBuild()) {
+ // If this is not a packaged build the resources are likely symlinks to
+ // outside the binary dir. Therefore in non-release builds we allow reads
+ // from the whole repository. MOZ_DEVELOPER_REPO_DIR is set by mach run.
+ const char* developer_repo_dir = PR_GetEnv("MOZ_DEVELOPER_REPO_DIR");
+ if (developer_repo_dir) {
+ policy->AddDir(rdonly, developer_repo_dir);
+ }
+ }
+
+ // VA-API needs GPU access and GL context creation (but not display
+ // server access, as of bug 1769499).
+ AddGLDependencies(policy.get());
+
+ // FFmpeg and GPU drivers may need general-case library loading
+ AddLdconfigPaths(policy.get());
+ AddLdLibraryEnvPaths(policy.get());
+
+ if (policy->IsEmpty()) {
+ policy = nullptr;
+ }
+ return policy;
+}
+
+/* static */ UniquePtr<SandboxBroker::Policy>
+SandboxBrokerPolicyFactory::GetSocketProcessPolicy(int aPid) {
+ auto policy = MakeUnique<SandboxBroker::Policy>();
+
+ policy->AddPath(rdonly, "/dev/urandom");
+ policy->AddPath(rdonly, "/dev/random");
+ policy->AddPath(rdonly, "/proc/sys/crypto/fips_enabled");
+ policy->AddPath(rdonly, "/proc/cpuinfo");
+ policy->AddPath(rdonly, "/proc/meminfo");
+ policy->AddDir(rdonly, "/sys/devices/cpu");
+ policy->AddDir(rdonly, "/sys/devices/system/cpu");
+ policy->AddDir(rdonly, "/lib");
+ policy->AddDir(rdonly, "/lib64");
+ policy->AddDir(rdonly, "/usr/lib");
+ policy->AddDir(rdonly, "/usr/lib32");
+ policy->AddDir(rdonly, "/usr/lib64");
+ policy->AddDir(rdonly, "/usr/share");
+ policy->AddDir(rdonly, "/usr/local/share");
+ policy->AddDir(rdonly, "/etc");
+
+ // glibc will try to stat64("/") while populating nsswitch database
+ // https://sourceware.org/git/?p=glibc.git;a=blob;f=nss/nss_database.c;h=cf0306adc47f12d9bc761ab1b013629f4482b7e6;hb=9826b03b747b841f5fc6de2054bf1ef3f5c4bdf3#l396
+ // denying will make getaddrinfo() return ENONAME
+ policy->AddDir(access, "/");
+
+ AddLdconfigPaths(policy.get());
+
+ // Socket process sandbox needs to allow shmem in order to support
+ // profiling. See Bug 1626385.
+ AddSharedMemoryPaths(policy.get(), aPid);
+
+ // Bug 1647957: memory reporting.
+ AddMemoryReporting(policy.get(), aPid);
+
+ // Firefox binary dir.
+ // Note that unlike the previous cases, we use NS_GetSpecialDirectory
+ // instead of GetSpecialSystemDirectory. The former requires a working XPCOM
+ // system, which may not be the case for some tests. For querying for the
+ // location of XPCOM things, we can use it anyway.
+ nsCOMPtr<nsIFile> ffDir;
+ nsresult rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(ffDir));
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString tmpPath;
+ rv = ffDir->GetNativePath(tmpPath);
+ if (NS_SUCCEEDED(rv)) {
+ policy->AddDir(rdonly, tmpPath.get());
+ }
+ }
+
+ if (policy->IsEmpty()) {
+ policy = nullptr;
+ }
+ return policy;
+}
+
+/* static */ UniquePtr<SandboxBroker::Policy>
+SandboxBrokerPolicyFactory::GetUtilityProcessPolicy(int aPid) {
+ auto policy = MakeUnique<SandboxBroker::Policy>();
+
+ policy->AddPath(rdonly, "/dev/urandom");
+ policy->AddPath(rdonly, "/proc/cpuinfo");
+ policy->AddPath(rdonly, "/proc/meminfo");
+ policy->AddPath(rdonly, nsPrintfCString("/proc/%d/exe", aPid).get());
+ policy->AddDir(rdonly, "/sys/devices/cpu");
+ policy->AddDir(rdonly, "/sys/devices/system/cpu");
+ policy->AddDir(rdonly, "/lib");
+ policy->AddDir(rdonly, "/lib64");
+ policy->AddDir(rdonly, "/usr/lib");
+ policy->AddDir(rdonly, "/usr/lib32");
+ policy->AddDir(rdonly, "/usr/lib64");
+ policy->AddDir(rdonly, "/usr/share");
+ policy->AddDir(rdonly, "/usr/local/share");
+ policy->AddDir(rdonly, "/etc");
+
+ // glibc will try to stat64("/") while populating nsswitch database
+ // https://sourceware.org/git/?p=glibc.git;a=blob;f=nss/nss_database.c;h=cf0306adc47f12d9bc761ab1b013629f4482b7e6;hb=9826b03b747b841f5fc6de2054bf1ef3f5c4bdf3#l396
+ // denying will make getaddrinfo() return ENONAME
+ policy->AddDir(access, "/");
+
+ AddLdconfigPaths(policy.get());
+ AddLdLibraryEnvPaths(policy.get());
+
+ // Utility process sandbox needs to allow shmem in order to support
+ // profiling. See Bug 1626385.
+ AddSharedMemoryPaths(policy.get(), aPid);
+
+ // Bug 1647957: memory reporting.
+ AddMemoryReporting(policy.get(), aPid);
+
+ // Firefox binary dir.
+ // Note that unlike the previous cases, we use NS_GetSpecialDirectory
+ // instead of GetSpecialSystemDirectory. The former requires a working XPCOM
+ // system, which may not be the case for some tests. For querying for the
+ // location of XPCOM things, we can use it anyway.
+ nsCOMPtr<nsIFile> ffDir;
+ nsresult rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(ffDir));
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString tmpPath;
+ rv = ffDir->GetNativePath(tmpPath);
+ if (NS_SUCCEEDED(rv)) {
+ policy->AddDir(rdonly, tmpPath.get());
+ }
+ }
+
+ if (policy->IsEmpty()) {
+ policy = nullptr;
+ }
+ return policy;
+}
+
+} // namespace mozilla
diff --git a/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.h b/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.h
new file mode 100644
index 0000000000..aff3487ef9
--- /dev/null
+++ b/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.h
@@ -0,0 +1,36 @@
+/* -*- 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/. */
+
+#ifndef mozilla_SandboxBrokerPolicyFactory_h
+#define mozilla_SandboxBrokerPolicyFactory_h
+
+#include "mozilla/SandboxBroker.h"
+
+#include <mutex>
+
+namespace mozilla {
+
+class SandboxBrokerPolicyFactory {
+ public:
+ SandboxBrokerPolicyFactory() = default;
+
+ UniquePtr<SandboxBroker::Policy> GetContentPolicy(int aPid,
+ bool aFileProcess);
+
+ static UniquePtr<SandboxBroker::Policy> GetRDDPolicy(int aPid);
+ static UniquePtr<SandboxBroker::Policy> GetSocketProcessPolicy(int aPid);
+ static UniquePtr<SandboxBroker::Policy> GetUtilityProcessPolicy(int aPid);
+
+ private:
+ UniquePtr<const SandboxBroker::Policy> mCommonContentPolicy;
+ std::once_flag mContentInited;
+
+ void InitContentPolicy();
+};
+
+} // namespace mozilla
+
+#endif // mozilla_SandboxBrokerPolicyFactory_h
diff --git a/security/sandbox/linux/broker/SandboxBrokerRealpath.cpp b/security/sandbox/linux/broker/SandboxBrokerRealpath.cpp
new file mode 100644
index 0000000000..b129af57fd
--- /dev/null
+++ b/security/sandbox/linux/broker/SandboxBrokerRealpath.cpp
@@ -0,0 +1,277 @@
+/*
+ * Copyright (c) 2003 Constantin S. Svintsoff <kostik@iclub.nsu.ru>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. The names of the authors may not be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/*
+ * This is originally from:
+ * android-n-mr2-preview-1-303-gccec0f4c1
+ * libc/upstream-freebsd/lib/libc/stdlib/realpath.c
+ */
+
+#if defined(LIBC_SCCS) && !defined(lint)
+static char sccsid[] = "@(#)realpath.c 8.1 (Berkeley) 2/16/94";
+#endif /* LIBC_SCCS and not lint */
+#include <sys/param.h>
+#include <sys/stat.h>
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "base/string_util.h"
+#include "SandboxBroker.h"
+
+// Original copy in, but not usable from here:
+// toolkit/crashreporter/google-breakpad/src/common/linux/linux_libc_support.cc
+static size_t my_strlcat(char* s1, const char* s2, size_t len) {
+ size_t pos1 = 0;
+
+ while (pos1 < len && s1[pos1] != '\0') pos1++;
+
+ if (pos1 == len) return pos1;
+
+ return pos1 + base::strlcpy(s1 + pos1, s2, len - pos1);
+}
+
+namespace mozilla {
+
+/*
+ * Original: realpath
+ * Find the real name of path, by removing all ".", ".." and symlink
+ * components. Returns (resolved) on success, or (NULL) on failure,
+ * in which case the path which caused trouble is left in (resolved).
+ * Changes:
+ * Resolve relative paths, but don't allow backing out of a symlink
+ * target. Fail with permission error if any dir is writable.
+ */
+char* SandboxBroker::SymlinkPath(const Policy* policy,
+ const char* __restrict path,
+ char* __restrict resolved, int* perms) {
+ struct stat sb;
+ char *p, *q, *s;
+ size_t left_len, resolved_len, backup_allowed;
+ unsigned symlinks;
+ int m, slen;
+ char left[PATH_MAX], next_token[PATH_MAX], symlink[PATH_MAX];
+
+ if (*perms) {
+ *perms = 0;
+ }
+ if (path == NULL) {
+ errno = EINVAL;
+ return (NULL);
+ }
+ if (path[0] == '\0') {
+ errno = ENOENT;
+ return (NULL);
+ }
+ if (resolved == NULL) {
+ resolved = (char*)malloc(PATH_MAX);
+ if (resolved == NULL) return (NULL);
+ m = 1;
+ } else
+ m = 0;
+ symlinks = 0;
+ backup_allowed = PATH_MAX;
+ if (path[0] == '/') {
+ resolved[0] = '/';
+ resolved[1] = '\0';
+ if (path[1] == '\0') return (resolved);
+ resolved_len = 1;
+ left_len = base::strlcpy(left, path + 1, sizeof(left));
+ } else {
+ if (getcwd(resolved, PATH_MAX) == NULL) {
+ if (m)
+ free(resolved);
+ else {
+ resolved[0] = '.';
+ resolved[1] = '\0';
+ }
+ return (NULL);
+ }
+ resolved_len = strlen(resolved);
+ left_len = base::strlcpy(left, path, sizeof(left));
+ }
+ if (left_len >= sizeof(left) || resolved_len >= PATH_MAX) {
+ if (m) free(resolved);
+ errno = ENAMETOOLONG;
+ return (NULL);
+ }
+
+ /*
+ * Iterate over path components in `left'.
+ */
+ while (left_len != 0) {
+ /*
+ * Extract the next path component and adjust `left'
+ * and its length.
+ */
+ p = strchr(left, '/');
+ s = p ? p : left + left_len;
+ if (s - left >= (ssize_t)sizeof(next_token)) {
+ if (m) free(resolved);
+ errno = ENAMETOOLONG;
+ return (NULL);
+ }
+ memcpy(next_token, left, s - left);
+ next_token[s - left] = '\0';
+ left_len -= s - left;
+ if (p != NULL) memmove(left, s + 1, left_len + 1);
+ if (resolved[resolved_len - 1] != '/') {
+ if (resolved_len + 1 >= PATH_MAX) {
+ if (m) free(resolved);
+ errno = ENAMETOOLONG;
+ return (NULL);
+ }
+ resolved[resolved_len++] = '/';
+ resolved[resolved_len] = '\0';
+ }
+ if (next_token[0] == '\0') {
+ /* Handle consequential slashes. */
+ continue;
+ } else if (strcmp(next_token, ".") == 0)
+ continue;
+ else if (strcmp(next_token, "..") == 0) {
+ /*
+ * Strip the last path component except when we have
+ * single "/"
+ */
+ if (resolved_len > 1) {
+ if (backup_allowed > 0) {
+ resolved[resolved_len - 1] = '\0';
+ q = strrchr(resolved, '/') + 1;
+ *q = '\0';
+ resolved_len = q - resolved;
+ backup_allowed--;
+ } else {
+ // Backing out past a symlink target.
+ // We don't allow this, because it can eliminate
+ // permissions we accumulated while descending.
+ if (m) free(resolved);
+ errno = EPERM;
+ return (NULL);
+ }
+ }
+ continue;
+ }
+
+ /*
+ * Append the next path component and lstat() it.
+ */
+ resolved_len = my_strlcat(resolved, next_token, PATH_MAX);
+ backup_allowed++;
+ if (resolved_len >= PATH_MAX) {
+ if (m) free(resolved);
+ errno = ENAMETOOLONG;
+ return (NULL);
+ }
+ if (lstat(resolved, &sb) != 0) {
+ if (m) free(resolved);
+ return (NULL);
+ }
+ if (S_ISLNK(sb.st_mode)) {
+ if (symlinks++ > MAXSYMLINKS) {
+ if (m) free(resolved);
+ errno = ELOOP;
+ return (NULL);
+ }
+ /* Our changes start here:
+ * It's a symlink, check for write permissions on the path where
+ * it sits in, in which case we won't resolve and just error out. */
+ int link_path_perms = policy->Lookup(resolved);
+ if (link_path_perms & MAY_WRITE) {
+ if (m) free(resolved);
+ errno = EPERM;
+ return (NULL);
+ } else {
+ /* Accumulate permissions so far */
+ *perms |= link_path_perms;
+ }
+ /* Original symlink lookup code */
+ slen = readlink(resolved, symlink, sizeof(symlink) - 1);
+ if (slen < 0) {
+ if (m) free(resolved);
+ return (NULL);
+ }
+ symlink[slen] = '\0';
+ if (symlink[0] == '/') {
+ resolved[1] = 0;
+ resolved_len = 1;
+ } else if (resolved_len > 1) {
+ /* Strip the last path component. */
+ resolved[resolved_len - 1] = '\0';
+ q = strrchr(resolved, '/') + 1;
+ *q = '\0';
+ resolved_len = q - resolved;
+ }
+
+ /*
+ * If there are any path components left, then
+ * append them to symlink. The result is placed
+ * in `left'.
+ */
+ if (p != NULL) {
+ if (symlink[slen - 1] != '/') {
+ if (slen + 1 >= (ssize_t)sizeof(symlink)) {
+ if (m) free(resolved);
+ errno = ENAMETOOLONG;
+ return (NULL);
+ }
+ symlink[slen] = '/';
+ symlink[slen + 1] = 0;
+ }
+ left_len = my_strlcat(symlink, left, sizeof(symlink));
+ if (left_len >= sizeof(left)) {
+ if (m) free(resolved);
+ errno = ENAMETOOLONG;
+ return (NULL);
+ }
+ }
+ left_len = base::strlcpy(left, symlink, sizeof(left));
+ backup_allowed = 0;
+ } else if (!S_ISDIR(sb.st_mode) && p != NULL) {
+ if (m) free(resolved);
+ errno = ENOTDIR;
+ return (NULL);
+ }
+ }
+
+ /*
+ * Remove trailing slash except when the resolved pathname
+ * is a single "/".
+ */
+ if (resolved_len > 1 && resolved[resolved_len - 1] == '/')
+ resolved[resolved_len - 1] = '\0';
+
+ /* Accumulate permissions. */
+ *perms |= policy->Lookup(resolved);
+
+ return (resolved);
+}
+
+} // namespace mozilla
diff --git a/security/sandbox/linux/broker/SandboxBrokerUtils.h b/security/sandbox/linux/broker/SandboxBrokerUtils.h
new file mode 100644
index 0000000000..89b028bece
--- /dev/null
+++ b/security/sandbox/linux/broker/SandboxBrokerUtils.h
@@ -0,0 +1,32 @@
+/* -*- 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/. */
+#ifndef mozilla_SandboxBrokerUtils_h
+#define mozilla_SandboxBrokerUtils_h
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include "sandbox/linux/system_headers/linux_syscalls.h"
+
+// On 32-bit Linux, stat calls are translated by libc into stat64
+// calls. We'll intercept those and handle them in the stat functions
+// but must be sure to use the right structure layout.
+
+#if defined(__NR_stat64) || defined(__NR_fstatat64)
+typedef struct stat64 statstruct;
+# define statsyscall stat64
+# define lstatsyscall lstat64
+# define fstatsyscall fstat64
+#elif defined(__NR_stat) || defined(__NR_newfstatat)
+typedef struct stat statstruct;
+# define statsyscall stat
+# define lstatsyscall lstat
+# define fstatsyscall fstat
+#else
+# error Missing stat syscall include.
+#endif
+
+#endif // mozilla_SandboxBrokerUtils_h
diff --git a/security/sandbox/linux/broker/moz.build b/security/sandbox/linux/broker/moz.build
new file mode 100644
index 0000000000..4ad2dfcb3b
--- /dev/null
+++ b/security/sandbox/linux/broker/moz.build
@@ -0,0 +1,37 @@
+# -*- Mode: python; python-indent: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+EXPORTS.mozilla += [
+ "SandboxBroker.h",
+ "SandboxBrokerCommon.h",
+ "SandboxBrokerPolicyFactory.h",
+]
+
+UNIFIED_SOURCES += [
+ "SandboxBroker.cpp",
+ "SandboxBrokerCommon.cpp",
+ "SandboxBrokerPolicyFactory.cpp",
+ "SandboxBrokerRealpath.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/security/sandbox/linux", # SandboxLogging.h, SandboxInfo.h
+]
+
+# Need this for mozilla::ipc::FileDescriptor etc.
+include("/ipc/chromium/chromium-config.mozbuild")
+
+# Need this for safe_sprintf.h used by SandboxLogging.h,
+# but it has to be after ipc/chromium/src.
+LOCAL_INCLUDES += [
+ "/security/sandbox/chromium",
+]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ CXXFLAGS += CONFIG["GLIB_CFLAGS"]
+ CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+
+FINAL_LIBRARY = "xul"