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