diff options
Diffstat (limited to 'security/sandbox/linux/broker')
-rw-r--r-- | security/sandbox/linux/broker/SandboxBroker.cpp | 1097 | ||||
-rw-r--r-- | security/sandbox/linux/broker/SandboxBroker.h | 180 | ||||
-rw-r--r-- | security/sandbox/linux/broker/SandboxBrokerCommon.cpp | 155 | ||||
-rw-r--r-- | security/sandbox/linux/broker/SandboxBrokerCommon.h | 77 | ||||
-rw-r--r-- | security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp | 1017 | ||||
-rw-r--r-- | security/sandbox/linux/broker/SandboxBrokerPolicyFactory.h | 36 | ||||
-rw-r--r-- | security/sandbox/linux/broker/SandboxBrokerRealpath.cpp | 277 | ||||
-rw-r--r-- | security/sandbox/linux/broker/SandboxBrokerUtils.h | 32 | ||||
-rw-r--r-- | security/sandbox/linux/broker/moz.build | 37 |
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" |