/* -*- 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 #include #include #include #include #include #include #include #ifdef XP_LINUX # include #endif #include #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 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 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::Create( UniquePtr aPolicy, int aChildPid, ipc::FileDescriptor& aClientFdOut) { int clientFd; // Can't use MakeUnique here because the constructor is private. UniquePtr 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(&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 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(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(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); } } // Truncate the reply to the size of the client's // buffer, matching the real readlink()'s behavior in // that case, and being careful with the input data. ssize_t callerSize = std::max(AssertedCast(req.mBufSize), ssize_t(0)); respSize = std::min(respSize, callerSize); resp.mError = AssertedCast(respSize); ios[1].iov_base = &respBuf; ios[1].iov_len = ReleaseAssertedCast(respSize); MOZ_RELEASE_ASSERT(ios[1].iov_len <= sizeof(respBuf)); } 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(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(&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