/* -*- 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 #ifdef XP_LINUX #include #endif #ifdef MOZ_WIDGET_GONK #include #include #endif #include "mozilla/Assertions.h" #include "mozilla/DebugOnly.h" #include "mozilla/Move.h" #include "mozilla/NullPtr.h" #include "mozilla/Sprintf.h" #include "mozilla/ipc/FileDescriptor.h" #include "sandbox/linux/system_headers/linux_syscalls.h" namespace mozilla { // This constructor signals failure by setting mFileDesc and aClientFd to -1. SandboxBroker::SandboxBroker(UniquePtr aPolicy, int aChildPid, int& aClientFd) : mChildPid(aChildPid), mPolicy(Move(aPolicy)) { int fds[2]; if (0 != socketpair(AF_UNIX, SOCK_SEQPACKET, 0, fds)) { SANDBOX_LOG_ERROR("SandboxBroker: socketpair failed: %s", strerror(errno)); mFileDesc = -1; aClientFd = -1; return; } mFileDesc = fds[0]; aClientFd = fds[1]; if (!PlatformThread::Create(0, this, &mThread)) { SANDBOX_LOG_ERROR("SandboxBroker: thread creation failed: %s", strerror(errno)); close(mFileDesc); close(aClientFd); mFileDesc = -1; aClientFd = -1; } } 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(Move(aPolicy), aChildPid, clientFd)); if (clientFd < 0) { rv = nullptr; } else { aClientFdOut = ipc::FileDescriptor(clientFd); } return Move(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() { } SandboxBroker::Policy::~Policy() { } SandboxBroker::Policy::Policy(const Policy& aOther) { for (auto iter = aOther.mMap.ConstIter(); !iter.Done(); iter.Next()) { mMap.Put(iter.Key(), iter.Data()); } } // 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); int perms; if (aCond == AddIfExistsNow) { struct stat statBuf; if (lstat(aPath, &statBuf) != 0) { return; } } if (!mMap.Get(path, &perms)) { perms = MAY_ACCESS; } else { MOZ_ASSERT(perms & MAY_ACCESS); } if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) { SANDBOX_LOG_ERROR("policy for %s: %d -> %d", aPath, perms, perms | aPerms); } perms |= aPerms; mMap.Put(path, perms); } 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; } nsDependentCString path(aPath); MOZ_ASSERT(path.Length() <= kMaxPathLen - 1); // Enforce trailing / on aPath if (path[path.Length() - 1] != '/') { path.Append('/'); } int origPerms; if (!mMap.Get(path, &origPerms)) { origPerms = MAY_ACCESS; } else { MOZ_ASSERT(origPerms & MAY_ACCESS); } int newPerms = origPerms | aPerms | RECURSIVE; if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) { SANDBOX_LOG_ERROR("policy for %s: %d -> %d", aPath, origPerms, newPerms); } mMap.Put(path, newPerms); } void SandboxBroker::Policy::AddPrefix(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 (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); } 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 (auto iter = mMap.ConstIter(); !iter.Done(); iter.Next()) { const nsACString& whiteListPath = iter.Key(); const int& perms = iter.Data(); 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 directories. 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|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_ASYNC | 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; } return (aPerms & needed) == needed; } static int DoStat(const char* aPath, void* aBuff, int aFlags) { if (aFlags & O_NOFOLLOW) { return lstatsyscall(aPath, (statstruct*)aBuff); } return statsyscall(aPath, (statstruct*)aBuff); } static int DoLink(const char* aPath, const char* aPath2, SandboxBrokerCommon::Operation aOper) { if (aOper == SandboxBrokerCommon::Operation::SANDBOX_FILE_LINK) { return link(aPath, aPath2); } else if (aOper == SandboxBrokerCommon::Operation::SANDBOX_FILE_SYMLINK) { return symlink(aPath, aPath2); } MOZ_CRASH("SandboxBroker: Unknown link operation"); } size_t SandboxBroker::ConvertToRealPath(char* aPath, size_t aBufSize, size_t aPathLen) { if (strstr(aPath, "..") != NULL) { char* result = realpath(aPath, NULL); if (result != NULL) { strncpy(aPath, result, aBufSize); aPath[aBufSize - 1] = '\0'; free(result); // Size changed, but guaranteed to be 0 terminated aPathLen = strlen(aPath); } // ValidatePath will handle failure to translate } return aPathLen; } void SandboxBroker::ThreadMain(void) { char threadName[16]; SprintfLiteral(threadName, "FS Broker %d", mChildPid); PlatformThread::SetName(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); #ifdef MOZ_WIDGET_GONK #ifdef __NR_setreuid32 static const long nr_setreuid = __NR_setreuid32; static const long nr_setregid = __NR_setregid32; #else static const long nr_setreuid = __NR_setreuid; static const long nr_setregid = __NR_setregid; #endif if (syscall(nr_setregid, getgid(), AID_APP + mChildPid) != 0 || syscall(nr_setreuid, getuid(), AID_APP + mChildPid) != 0) { MOZ_CRASH("SandboxBroker: failed to drop privileges"); } #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; size_t pathLen2; 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_ERROR("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_ERROR("bad read from pid %d: %s", mChildPid, strerror(errno)); shutdown(mFileDesc, SHUT_RD); break; } if (recvd < static_cast(sizeof(req))) { SANDBOX_LOG_ERROR("bad read from pid %d (%d < %d)", mChildPid, recvd, sizeof(req)); shutdown(mFileDesc, SHUT_RD); break; } if (respfd == -1) { SANDBOX_LOG_ERROR("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_ERROR("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 + 1); // First string is guaranteed to be 0-terminated. pathLen = first_len; // Look up the first pathname but first translate relative paths. pathLen = ConvertToRealPath(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 = ConvertToRealPath(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 (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)) { // This can't use access() itself because that uses the ruid // and not the euid. In theory faccessat() with AT_EACCESS // would work, but Linux doesn't actually implement the // flags != 0 case; glibc has a hack which doesn't even work // in this case so it'll ignore the flag, and Bionic just // passes through the syscall and always ignores the flags. // // Instead, because we've already checked the requested // r/w/x bits against the policy, just return success if the // file exists and hope that's close enough. if (stat(pathBuf, (struct stat*)&respBuf) == 0) { resp.mError = 0; } else { resp.mError = -errno; } } else { AuditDenial(req.mOp, req.mFlags, perms, pathBuf); } break; case SANDBOX_FILE_STAT: if (DoStat(pathBuf, (struct stat*)&respBuf, req.mFlags) == 0) { resp.mError = 0; ios[1].iov_base = &respBuf; ios[1].iov_len = req.mBufSize; } 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, 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, 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 { AuditDenial(req.mOp, req.mFlags, perms, pathBuf); } break; case SANDBOX_FILE_UNLINK: if (permissive || AllowOperation(W_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) { 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; } } else { MOZ_ASSERT(perms == 0); AuditDenial(req.mOp, req.mFlags, perms, pathBuf); } const size_t numIO = ios[1].iov_len > 0 ? 2 : 1; DebugOnly sent = SendWithFd(respfd, ios, numIO, openedFd); close(respfd); MOZ_ASSERT(sent < 0 || static_cast(sent) == ios[0].iov_len + ios[1].iov_len); 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_ERROR("SandboxBroker: would have denied op=%d rflags=%o perms=%d path=%s for pid=%d" \ " permissive=1 error=\"%s\"", aOp, aFlags, aPerms, aPath, mChildPid, strerror(errno)); } void SandboxBroker::AuditDenial(int aOp, int aFlags, int aPerms, const char* aPath) { if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) { SANDBOX_LOG_ERROR("SandboxBroker: denied op=%d rflags=%o perms=%d path=%s for pid=%d" \ " error=\"%s\"", aOp, aFlags, aPerms, aPath, mChildPid, strerror(errno)); } } } // namespace mozilla