From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- security/sandbox/linux/LinuxSched.h | 35 + security/sandbox/linux/Sandbox.cpp | 813 ++++++++ security/sandbox/linux/Sandbox.h | 76 + security/sandbox/linux/SandboxBrokerClient.cpp | 274 +++ security/sandbox/linux/SandboxBrokerClient.h | 57 + security/sandbox/linux/SandboxChrootProto.h | 21 + security/sandbox/linux/SandboxFilter.cpp | 2152 ++++++++++++++++++++ security/sandbox/linux/SandboxFilter.h | 46 + security/sandbox/linux/SandboxFilterUtil.cpp | 142 ++ security/sandbox/linux/SandboxFilterUtil.h | 248 +++ security/sandbox/linux/SandboxHooks.cpp | 110 + security/sandbox/linux/SandboxInfo.cpp | 201 ++ security/sandbox/linux/SandboxInfo.h | 70 + security/sandbox/linux/SandboxInternal.h | 28 + security/sandbox/linux/SandboxLogging.cpp | 148 ++ security/sandbox/linux/SandboxLogging.h | 81 + security/sandbox/linux/SandboxOpenedFiles.cpp | 77 + security/sandbox/linux/SandboxOpenedFiles.h | 97 + security/sandbox/linux/SandboxReporterClient.cpp | 88 + security/sandbox/linux/SandboxReporterClient.h | 47 + security/sandbox/linux/broker/SandboxBroker.cpp | 1104 ++++++++++ security/sandbox/linux/broker/SandboxBroker.h | 180 ++ .../sandbox/linux/broker/SandboxBrokerCommon.cpp | 159 ++ .../sandbox/linux/broker/SandboxBrokerCommon.h | 77 + .../linux/broker/SandboxBrokerPolicyFactory.cpp | 1083 ++++++++++ .../linux/broker/SandboxBrokerPolicyFactory.h | 36 + .../sandbox/linux/broker/SandboxBrokerRealpath.cpp | 277 +++ security/sandbox/linux/broker/SandboxBrokerUtils.h | 32 + security/sandbox/linux/broker/moz.build | 37 + security/sandbox/linux/glue/SandboxCrash.cpp | 118 ++ security/sandbox/linux/glue/SandboxPrefBridge.cpp | 50 + security/sandbox/linux/glue/moz.build | 35 + security/sandbox/linux/gtest/TestBroker.cpp | 689 +++++++ security/sandbox/linux/gtest/TestBrokerPolicy.cpp | 95 + security/sandbox/linux/gtest/TestLogging.cpp | 56 + security/sandbox/linux/gtest/moz.build | 26 + security/sandbox/linux/interfaces/moz.build | 11 + .../linux/interfaces/mozISandboxReporter.idl | 65 + .../sandbox/linux/launch/LinuxCapabilities.cpp | 26 + security/sandbox/linux/launch/LinuxCapabilities.h | 122 ++ security/sandbox/linux/launch/SandboxLaunch.cpp | 675 ++++++ security/sandbox/linux/launch/SandboxLaunch.h | 71 + security/sandbox/linux/launch/moz.build | 33 + security/sandbox/linux/moz.build | 139 ++ .../sandbox/linux/reporter/SandboxReporter.cpp | 299 +++ security/sandbox/linux/reporter/SandboxReporter.h | 86 + .../sandbox/linux/reporter/SandboxReporterCommon.h | 66 + .../linux/reporter/SandboxReporterWrappers.cpp | 199 ++ security/sandbox/linux/reporter/components.conf | 13 + security/sandbox/linux/reporter/moz.build | 34 + 50 files changed, 10704 insertions(+) create mode 100644 security/sandbox/linux/LinuxSched.h create mode 100644 security/sandbox/linux/Sandbox.cpp create mode 100644 security/sandbox/linux/Sandbox.h create mode 100644 security/sandbox/linux/SandboxBrokerClient.cpp create mode 100644 security/sandbox/linux/SandboxBrokerClient.h create mode 100644 security/sandbox/linux/SandboxChrootProto.h create mode 100644 security/sandbox/linux/SandboxFilter.cpp create mode 100644 security/sandbox/linux/SandboxFilter.h create mode 100644 security/sandbox/linux/SandboxFilterUtil.cpp create mode 100644 security/sandbox/linux/SandboxFilterUtil.h create mode 100644 security/sandbox/linux/SandboxHooks.cpp create mode 100644 security/sandbox/linux/SandboxInfo.cpp create mode 100644 security/sandbox/linux/SandboxInfo.h create mode 100644 security/sandbox/linux/SandboxInternal.h create mode 100644 security/sandbox/linux/SandboxLogging.cpp create mode 100644 security/sandbox/linux/SandboxLogging.h create mode 100644 security/sandbox/linux/SandboxOpenedFiles.cpp create mode 100644 security/sandbox/linux/SandboxOpenedFiles.h create mode 100644 security/sandbox/linux/SandboxReporterClient.cpp create mode 100644 security/sandbox/linux/SandboxReporterClient.h create mode 100644 security/sandbox/linux/broker/SandboxBroker.cpp create mode 100644 security/sandbox/linux/broker/SandboxBroker.h create mode 100644 security/sandbox/linux/broker/SandboxBrokerCommon.cpp create mode 100644 security/sandbox/linux/broker/SandboxBrokerCommon.h create mode 100644 security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp create mode 100644 security/sandbox/linux/broker/SandboxBrokerPolicyFactory.h create mode 100644 security/sandbox/linux/broker/SandboxBrokerRealpath.cpp create mode 100644 security/sandbox/linux/broker/SandboxBrokerUtils.h create mode 100644 security/sandbox/linux/broker/moz.build create mode 100644 security/sandbox/linux/glue/SandboxCrash.cpp create mode 100644 security/sandbox/linux/glue/SandboxPrefBridge.cpp create mode 100644 security/sandbox/linux/glue/moz.build create mode 100644 security/sandbox/linux/gtest/TestBroker.cpp create mode 100644 security/sandbox/linux/gtest/TestBrokerPolicy.cpp create mode 100644 security/sandbox/linux/gtest/TestLogging.cpp create mode 100644 security/sandbox/linux/gtest/moz.build create mode 100644 security/sandbox/linux/interfaces/moz.build create mode 100644 security/sandbox/linux/interfaces/mozISandboxReporter.idl create mode 100644 security/sandbox/linux/launch/LinuxCapabilities.cpp create mode 100644 security/sandbox/linux/launch/LinuxCapabilities.h create mode 100644 security/sandbox/linux/launch/SandboxLaunch.cpp create mode 100644 security/sandbox/linux/launch/SandboxLaunch.h create mode 100644 security/sandbox/linux/launch/moz.build create mode 100644 security/sandbox/linux/moz.build create mode 100644 security/sandbox/linux/reporter/SandboxReporter.cpp create mode 100644 security/sandbox/linux/reporter/SandboxReporter.h create mode 100644 security/sandbox/linux/reporter/SandboxReporterCommon.h create mode 100644 security/sandbox/linux/reporter/SandboxReporterWrappers.cpp create mode 100644 security/sandbox/linux/reporter/components.conf create mode 100644 security/sandbox/linux/reporter/moz.build (limited to 'security/sandbox/linux') diff --git a/security/sandbox/linux/LinuxSched.h b/security/sandbox/linux/LinuxSched.h new file mode 100644 index 0000000000..3ec34df670 --- /dev/null +++ b/security/sandbox/linux/LinuxSched.h @@ -0,0 +1,35 @@ +/* -*- 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_LinuxSched_h +#define mozilla_LinuxSched_h + +#include + +// Some build environments, in particular the Android NDK, don't +// define some of the newer clone/unshare flags ("newer" relatively +// speaking; CLONE_NEWUTS is present since kernel 2.6.19 in 2006). + +#ifndef CLONE_NEWUTS +# define CLONE_NEWUTS 0x04000000 +#endif +#ifndef CLONE_NEWIPC +# define CLONE_NEWIPC 0x08000000 +#endif +#ifndef CLONE_NEWUSER +# define CLONE_NEWUSER 0x10000000 +#endif +#ifndef CLONE_NEWPID +# define CLONE_NEWPID 0x20000000 +#endif +#ifndef CLONE_NEWNET +# define CLONE_NEWNET 0x40000000 +#endif +#ifndef CLONE_IO +# define CLONE_IO 0x80000000 +#endif + +#endif diff --git a/security/sandbox/linux/Sandbox.cpp b/security/sandbox/linux/Sandbox.cpp new file mode 100644 index 0000000000..9b1196bc5f --- /dev/null +++ b/security/sandbox/linux/Sandbox.cpp @@ -0,0 +1,813 @@ +/* -*- 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 "Sandbox.h" + +#include "LinuxSched.h" +#include "SandboxBrokerClient.h" +#include "SandboxChrootProto.h" +#include "SandboxFilter.h" +#include "SandboxInternal.h" +#include "SandboxLogging.h" +#include "SandboxOpenedFiles.h" +#include "SandboxReporterClient.h" + +#include +#ifdef NIGHTLY_BUILD +# include "dlfcn.h" +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mozilla/Array.h" +#include "mozilla/Atomics.h" +#include "mozilla/Attributes.h" +#include "mozilla/Range.h" +#include "mozilla/SandboxInfo.h" +#include "mozilla/StackWalk.h" +#include "mozilla/Span.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/ipc/UtilityProcessSandboxing.h" +#include "prenv.h" +#include "base/posix/eintr_wrapper.h" +#include "sandbox/linux/bpf_dsl/bpf_dsl.h" +#include "sandbox/linux/bpf_dsl/codegen.h" +#include "sandbox/linux/bpf_dsl/dump_bpf.h" +#include "sandbox/linux/bpf_dsl/policy.h" +#include "sandbox/linux/bpf_dsl/policy_compiler.h" +#include "sandbox/linux/bpf_dsl/seccomp_macros.h" +#include "sandbox/linux/seccomp-bpf/trap.h" +#include "sandbox/linux/system_headers/linux_filter.h" +#include "sandbox/linux/system_headers/linux_seccomp.h" +#include "sandbox/linux/system_headers/linux_syscalls.h" +#if defined(ANDROID) +# include "sandbox/linux/system_headers/linux_ucontext.h" +#endif + +#ifndef SECCOMP_FILTER_FLAG_SPEC_ALLOW +# define SECCOMP_FILTER_FLAG_SPEC_ALLOW (1UL << 2) +#endif + +#ifdef MOZ_ASAN +// Copy libsanitizer declarations to avoid depending on ASAN headers. +// See also bug 1081242 comment #4. +extern "C" { +namespace __sanitizer { +// Win64 uses long long, but this is Linux. +typedef signed long sptr; +} // namespace __sanitizer + +typedef struct { + int coverage_sandboxed; + __sanitizer::sptr coverage_fd; + unsigned int coverage_max_block_size; +} __sanitizer_sandbox_arguments; + +MOZ_IMPORT_API void __sanitizer_sandbox_on_notify( + __sanitizer_sandbox_arguments* args); +} // extern "C" +#endif // MOZ_ASAN + +// Signal number used to enable seccomp on each thread. +mozilla::Atomic gSeccompTsyncBroadcastSignum(0); + +namespace mozilla { + +static mozilla::Atomic gSandboxCrashOnError(false); + +// This is initialized by SandboxSetCrashFunc(). +SandboxCrashFunc gSandboxCrashFunc; + +static SandboxReporterClient* gSandboxReporterClient; +static void (*gChromiumSigSysHandler)(int, siginfo_t*, void*); + +// Test whether a ucontext, interpreted as the state after a syscall, +// indicates the given error. See also sandbox::Syscall::PutValueInUcontext. +static bool ContextIsError(const ucontext_t* aContext, int aError) { + // Avoid integer promotion warnings. (The unary addition makes + // the decltype not evaluate to a reference type.) + typedef decltype(+SECCOMP_RESULT(aContext)) reg_t; + +#ifdef __mips__ + return SECCOMP_PARM4(aContext) != 0 && + SECCOMP_RESULT(aContext) == static_cast(aError); +#else + return SECCOMP_RESULT(aContext) == static_cast(-aError); +#endif +} + +/** + * This is the SIGSYS handler function. It delegates to the Chromium + * TrapRegistry handler (see InstallSigSysHandler, below) and, if the + * trap handler installed by the policy would fail with ENOSYS, + * crashes the process. This allows unintentional policy failures to + * be reported as crash dumps and fixed. It also logs information + * about the failed system call. + * + * Note that this could be invoked in parallel on multiple threads and + * that it could be in async signal context (e.g., intercepting an + * open() called from an async signal handler). + */ +MOZ_NEVER_INLINE static void SigSysHandler(int nr, siginfo_t* info, + void* void_context) { + ucontext_t* ctx = static_cast(void_context); + // This shouldn't ever be null, but the Chromium handler checks for + // that and refrains from crashing, so let's not crash release builds: + MOZ_DIAGNOSTIC_ASSERT(ctx); + if (!ctx) { + return; + } + + // Save a copy of the context before invoking the trap handler, + // which will overwrite one or more registers with the return value. + ucontext_t savedCtx = *ctx; + + gChromiumSigSysHandler(nr, info, ctx); + if (!ContextIsError(ctx, ENOSYS)) { + return; + } + + SandboxReport report = gSandboxReporterClient->MakeReportAndSend(&savedCtx); + + // TODO, someday when this is enabled on MIPS: include the two extra + // args in the error message. + SANDBOX_LOG( + "seccomp sandbox violation: pid %d, tid %d, syscall %d," + " args %d %d %d %d %d %d.%s", + report.mPid, report.mTid, report.mSyscall, report.mArgs[0], + report.mArgs[1], report.mArgs[2], report.mArgs[3], report.mArgs[4], + report.mArgs[5], gSandboxCrashOnError ? " Killing process." : ""); + + if (gSandboxCrashOnError) { + // Bug 1017393: record syscall number somewhere useful. + info->si_addr = reinterpret_cast(report.mSyscall); + + gSandboxCrashFunc(nr, info, &savedCtx, CallerPC()); + _exit(127); + } +} + +/** + * This function installs the SIGSYS handler. This is slightly + * complicated because we want to use Chromium's handler to dispatch + * to specific trap handlers defined in the policy, but we also need + * the full original signal context to give to Breakpad for crash + * dumps. So we install Chromium's handler first, then retrieve its + * address so our replacement can delegate to it. + */ +static void InstallSigSysHandler(void) { + struct sigaction act; + + // Ensure that the Chromium handler is installed. + Unused << sandbox::Trap::Registry(); + + // If the signal handling state isn't as expected, crash now instead + // of crashing later (and more confusingly) when SIGSYS happens. + + if (sigaction(SIGSYS, nullptr, &act) != 0) { + MOZ_CRASH("Couldn't read old SIGSYS disposition"); + } + if ((act.sa_flags & SA_SIGINFO) != SA_SIGINFO) { + MOZ_CRASH("SIGSYS not already set to a siginfo handler?"); + } + MOZ_RELEASE_ASSERT(act.sa_sigaction); + gChromiumSigSysHandler = act.sa_sigaction; + act.sa_sigaction = SigSysHandler; + // Currently, SA_NODEFER should already be set by the Chromium code, + // but it's harmless to ensure that it's set: + MOZ_ASSERT(act.sa_flags & SA_NODEFER); + act.sa_flags |= SA_NODEFER; + if (sigaction(SIGSYS, &act, nullptr) < 0) { + MOZ_CRASH("Couldn't change SIGSYS disposition"); + } +} + +/** + * This function installs the syscall filter, a.k.a. seccomp. The + * aUseTSync flag indicates whether this should apply to all threads + * in the process -- which will fail if the kernel doesn't support + * that -- or only the current thread. + * + * SECCOMP_MODE_FILTER is the "bpf" mode of seccomp which allows + * to pass a bpf program (in our case, it contains a syscall + * whitelist). + * + * PR_SET_NO_NEW_PRIVS ensures that it is impossible to grant more + * syscalls to the process beyond this point (even after fork()), and + * prevents gaining capabilities (e.g., by exec'ing a setuid root + * program). The kernel won't allow seccomp-bpf without doing this, + * because otherwise it could be used for privilege escalation attacks. + * + * Returns false if the filter was already installed (see the + * PR_SET_NO_NEW_PRIVS rule in SandboxFilter.cpp). Crashes on any + * other error condition. + * + * @see SandboxInfo + * @see BroadcastSetThreadSandbox + */ +[[nodiscard]] static bool InstallSyscallFilter(const sock_fprog* aProg, + bool aUseTSync) { + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { + if (!aUseTSync && errno == ETXTBSY) { + return false; + } + SANDBOX_LOG_ERRNO("prctl(PR_SET_NO_NEW_PRIVS) failed"); + MOZ_CRASH("prctl(PR_SET_NO_NEW_PRIVS)"); + } + + if (aUseTSync) { + // Try with SECCOMP_FILTER_FLAGS_SPEC_ALLOW, and then without if + // that fails with EINVAL (or if an env var is set, for testing + // purposes). + // + // Context: Linux 4.17 applied some Spectre mitigations (SSBD and + // STIBP) by default when seccomp-bpf is used, but also added that + // flag to opt out (and also sysadmin-level overrides). Later, + // Linux 5.16 turned them off by default; the rationale seems to + // be, roughly: the attacks are impractical or were already + // mitigated in other ways, there are worse attacks that these + // measures don't stop, and the performance impact is severe + // enough that container software was already opting out. + // + // For the full rationale, see + // https://github.com/torvalds/linux/commit/2f46993d83ff4abb310e + // + // In our case, STIBP causes a noticeable performance hit: WASM + // microbenchmarks of indirect calls regress by up to 2x or 3x + // depending on CPU. Given that upstream Linux has changed the + // default years ago, we opt out. + + static const bool kSpecAllow = !PR_GetEnv("MOZ_SANDBOX_NO_SPEC_ALLOW"); + + const auto setSeccomp = [aProg](int aFlags) -> long { + return syscall(__NR_seccomp, SECCOMP_SET_MODE_FILTER, + SECCOMP_FILTER_FLAG_TSYNC | aFlags, aProg); + }; + + long rv; + if (kSpecAllow) { + rv = setSeccomp(SECCOMP_FILTER_FLAG_SPEC_ALLOW); + } else { + rv = -1; + errno = EINVAL; + } + if (rv != 0 && errno == EINVAL) { + rv = setSeccomp(0); + } + if (rv != 0) { + SANDBOX_LOG_ERRNO("thread-synchronized seccomp failed"); + MOZ_CRASH("seccomp+tsync failed, but kernel supports tsync"); + } + } else { + if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, (unsigned long)aProg, 0, + 0)) { + SANDBOX_LOG_ERRNO("prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER) failed"); + MOZ_CRASH("prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER)"); + } + } + return true; +} + +// Use signals for permissions that need to be set per-thread. +// The communication channel from the signal handler back to the main thread. +static mozilla::Atomic gSetSandboxDone; +// Pass the filter itself through a global. +const sock_fprog* gSetSandboxFilter; + +// We have to dynamically allocate the signal number; see bug 1038900. +// This function returns the first realtime signal currently set to +// default handling (i.e., not in use), or 0 if none could be found. +// +// WARNING: if this function or anything similar to it (including in +// external libraries) is used on multiple threads concurrently, there +// will be a race condition. +static int FindFreeSignalNumber() { + for (int signum = SIGRTMAX; signum >= SIGRTMIN; --signum) { + struct sigaction sa; + + if (sigaction(signum, nullptr, &sa) == 0 && + (sa.sa_flags & SA_SIGINFO) == 0 && sa.sa_handler == SIG_DFL) { + return signum; + } + } + return 0; +} + +// Returns true if sandboxing was enabled, or false if sandboxing +// already was enabled. Crashes if sandboxing could not be enabled. +static bool SetThreadSandbox() { + return InstallSyscallFilter(gSetSandboxFilter, false); +} + +static void SetThreadSandboxHandler(int signum) { + // The non-zero number sent back to the main thread indicates + // whether action was taken. + if (SetThreadSandbox()) { + gSetSandboxDone = 2; + } else { + gSetSandboxDone = 1; + } + // Wake up the main thread. See the FUTEX_WAIT call, below, for an + // explanation. + syscall(__NR_futex, reinterpret_cast(&gSetSandboxDone), FUTEX_WAKE, 1); +} + +static void EnterChroot() { + const char* env = PR_GetEnv(kSandboxChrootEnvFlag); + if (!env || !*env || *env == '0') { + return; + } + char msg = kSandboxChrootRequest; + ssize_t msg_len = HANDLE_EINTR(write(kSandboxChrootClientFd, &msg, 1)); + MOZ_RELEASE_ASSERT(msg_len == 1); + msg_len = HANDLE_EINTR(read(kSandboxChrootClientFd, &msg, 1)); + MOZ_RELEASE_ASSERT(msg_len == 1); + MOZ_RELEASE_ASSERT(msg == kSandboxChrootResponse); + close(kSandboxChrootClientFd); +} + +static void BroadcastSetThreadSandbox(const sock_fprog* aFilter) { + pid_t pid, tid, myTid; + DIR* taskdp; + struct dirent* de; + + // This function does not own *aFilter, so this global needs to + // always be zeroed before returning. + gSetSandboxFilter = aFilter; + + static_assert(sizeof(mozilla::Atomic) == sizeof(int), + "mozilla::Atomic isn't represented by an int"); + pid = getpid(); + myTid = syscall(__NR_gettid); + taskdp = opendir("/proc/self/task"); + if (taskdp == nullptr) { + SANDBOX_LOG_ERRNO("opendir /proc/self/task"); + MOZ_CRASH("failed while trying to open directory /proc/self/task"); + } + + // In case this races with a not-yet-deprivileged thread cloning + // itself, repeat iterating over all threads until we find none + // that are still privileged. + bool sandboxProgress; + const int tsyncSignum = gSeccompTsyncBroadcastSignum; + do { + sandboxProgress = false; + // For each thread... + while ((de = readdir(taskdp))) { + char* endptr; + tid = strtol(de->d_name, &endptr, 10); + if (*endptr != '\0' || tid <= 0) { + // Not a task ID. + continue; + } + if (tid == myTid) { + // Drop this thread's privileges last, below, so we can + // continue to signal other threads. + continue; + } + + MOZ_RELEASE_ASSERT(tsyncSignum != 0); + + // Reset the futex cell and signal. + gSetSandboxDone = 0; + if (syscall(__NR_tgkill, pid, tid, tsyncSignum) != 0) { + if (errno == ESRCH) { + SANDBOX_LOG("Thread %d unexpectedly exited.", tid); + // Rescan threads, in case it forked before exiting. + sandboxProgress = true; + continue; + } + SANDBOX_LOG_ERRNO("tgkill(%d,%d)", pid, tid); + MOZ_CRASH("failed while trying to send a signal to a thread"); + } + // It's unlikely, but if the thread somehow manages to exit + // after receiving the signal but before entering the signal + // handler, we need to avoid blocking forever. + // + // Using futex directly lets the signal handler send the wakeup + // from an async signal handler (pthread mutex/condvar calls + // aren't allowed), and to use a relative timeout that isn't + // affected by changes to the system clock (not possible with + // POSIX semaphores). + // + // If a thread doesn't respond within a reasonable amount of + // time, but still exists, we crash -- the alternative is either + // blocking forever or silently losing security, and it + // shouldn't actually happen. + static const int crashDelay = 10; // seconds + struct timespec timeLimit; + clock_gettime(CLOCK_MONOTONIC, &timeLimit); + timeLimit.tv_sec += crashDelay; + while (true) { + static const struct timespec futexTimeout = {0, + 10 * 1000 * 1000}; // 10ms + // Atomically: if gSetSandboxDone == 0, then sleep. + if (syscall(__NR_futex, reinterpret_cast(&gSetSandboxDone), + FUTEX_WAIT, 0, &futexTimeout) != 0) { + if (errno != EWOULDBLOCK && errno != ETIMEDOUT && errno != EINTR) { + SANDBOX_LOG_ERRNO("FUTEX_WAIT"); + MOZ_CRASH("failed during FUTEX_WAIT"); + } + } + // Did the handler finish? + if (gSetSandboxDone > 0) { + if (gSetSandboxDone == 2) { + sandboxProgress = true; + } + break; + } + // Has the thread ceased to exist? + if (syscall(__NR_tgkill, pid, tid, 0) != 0) { + if (errno == ESRCH) { + SANDBOX_LOG("Thread %d unexpectedly exited.", tid); + } + // Rescan threads, in case it forked before exiting. + // Also, if it somehow failed in a way that wasn't ESRCH, + // and still exists, that will be handled on the next pass. + sandboxProgress = true; + break; + } + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + if (now.tv_sec > timeLimit.tv_sec || + (now.tv_sec == timeLimit.tv_sec && + now.tv_nsec > timeLimit.tv_nsec)) { + SANDBOX_LOG( + "Thread %d unresponsive for %d seconds." + " Killing process.", + tid, crashDelay); + + MOZ_CRASH("failed while waiting for unresponsive thread"); + } + } + } + rewinddir(taskdp); + } while (sandboxProgress); + + void (*oldHandler)(int); + oldHandler = signal(tsyncSignum, SIG_DFL); + if (oldHandler != SetThreadSandboxHandler) { + // See the comment on FindFreeSignalNumber about race conditions. + SANDBOX_LOG("handler for signal %d was changed to %p!", tsyncSignum, + oldHandler); + MOZ_CRASH("handler for the signal was changed to another"); + } + gSeccompTsyncBroadcastSignum = 0; + Unused << closedir(taskdp); + // And now, deprivilege the main thread: + SetThreadSandbox(); + gSetSandboxFilter = nullptr; +} + +static void ApplySandboxWithTSync(sock_fprog* aFilter) { + // At this point we're committed to using tsync, because we'd have + // needed to allocate a signal and prevent it from being blocked on + // other threads (see SandboxHooks.cpp), so there's no attempt to + // fall back to the non-tsync path. + if (!InstallSyscallFilter(aFilter, true)) { + MOZ_CRASH("failed while trying to install syscall filter"); + } +} + +#ifdef NIGHTLY_BUILD +static bool IsLibPresent(const char* aName) { + if (const auto handle = dlopen(aName, RTLD_LAZY | RTLD_NOLOAD)) { + dlclose(handle); + return true; + } + return false; +} + +static const Array kLibsThatWillCrash{ + "libesets_pac.so", +}; +#endif // NIGHTLY_BUILD + +void SandboxEarlyInit() { + if (PR_GetEnv("MOZ_SANDBOXED") == nullptr) { + return; + } + + // Fix LD_PRELOAD for any child processes. See bug 1434392 comment #10; + // this can probably go away when audio remoting is mandatory. + const char* oldPreload = PR_GetEnv("MOZ_ORIG_LD_PRELOAD"); + char* preloadEntry; + // This string is "leaked" because the environment takes ownership. + if (asprintf(&preloadEntry, "LD_PRELOAD=%s", oldPreload ? oldPreload : "") != + -1) { + PR_SetEnv(preloadEntry); + } + + // If TSYNC is not supported, set up signal handler + // used to enable seccomp on each thread. + if (!SandboxInfo::Get().Test(SandboxInfo::kHasSeccompTSync)) { + // The signal number has to be chosen early, so that the + // interceptions in SandboxHooks.cpp can prevent it from being + // masked. + const int tsyncSignum = FindFreeSignalNumber(); + if (tsyncSignum == 0) { + SANDBOX_LOG("No available signal numbers!"); + MOZ_CRASH("failed while trying to find a free signal number"); + } + gSeccompTsyncBroadcastSignum = tsyncSignum; + + // ...and the signal handler also needs to be installed now, to + // indicate to anything else looking for free signals that it's + // claimed. + void (*oldHandler)(int); + oldHandler = signal(tsyncSignum, SetThreadSandboxHandler); + if (oldHandler != SIG_DFL) { + // See the comment on FindFreeSignalNumber about race conditions. + if (oldHandler == SIG_ERR) { + MOZ_CRASH("failed while registering the signal handler"); + } else { + MOZ_CRASH("failed because the signal is in use by another handler"); + } + SANDBOX_LOG("signal %d in use by handler %p!\n", tsyncSignum, oldHandler); + } + } +} + +static void RunGlibcLazyInitializers() { + // Make glibc's lazy initialization of shm_open() run before sandboxing + int fd = shm_open("/dummy", O_RDONLY, 0); + if (fd > 0) { + close(fd); // In the unlikely case we actually opened something + } +} + +static void SandboxLateInit() { +#ifdef NIGHTLY_BUILD + gSandboxCrashOnError = true; + for (const char* name : kLibsThatWillCrash) { + if (IsLibPresent(name)) { + gSandboxCrashOnError = false; + break; + } + } +#endif + + if (const char* envVar = PR_GetEnv("MOZ_SANDBOX_CRASH_ON_ERROR")) { + if (envVar[0]) { + gSandboxCrashOnError = envVar[0] != '0'; + } + } + + RunGlibcLazyInitializers(); +} + +// Common code for sandbox startup. +static void SetCurrentProcessSandbox( + UniquePtr aPolicy) { + MOZ_ASSERT(gSandboxCrashFunc); + MOZ_RELEASE_ASSERT(gSandboxReporterClient != nullptr); + SandboxLateInit(); + + // Auto-collect child processes -- mainly the chroot helper if + // present, but also anything setns()ed into the pid namespace (not + // yet implemented). This process won't be able to waitpid them + // after the seccomp-bpf policy is applied. + signal(SIGCHLD, SIG_IGN); + + // Note: PolicyCompiler borrows the policy and registry for its + // lifetime, but does not take ownership of them. + sandbox::bpf_dsl::PolicyCompiler compiler(aPolicy.get(), + sandbox::Trap::Registry()); + + // In case of errors detected by the compiler (like ABI violations), + // log and crash normally; the default is SECCOMP_RET_KILL_THREAD, + // which results in hard-to-debug hangs. + compiler.SetPanicFunc([](const char* error) -> sandbox::bpf_dsl::ResultExpr { + // Note: this assumes that `error` is a string literal, which is + // currently the case for all callers. (An intentionally leaked + // heap allocation would also work.) + return sandbox::bpf_dsl::Trap( + [](const sandbox::arch_seccomp_data&, void* aux) -> intptr_t { + auto error = reinterpret_cast(aux); + SANDBOX_LOG("Panic: %s", error); + MOZ_CRASH("Sandbox Panic"); + // unreachable + }, + (void*)error); + }); + + sandbox::CodeGen::Program program = compiler.Compile(); + if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) { + sandbox::bpf_dsl::DumpBPF::PrintProgram(program); + } + + InstallSigSysHandler(); + +#ifdef MOZ_ASAN + __sanitizer_sandbox_arguments asanArgs; + asanArgs.coverage_sandboxed = 1; + asanArgs.coverage_fd = -1; + asanArgs.coverage_max_block_size = 0; + __sanitizer_sandbox_on_notify(&asanArgs); +#endif + + // The syscall takes a C-style array, so copy the vector into one. + size_t programLen = program.size(); + UniquePtr flatProgram(new sock_filter[programLen]); + for (auto i = program.begin(); i != program.end(); ++i) { + flatProgram[i - program.begin()] = *i; + } + + sock_fprog fprog; + fprog.filter = flatProgram.get(); + fprog.len = static_cast(programLen); + MOZ_RELEASE_ASSERT(static_cast(fprog.len) == programLen); + + const SandboxInfo info = SandboxInfo::Get(); + if (info.Test(SandboxInfo::kHasSeccompTSync)) { + if (info.Test(SandboxInfo::kVerbose)) { + SANDBOX_LOG("using seccomp tsync"); + } + ApplySandboxWithTSync(&fprog); + } else { + if (info.Test(SandboxInfo::kVerbose)) { + SANDBOX_LOG("no tsync support; using signal broadcast"); + } + BroadcastSetThreadSandbox(&fprog); + } + + // Now that all threads' filesystem accesses are being intercepted + // (if a broker is used) it's safe to chroot the process: + EnterChroot(); +} + +/** + * Starts the seccomp sandbox for a content process. Should be called + * only once, and before any potentially harmful content is loaded. + * + * Will normally make the process exit on failure. + */ +bool SetContentProcessSandbox(ContentProcessSandboxParams&& aParams) { + int brokerFd = aParams.mBrokerFd; + aParams.mBrokerFd = -1; + + if (!SandboxInfo::Get().Test(SandboxInfo::kEnabledForContent)) { + if (brokerFd >= 0) { + close(brokerFd); + } + return false; + } + + auto procType = aParams.mFileProcess ? SandboxReport::ProcType::FILE + : SandboxReport::ProcType::CONTENT; + gSandboxReporterClient = new SandboxReporterClient(procType); + + // This needs to live until the process exits. + static SandboxBrokerClient* sBroker; + if (brokerFd >= 0) { + sBroker = new SandboxBrokerClient(brokerFd); + } + + SetCurrentProcessSandbox( + GetContentSandboxPolicy(sBroker, std::move(aParams))); + return true; +} +/** + * Starts the seccomp sandbox for a media plugin process. Should be + * called only once, and before any potentially harmful content is + * loaded -- including the plugin itself, if it's considered untrusted. + * + * The file indicated by aFilePath, if non-null, can be open()ed + * read-only, once, after the sandbox starts; it should be the .so + * file implementing the not-yet-loaded plugin. + * + * Will normally make the process exit on failure. + */ +void SetMediaPluginSandbox(const char* aFilePath) { + MOZ_RELEASE_ASSERT(aFilePath != nullptr); + if (!SandboxInfo::Get().Test(SandboxInfo::kEnabledForMedia)) { + return; + } + + gSandboxReporterClient = + new SandboxReporterClient(SandboxReport::ProcType::MEDIA_PLUGIN); + + SandboxOpenedFile plugin(aFilePath); + if (!plugin.IsOpen()) { + SANDBOX_LOG_ERRNO("failed to open plugin file %s", aFilePath); + MOZ_CRASH("failed while trying to open the plugin file "); + } + + auto files = new SandboxOpenedFiles(); + files->Add(std::move(plugin)); + files->Add("/dev/urandom", SandboxOpenedFile::Dup::YES); + files->Add("/dev/random", SandboxOpenedFile::Dup::YES); + files->Add("/etc/ld.so.cache"); // Needed for NSS in clearkey. + files->Add("/sys/devices/system/cpu/cpu0/tsc_freq_khz"); + files->Add("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq"); + files->Add("/proc/cpuinfo"); // Info also available via CPUID instruction. + files->Add("/proc/sys/crypto/fips_enabled"); // Needed for NSS in clearkey. +#ifdef __i386__ + files->Add("/proc/self/auxv"); // Info also in process's address space. +#endif + // Bug 1712506: the Widevine CDM will try to access these but + // doesn't appear to need them. + files->Add("/sys/devices/system/cpu/online", SandboxOpenedFile::Error{}); + files->Add("/proc/stat", SandboxOpenedFile::Error{}); + files->Add("/proc/net/unix", SandboxOpenedFile::Error{}); + files->Add("/proc/self/maps", SandboxOpenedFile::Error{}); + + // Finally, start the sandbox. + SetCurrentProcessSandbox(GetMediaSandboxPolicy(files)); +} + +void SetRemoteDataDecoderSandbox(int aBroker) { + if (!SandboxInfo::Get().Test(SandboxInfo::kHasSeccompBPF) || + PR_GetEnv("MOZ_DISABLE_RDD_SANDBOX")) { + if (aBroker >= 0) { + close(aBroker); + } + return; + } + + gSandboxReporterClient = + new SandboxReporterClient(SandboxReport::ProcType::RDD); + + // FIXME(bug 1513773): merge this with the one for content? + static SandboxBrokerClient* sBroker; + if (aBroker >= 0) { + sBroker = new SandboxBrokerClient(aBroker); + } + + SetCurrentProcessSandbox(GetDecoderSandboxPolicy(sBroker)); +} + +void SetSocketProcessSandbox(int aBroker) { + if (!SandboxInfo::Get().Test(SandboxInfo::kHasSeccompBPF) || + PR_GetEnv("MOZ_DISABLE_SOCKET_PROCESS_SANDBOX")) { + if (aBroker >= 0) { + close(aBroker); + } + return; + } + + gSandboxReporterClient = + new SandboxReporterClient(SandboxReport::ProcType::SOCKET_PROCESS); + + static SandboxBrokerClient* sBroker; + if (aBroker >= 0) { + sBroker = new SandboxBrokerClient(aBroker); + } + + SetCurrentProcessSandbox(GetSocketProcessSandboxPolicy(sBroker)); +} + +void SetUtilitySandbox(int aBroker, ipc::SandboxingKind aKind) { + if (!SandboxInfo::Get().Test(SandboxInfo::kHasSeccompBPF) || + !IsUtilitySandboxEnabled(aKind)) { + if (aBroker >= 0) { + close(aBroker); + } + return; + } + + gSandboxReporterClient = + new SandboxReporterClient(SandboxReport::ProcType::UTILITY); + + static SandboxBrokerClient* sBroker; + if (aBroker >= 0) { + sBroker = new SandboxBrokerClient(aBroker); + } + + UniquePtr policy; + switch (aKind) { + case ipc::SandboxingKind::GENERIC_UTILITY: + policy = GetUtilitySandboxPolicy(sBroker); + break; + + default: + MOZ_ASSERT(false, "Invalid SandboxingKind"); + break; + } + + SetCurrentProcessSandbox(std::move(policy)); +} + +bool SetSandboxCrashOnError(bool aValue) { + bool oldValue = gSandboxCrashOnError; + gSandboxCrashOnError = aValue; + return oldValue; +} + +} // namespace mozilla diff --git a/security/sandbox/linux/Sandbox.h b/security/sandbox/linux/Sandbox.h new file mode 100644 index 0000000000..575f57a3a3 --- /dev/null +++ b/security/sandbox/linux/Sandbox.h @@ -0,0 +1,76 @@ +/* -*- 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_Sandbox_h +#define mozilla_Sandbox_h + +#include "mozilla/Maybe.h" +#include "mozilla/Types.h" +#include "nsXULAppAPI.h" +#include + +#include "mozilla/ipc/UtilityProcessSandboxing.h" + +// This defines the entry points for a content process to start +// sandboxing itself. See also SandboxInfo.h for what parts of +// sandboxing are enabled/supported. + +namespace mozilla { + +namespace ipc { +class FileDescriptor; +} // namespace ipc + +// This must be called early, before glib creates any worker threads. +// (See bug 1176099.) +MOZ_EXPORT void SandboxEarlyInit(); + +// A collection of sandbox parameters that have to be extracted from +// prefs or other libxul facilities and passed down, because +// libmozsandbox can't link against the APIs to read them. +struct ContentProcessSandboxParams { + // Content sandbox level; see also GetEffectiveSandboxLevel in + // SandboxSettings.h and the comments for the Linux version of + // "security.sandbox.content.level" in browser/app/profile/firefox.js + int mLevel = 0; + // The filesystem broker client file descriptor, or -1 to allow + // direct filesystem access. (Warning: this is not a RAII class and + // will not close the fd on destruction.) + int mBrokerFd = -1; + // Determines whether we allow reading all files, for processes that + // render file:/// URLs. + bool mFileProcess = false; + // Syscall numbers to allow even if the seccomp-bpf policy otherwise + // wouldn't. + std::vector mSyscallWhitelist; + + static ContentProcessSandboxParams ForThisProcess( + const Maybe& aBroker); +}; + +// Call only if SandboxInfo::CanSandboxContent() returns true. +// (No-op if the sandbox is disabled.) +// isFileProcess determines whether we allow system wide file reads. +MOZ_EXPORT bool SetContentProcessSandbox(ContentProcessSandboxParams&& aParams); + +// Call only if SandboxInfo::CanSandboxMedia() returns true. +// (No-op if MOZ_DISABLE_GMP_SANDBOX is set.) +// aFilePath is the path to the plugin file. +MOZ_EXPORT void SetMediaPluginSandbox(const char* aFilePath); + +MOZ_EXPORT void SetRemoteDataDecoderSandbox(int aBroker); + +MOZ_EXPORT void SetSocketProcessSandbox(int aBroker); + +MOZ_EXPORT void SetUtilitySandbox(int aBroker, ipc::SandboxingKind aKind); + +// We want to turn on/off crashing on error when running some tests +// This will return current value and set the aValue we pass +MOZ_EXPORT bool SetSandboxCrashOnError(bool aValue); + +} // namespace mozilla + +#endif // mozilla_Sandbox_h diff --git a/security/sandbox/linux/SandboxBrokerClient.cpp b/security/sandbox/linux/SandboxBrokerClient.cpp new file mode 100644 index 0000000000..8dd8a2f5cf --- /dev/null +++ b/security/sandbox/linux/SandboxBrokerClient.cpp @@ -0,0 +1,274 @@ +/* -*- 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 "SandboxBrokerClient.h" +#include "SandboxInfo.h" +#include "SandboxLogging.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mozilla/Assertions.h" +#include "base/strings/safe_sprintf.h" + +namespace mozilla { + +SandboxBrokerClient::SandboxBrokerClient(int aFd) : mFileDesc(aFd) {} + +SandboxBrokerClient::~SandboxBrokerClient() { close(mFileDesc); } + +int SandboxBrokerClient::DoCall(const Request* aReq, const char* aPath, + const char* aPath2, void* aResponseBuff, + bool expectFd) { + // Remap /proc/self to the actual pid, so that the broker can open + // it. This happens here instead of in the broker to follow the + // principle of least privilege and keep the broker as simple as + // possible. (Note: when pid namespaces happen, this will also need + // to remap the inner pid to the outer pid.) + // We only remap the first path. + static const char kProcSelf[] = "/proc/self/"; + static const size_t kProcSelfLen = sizeof(kProcSelf) - 1; + const char* path = aPath; + // This buffer just needs to be large enough for any such path that + // the policy would actually allow. sizeof("/proc/2147483647/") == 18. + char rewrittenPath[64]; + if (strncmp(aPath, kProcSelf, kProcSelfLen) == 0) { + ssize_t len = base::strings::SafeSPrintf(rewrittenPath, "/proc/%d/%s", + getpid(), aPath + kProcSelfLen); + if (static_cast(len) < sizeof(rewrittenPath)) { + if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) { + SANDBOX_LOG("rewriting %s -> %s", aPath, rewrittenPath); + } + path = rewrittenPath; + } else { + SANDBOX_LOG("not rewriting unexpectedly long path %s", aPath); + } + } + + struct iovec ios[3]; + int respFds[2]; + + // Set up iovecs for request + path. + ios[0].iov_base = const_cast(aReq); + ios[0].iov_len = sizeof(*aReq); + ios[1].iov_base = const_cast(path); + ios[1].iov_len = strlen(path) + 1; + if (aPath2 != nullptr) { + ios[2].iov_base = const_cast(aPath2); + ios[2].iov_len = strlen(aPath2) + 1; + } else { + ios[2].iov_base = nullptr; + ios[2].iov_len = 0; + } + if (ios[1].iov_len > kMaxPathLen) { + return -ENAMETOOLONG; + } + if (ios[2].iov_len > kMaxPathLen) { + return -ENAMETOOLONG; + } + + // Create response socket and send request. + if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, respFds) < 0) { + return -errno; + } + const ssize_t sent = SendWithFd(mFileDesc, ios, 3, respFds[1]); + const int sendErrno = errno; + MOZ_ASSERT(sent < 0 || static_cast(sent) == + ios[0].iov_len + ios[1].iov_len + ios[2].iov_len); + close(respFds[1]); + if (sent < 0) { + close(respFds[0]); + return -sendErrno; + } + + // Set up iovecs for response. + Response resp; + ios[0].iov_base = &resp; + ios[0].iov_len = sizeof(resp); + if (aResponseBuff) { + ios[1].iov_base = aResponseBuff; + ios[1].iov_len = aReq->mBufSize; + } else { + ios[1].iov_base = nullptr; + ios[1].iov_len = 0; + } + + // Wait for response and return appropriately. + int openedFd = -1; + const ssize_t recvd = RecvWithFd(respFds[0], ios, aResponseBuff ? 2 : 1, + expectFd ? &openedFd : nullptr); + const int recvErrno = errno; + close(respFds[0]); + if (recvd < 0) { + return -recvErrno; + } + if (recvd == 0) { + SANDBOX_LOG("Unexpected EOF, op %d flags 0%o path %s", aReq->mOp, + aReq->mFlags, path); + return -EIO; + } + MOZ_ASSERT(static_cast(recvd) <= ios[0].iov_len + ios[1].iov_len); + // Some calls such as readlink return a size if successful + if (resp.mError >= 0) { + // Success! + if (expectFd) { + MOZ_ASSERT(openedFd >= 0); + return openedFd; + } + return resp.mError; + } + if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) { + // Keep in mind that "rejected" files can include ones that don't + // actually exist, if it's something that's optional or part of a + // search path (e.g., shared libraries). In those cases, this + // error message is expected. + SANDBOX_LOG("Failed errno %d op %s flags 0%o path %s", resp.mError, + OperationDescription[aReq->mOp], aReq->mFlags, path); + } + if (openedFd >= 0) { + close(openedFd); + } + return resp.mError; +} + +int SandboxBrokerClient::Open(const char* aPath, int aFlags) { + Request req = {SANDBOX_FILE_OPEN, aFlags, 0}; + int maybeFd = DoCall(&req, aPath, nullptr, nullptr, true); + if (maybeFd >= 0) { + // NSPR has opinions about file flags. Fix O_CLOEXEC. + if ((aFlags & O_CLOEXEC) == 0) { + fcntl(maybeFd, F_SETFD, 0); + } + } + return maybeFd; +} + +int SandboxBrokerClient::Access(const char* aPath, int aMode) { + Request req = {SANDBOX_FILE_ACCESS, aMode, 0}; + return DoCall(&req, aPath, nullptr, nullptr, false); +} + +int SandboxBrokerClient::Stat(const char* aPath, statstruct* aStat) { + if (!aPath || !aStat) { + return -EFAULT; + } + + Request req = {SANDBOX_FILE_STAT, 0, sizeof(statstruct)}; + return DoCall(&req, aPath, nullptr, (void*)aStat, false); +} + +int SandboxBrokerClient::LStat(const char* aPath, statstruct* aStat) { + if (!aPath || !aStat) { + return -EFAULT; + } + + Request req = {SANDBOX_FILE_STAT, O_NOFOLLOW, sizeof(statstruct)}; + return DoCall(&req, aPath, nullptr, (void*)aStat, false); +} + +int SandboxBrokerClient::Chmod(const char* aPath, int aMode) { + Request req = {SANDBOX_FILE_CHMOD, aMode, 0}; + return DoCall(&req, aPath, nullptr, nullptr, false); +} + +int SandboxBrokerClient::Link(const char* aOldPath, const char* aNewPath) { + Request req = {SANDBOX_FILE_LINK, 0, 0}; + return DoCall(&req, aOldPath, aNewPath, nullptr, false); +} + +int SandboxBrokerClient::Symlink(const char* aOldPath, const char* aNewPath) { + Request req = {SANDBOX_FILE_SYMLINK, 0, 0}; + return DoCall(&req, aOldPath, aNewPath, nullptr, false); +} + +int SandboxBrokerClient::Rename(const char* aOldPath, const char* aNewPath) { + Request req = {SANDBOX_FILE_RENAME, 0, 0}; + return DoCall(&req, aOldPath, aNewPath, nullptr, false); +} + +int SandboxBrokerClient::Mkdir(const char* aPath, int aMode) { + Request req = {SANDBOX_FILE_MKDIR, aMode, 0}; + return DoCall(&req, aPath, nullptr, nullptr, false); +} + +int SandboxBrokerClient::Unlink(const char* aPath) { + Request req = {SANDBOX_FILE_UNLINK, 0, 0}; + return DoCall(&req, aPath, nullptr, nullptr, false); +} + +int SandboxBrokerClient::Rmdir(const char* aPath) { + Request req = {SANDBOX_FILE_RMDIR, 0, 0}; + return DoCall(&req, aPath, nullptr, nullptr, false); +} + +int SandboxBrokerClient::Readlink(const char* aPath, void* aBuff, + size_t aSize) { + Request req = {SANDBOX_FILE_READLINK, 0, aSize}; + return DoCall(&req, aPath, nullptr, aBuff, false); +} + +int SandboxBrokerClient::Connect(const sockaddr_un* aAddr, size_t aLen, + int aType) { + static constexpr size_t maxLen = sizeof(aAddr->sun_path); + const char* path = aAddr->sun_path; + const auto addrEnd = reinterpret_cast(aAddr) + aLen; + // Ensure that the length isn't impossibly small. + if (addrEnd <= path) { + return -EINVAL; + } + // Unix domain only + if (aAddr->sun_family != AF_UNIX) { + return -EAFNOSUPPORT; + } + // How much of sun_path may be accessed? + auto bufLen = static_cast(addrEnd - path); + if (bufLen > maxLen) { + bufLen = maxLen; + } + + // Try to handle abstract addresses where the address (the part + // after the leading null byte) resembles a pathname: a leading + // slash and no embedded nulls. + // + // `DoCall` expects null-terminated strings, but in this case the + // "path" is terminated by the sockaddr length (without a null), so + // we need to make a copy. + if (bufLen >= 2 && path[0] == '\0' && path[1] == '/' && + !memchr(path + 1, '\0', bufLen - 1)) { + char tmpBuf[maxLen]; + MOZ_RELEASE_ASSERT(bufLen - 1 < maxLen); + memcpy(tmpBuf, path + 1, bufLen - 1); + tmpBuf[bufLen - 1] = '\0'; + + const Request req = {SANDBOX_SOCKET_CONNECT_ABSTRACT, aType, 0}; + return DoCall(&req, tmpBuf, nullptr, nullptr, true); + } + + // Require null-termination. (Linux doesn't require it, but + // applications usually null-terminate for portability, and not + // handling unterminated strings means we don't have to copy the path.) + const size_t pathLen = strnlen(path, bufLen); + if (pathLen == bufLen) { + return -ENAMETOOLONG; + } + + // Abstract addresses are handled only in some specific case, error in others + if (pathLen == 0) { + return -ENETUNREACH; + } + + const Request req = {SANDBOX_SOCKET_CONNECT, aType, 0}; + return DoCall(&req, path, nullptr, nullptr, true); +} + +} // namespace mozilla diff --git a/security/sandbox/linux/SandboxBrokerClient.h b/security/sandbox/linux/SandboxBrokerClient.h new file mode 100644 index 0000000000..9e4c1825c3 --- /dev/null +++ b/security/sandbox/linux/SandboxBrokerClient.h @@ -0,0 +1,57 @@ +/* -*- 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_SandboxBrokerClient_h +#define mozilla_SandboxBrokerClient_h + +#include "broker/SandboxBrokerCommon.h" +#include "broker/SandboxBrokerUtils.h" + +#include "mozilla/Attributes.h" + +// This is the client for the sandbox broker described in +// broker/SandboxBroker.h; its constructor takes the file descriptor +// returned by SandboxBroker::Create, passed to the child over IPC. +// +// The operations exposed here can be called from any thread and in +// async signal handlers, like the corresponding system calls. The +// intended use is from a seccomp-bpf SIGSYS handler, to transparently +// replace those syscalls, but they could also be used directly. + +struct stat; +struct sockaddr_un; + +namespace mozilla { + +class SandboxBrokerClient final : private SandboxBrokerCommon { + public: + explicit SandboxBrokerClient(int aFd); + ~SandboxBrokerClient(); + + int Open(const char* aPath, int aFlags); + int Access(const char* aPath, int aMode); + int Stat(const char* aPath, statstruct* aStat); + int LStat(const char* aPath, statstruct* aStat); + int Chmod(const char* aPath, int aMode); + int Link(const char* aPath, const char* aPath2); + int Mkdir(const char* aPath, int aMode); + int Symlink(const char* aOldPath, const char* aNewPath); + int Rename(const char* aOldPath, const char* aNewPath); + int Unlink(const char* aPath); + int Rmdir(const char* aPath); + int Readlink(const char* aPath, void* aBuf, size_t aBufSize); + int Connect(const struct sockaddr_un* aAddr, size_t aLen, int aType); + + private: + int mFileDesc; + + int DoCall(const Request* aReq, const char* aPath, const char* aPath2, + void* aReponseBuff, bool expectFd); +}; + +} // namespace mozilla + +#endif // mozilla_SandboxBrokerClient_h diff --git a/security/sandbox/linux/SandboxChrootProto.h b/security/sandbox/linux/SandboxChrootProto.h new file mode 100644 index 0000000000..362e15a219 --- /dev/null +++ b/security/sandbox/linux/SandboxChrootProto.h @@ -0,0 +1,21 @@ +/* -*- 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_SandboxChrootProto_h +#define mozilla_SandboxChrootProto_h + +#include "mozilla/Types.h" + +namespace mozilla { + +static const int kSandboxChrootClientFd = 6; +static const char kSandboxChrootRequest = 'C'; +static const char kSandboxChrootResponse = 'O'; +static const char kSandboxChrootEnvFlag[] = "MOZ_SANDBOX_USE_CHROOT"; + +} // namespace mozilla + +#endif // mozilla_SandboxChrootProto_h diff --git a/security/sandbox/linux/SandboxFilter.cpp b/security/sandbox/linux/SandboxFilter.cpp new file mode 100644 index 0000000000..2eec3b27a7 --- /dev/null +++ b/security/sandbox/linux/SandboxFilter.cpp @@ -0,0 +1,2152 @@ +/* -*- 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 "SandboxFilter.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "Sandbox.h" // for ContentProcessSandboxParams +#include "SandboxBrokerClient.h" +#include "SandboxFilterUtil.h" +#include "SandboxInfo.h" +#include "SandboxInternal.h" +#include "SandboxLogging.h" +#include "SandboxOpenedFiles.h" +#include "mozilla/PodOperations.h" +#include "mozilla/ProcInfo_linux.h" +#include "mozilla/TemplateLib.h" +#include "mozilla/UniquePtr.h" +#include "prenv.h" +#include "sandbox/linux/bpf_dsl/bpf_dsl.h" +#include "sandbox/linux/system_headers/linux_seccomp.h" +#include "sandbox/linux/system_headers/linux_syscalls.h" + +using namespace sandbox::bpf_dsl; +#define CASES SANDBOX_BPF_DSL_CASES + +// Fill in defines in case of old headers. +// (Warning: these are wrong on PA-RISC.) +#ifndef MADV_HUGEPAGE +# define MADV_HUGEPAGE 14 +#endif +#ifndef MADV_NOHUGEPAGE +# define MADV_NOHUGEPAGE 15 +#endif +#ifndef MADV_DONTDUMP +# define MADV_DONTDUMP 16 +#endif + +// Added in Linux 4.5; see bug 1303813. +#ifndef MADV_FREE +# define MADV_FREE 8 +#endif + +#ifndef PR_SET_PTRACER +# define PR_SET_PTRACER 0x59616d61 +#endif + +// Linux 5.17+ +#ifndef PR_SET_VMA +# define PR_SET_VMA 0x53564d41 +#endif +#ifndef PR_SET_VMA_ANON_NAME +# define PR_SET_VMA_ANON_NAME 0 +#endif + +// The headers define O_LARGEFILE as 0 on x86_64, but we need the +// actual value because it shows up in file flags. +#define O_LARGEFILE_REAL 00100000 + +// Not part of UAPI, but userspace sees it in F_GETFL; see bug 1650751. +#define FMODE_NONOTIFY 0x4000000 + +#ifndef F_LINUX_SPECIFIC_BASE +# define F_LINUX_SPECIFIC_BASE 1024 +#else +static_assert(F_LINUX_SPECIFIC_BASE == 1024); +#endif + +#ifndef F_ADD_SEALS +# define F_ADD_SEALS (F_LINUX_SPECIFIC_BASE + 9) +# define F_GET_SEALS (F_LINUX_SPECIFIC_BASE + 10) +#else +static_assert(F_ADD_SEALS == (F_LINUX_SPECIFIC_BASE + 9)); +static_assert(F_GET_SEALS == (F_LINUX_SPECIFIC_BASE + 10)); +#endif + +// To avoid visual confusion between "ifdef ANDROID" and "ifndef ANDROID": +#ifndef ANDROID +# define DESKTOP +#endif + +namespace { +static const unsigned long kIoctlTypeMask = _IOC_TYPEMASK << _IOC_TYPESHIFT; +static const unsigned long kTtyIoctls = TIOCSTI & kIoctlTypeMask; +// On some older architectures (but not x86 or ARM), ioctls are +// assigned type fields differently, and the TIOC/TC/FIO group +// isn't all the same type. If/when we support those archs, +// this would need to be revised (but really this should be a +// default-deny policy; see below). +static_assert(kTtyIoctls == (TCSETA & kIoctlTypeMask) && + kTtyIoctls == (FIOASYNC & kIoctlTypeMask), + "tty-related ioctls use the same type"); +}; // namespace + +// This file defines the seccomp-bpf system call filter policies. +// See also SandboxFilterUtil.h, for the CASES_FOR_* macros and +// SandboxFilterBase::Evaluate{Socket,Ipc}Call. +// +// One important difference from how Chromium bpf_dsl filters are +// normally interpreted: returning -ENOSYS from a Trap() handler +// indicates an unexpected system call; SigSysHandler() in Sandbox.cpp +// will detect this, request a crash dump, and terminate the process. +// This does not apply to using Error(ENOSYS) in the policy, so that +// can be used if returning an actual ENOSYS is needed. + +namespace mozilla { + +// This class allows everything used by the sandbox itself, by the +// core IPC code, by the crash reporter, or other core code. It also +// contains support for brokering file operations, but file access is +// denied if no broker client is provided by the concrete class. +class SandboxPolicyCommon : public SandboxPolicyBase { + protected: + // Subclasses can assign these in their constructors to loosen the + // default settings. + SandboxBrokerClient* mBroker = nullptr; + bool mMayCreateShmem = false; + bool mAllowUnsafeSocketPair = false; + bool mBrokeredConnect = false; // Can connect() be brokered? + + SandboxPolicyCommon() = default; + + typedef const sandbox::arch_seccomp_data& ArgsRef; + + static intptr_t BlockedSyscallTrap(ArgsRef aArgs, void* aux) { + MOZ_ASSERT(!aux); + return -ENOSYS; + } + + // Convert Unix-style "return -1 and set errno" APIs back into the + // Linux ABI "return -err" style. + static intptr_t ConvertError(long rv) { return rv < 0 ? -errno : rv; } + + template + static intptr_t DoSyscall(long nr, Args... args) { + static_assert(std::conjunction_v< + std::conditional_t<(sizeof(Args) <= sizeof(void*)), + std::true_type, std::false_type>...>, + "each syscall arg is at most one word"); + return ConvertError(syscall(nr, args...)); + } + + // Mesa's amdgpu driver uses kcmp with KCMP_FILE; see also bug + // 1624743. This policy restricts it to the process's own pid, + // which should be sufficient on its own if we need to remove the + // `type` restriction in the future. + // + // (Note: if we end up with more Mesa-specific hooks needed in + // several process types, we could put them into this class's + // EvaluateSyscall guarded by a boolean member variable, or + // introduce another layer of subclassing.) + ResultExpr KcmpPolicyForMesa() const { + // The real KCMP_FILE is part of an anonymous enum in + // , but we can't depend on having that header, + // and it's not a #define so the usual #ifndef approach + // doesn't work. + static const int kKcmpFile = 0; + const pid_t myPid = getpid(); + Arg pid1(0), pid2(1); + Arg type(2); + return If(AllOf(pid1 == myPid, pid2 == myPid, type == kKcmpFile), Allow()) + .Else(InvalidSyscall()); + } + + static intptr_t SchedTrap(ArgsRef aArgs, void* aux) { + const pid_t tid = syscall(__NR_gettid); + if (aArgs.args[0] == static_cast(tid)) { + return DoSyscall(aArgs.nr, 0, static_cast(aArgs.args[1]), + static_cast(aArgs.args[2]), + static_cast(aArgs.args[3]), + static_cast(aArgs.args[4]), + static_cast(aArgs.args[5])); + } + return -EPERM; + } + + private: + // Bug 1093893: Translate tkill to tgkill for pthread_kill; fixed in + // bionic commit 10c8ce59a (in JB and up; API level 16 = Android 4.1). + // Bug 1376653: musl also needs this, and security-wise it's harmless. + static intptr_t TKillCompatTrap(ArgsRef aArgs, void* aux) { + auto tid = static_cast(aArgs.args[0]); + auto sig = static_cast(aArgs.args[1]); + return DoSyscall(__NR_tgkill, getpid(), tid, sig); + } + + static intptr_t SetNoNewPrivsTrap(ArgsRef& aArgs, void* aux) { + if (gSetSandboxFilter == nullptr) { + // Called after BroadcastSetThreadSandbox finished, therefore + // not our doing and not expected. + return BlockedSyscallTrap(aArgs, nullptr); + } + // Signal that the filter is already in place. + return -ETXTBSY; + } + + // Trap handlers for filesystem brokering. + // (The amount of code duplication here could be improved....) +#ifdef __NR_open + static intptr_t OpenTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto path = reinterpret_cast(aArgs.args[0]); + auto flags = static_cast(aArgs.args[1]); + return broker->Open(path, flags); + } + + static intptr_t AccessTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto path = reinterpret_cast(aArgs.args[0]); + auto mode = static_cast(aArgs.args[1]); + return broker->Access(path, mode); + } + + static intptr_t StatTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto path = reinterpret_cast(aArgs.args[0]); + auto buf = reinterpret_cast(aArgs.args[1]); + return broker->Stat(path, buf); + } + + static intptr_t LStatTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto path = reinterpret_cast(aArgs.args[0]); + auto buf = reinterpret_cast(aArgs.args[1]); + return broker->LStat(path, buf); + } + + static intptr_t ChmodTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto path = reinterpret_cast(aArgs.args[0]); + auto mode = static_cast(aArgs.args[1]); + return broker->Chmod(path, mode); + } + + static intptr_t LinkTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto path = reinterpret_cast(aArgs.args[0]); + auto path2 = reinterpret_cast(aArgs.args[1]); + return broker->Link(path, path2); + } + + static intptr_t SymlinkTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto path = reinterpret_cast(aArgs.args[0]); + auto path2 = reinterpret_cast(aArgs.args[1]); + return broker->Symlink(path, path2); + } + + static intptr_t RenameTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto path = reinterpret_cast(aArgs.args[0]); + auto path2 = reinterpret_cast(aArgs.args[1]); + return broker->Rename(path, path2); + } + + static intptr_t MkdirTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto path = reinterpret_cast(aArgs.args[0]); + auto mode = static_cast(aArgs.args[1]); + return broker->Mkdir(path, mode); + } + + static intptr_t RmdirTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto path = reinterpret_cast(aArgs.args[0]); + return broker->Rmdir(path); + } + + static intptr_t UnlinkTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto path = reinterpret_cast(aArgs.args[0]); + if (path && path[0] == '\0') { + // If the path is empty, then just fail the call here + return -ENOENT; + } + return broker->Unlink(path); + } + + static intptr_t ReadlinkTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto path = reinterpret_cast(aArgs.args[0]); + auto buf = reinterpret_cast(aArgs.args[1]); + auto size = static_cast(aArgs.args[2]); + return broker->Readlink(path, buf, size); + } +#endif // __NR_open + + static intptr_t OpenAtTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto fd = static_cast(aArgs.args[0]); + auto path = reinterpret_cast(aArgs.args[1]); + auto flags = static_cast(aArgs.args[2]); + if (fd != AT_FDCWD && path[0] != '/') { + SANDBOX_LOG("unsupported fd-relative openat(%d, \"%s\", 0%o)", fd, path, + flags); + return BlockedSyscallTrap(aArgs, nullptr); + } + return broker->Open(path, flags); + } + + static intptr_t AccessAtTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto fd = static_cast(aArgs.args[0]); + auto path = reinterpret_cast(aArgs.args[1]); + auto mode = static_cast(aArgs.args[2]); + // Linux's faccessat syscall has no "flags" argument. Attempting + // to handle the flags != 0 case is left to userspace; this is + // impossible to do correctly in all cases, but that's not our + // problem. + // + // Starting with kernel 5.8+ and glibc 2.33, there is faccessat2 that + // supports flags, handled below. + if (fd != AT_FDCWD && path[0] != '/') { + SANDBOX_LOG("unsupported fd-relative faccessat(%d, \"%s\", %d)", fd, path, + mode); + return BlockedSyscallTrap(aArgs, nullptr); + } + return broker->Access(path, mode); + } + + static intptr_t AccessAt2Trap(ArgsRef aArgs, void* aux) { + auto* broker = static_cast(aux); + auto fd = static_cast(aArgs.args[0]); + const auto* path = reinterpret_cast(aArgs.args[1]); + auto mode = static_cast(aArgs.args[2]); + auto flags = static_cast(aArgs.args[3]); + if (fd != AT_FDCWD && path[0] != '/') { + SANDBOX_LOG("unsupported fd-relative faccessat2(%d, \"%s\", %d, %d)", fd, + path, mode, flags); + return BlockedSyscallTrap(aArgs, nullptr); + } + if ((flags & ~AT_EACCESS) == 0) { + return broker->Access(path, mode); + } + return ConvertError(ENOSYS); + } + + static intptr_t StatAtTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto fd = static_cast(aArgs.args[0]); + auto path = reinterpret_cast(aArgs.args[1]); + auto buf = reinterpret_cast(aArgs.args[2]); + auto flags = static_cast(aArgs.args[3]); + + if (fd != AT_FDCWD && (flags & AT_EMPTY_PATH) && path && + !strcmp(path, "")) { +#ifdef __NR_fstat64 + return DoSyscall(__NR_fstat64, fd, buf); +#else + return DoSyscall(__NR_fstat, fd, buf); +#endif + } + + if (!broker) { + return BlockedSyscallTrap(aArgs, nullptr); + } + + if (fd != AT_FDCWD && path && path[0] != '/') { + SANDBOX_LOG("unsupported fd-relative fstatat(%d, \"%s\", %p, 0x%x)", fd, + path, buf, flags); + return BlockedSyscallTrap(aArgs, nullptr); + } + + int badFlags = flags & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT); + if (badFlags != 0) { + SANDBOX_LOG("unsupported flags 0x%x in fstatat(%d, \"%s\", %p, 0x%x)", + badFlags, fd, path, buf, flags); + return BlockedSyscallTrap(aArgs, nullptr); + } + return (flags & AT_SYMLINK_NOFOLLOW) == 0 ? broker->Stat(path, buf) + : broker->LStat(path, buf); + } + + static intptr_t ChmodAtTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto fd = static_cast(aArgs.args[0]); + auto path = reinterpret_cast(aArgs.args[1]); + auto mode = static_cast(aArgs.args[2]); + auto flags = static_cast(aArgs.args[3]); + if (fd != AT_FDCWD && path[0] != '/') { + SANDBOX_LOG("unsupported fd-relative chmodat(%d, \"%s\", 0%o, %d)", fd, + path, mode, flags); + return BlockedSyscallTrap(aArgs, nullptr); + } + if (flags != 0) { + SANDBOX_LOG("unsupported flags in chmodat(%d, \"%s\", 0%o, %d)", fd, path, + mode, flags); + return BlockedSyscallTrap(aArgs, nullptr); + } + return broker->Chmod(path, mode); + } + + static intptr_t LinkAtTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto fd = static_cast(aArgs.args[0]); + auto path = reinterpret_cast(aArgs.args[1]); + auto fd2 = static_cast(aArgs.args[2]); + auto path2 = reinterpret_cast(aArgs.args[3]); + auto flags = static_cast(aArgs.args[4]); + if ((fd != AT_FDCWD && path[0] != '/') || + (fd2 != AT_FDCWD && path2[0] != '/')) { + SANDBOX_LOG( + "unsupported fd-relative linkat(%d, \"%s\", %d, \"%s\", 0x%x)", fd, + path, fd2, path2, flags); + return BlockedSyscallTrap(aArgs, nullptr); + } + if (flags != 0) { + SANDBOX_LOG("unsupported flags in linkat(%d, \"%s\", %d, \"%s\", 0x%x)", + fd, path, fd2, path2, flags); + return BlockedSyscallTrap(aArgs, nullptr); + } + return broker->Link(path, path2); + } + + static intptr_t SymlinkAtTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto path = reinterpret_cast(aArgs.args[0]); + auto fd2 = static_cast(aArgs.args[1]); + auto path2 = reinterpret_cast(aArgs.args[2]); + if (fd2 != AT_FDCWD && path2[0] != '/') { + SANDBOX_LOG("unsupported fd-relative symlinkat(\"%s\", %d, \"%s\")", path, + fd2, path2); + return BlockedSyscallTrap(aArgs, nullptr); + } + return broker->Symlink(path, path2); + } + + static intptr_t RenameAtTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto fd = static_cast(aArgs.args[0]); + auto path = reinterpret_cast(aArgs.args[1]); + auto fd2 = static_cast(aArgs.args[2]); + auto path2 = reinterpret_cast(aArgs.args[3]); + if ((fd != AT_FDCWD && path[0] != '/') || + (fd2 != AT_FDCWD && path2[0] != '/')) { + SANDBOX_LOG("unsupported fd-relative renameat(%d, \"%s\", %d, \"%s\")", + fd, path, fd2, path2); + return BlockedSyscallTrap(aArgs, nullptr); + } + return broker->Rename(path, path2); + } + + static intptr_t MkdirAtTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto fd = static_cast(aArgs.args[0]); + auto path = reinterpret_cast(aArgs.args[1]); + auto mode = static_cast(aArgs.args[2]); + if (fd != AT_FDCWD && path[0] != '/') { + SANDBOX_LOG("unsupported fd-relative mkdirat(%d, \"%s\", 0%o)", fd, path, + mode); + return BlockedSyscallTrap(aArgs, nullptr); + } + return broker->Mkdir(path, mode); + } + + static intptr_t UnlinkAtTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto fd = static_cast(aArgs.args[0]); + auto path = reinterpret_cast(aArgs.args[1]); + auto flags = static_cast(aArgs.args[2]); + if (path && path[0] == '\0') { + // If the path is empty, then just fail the call here + return -ENOENT; + } + if (fd != AT_FDCWD && path[0] != '/') { + SANDBOX_LOG("unsupported fd-relative unlinkat(%d, \"%s\", 0x%x)", fd, + path, flags); + return BlockedSyscallTrap(aArgs, nullptr); + } + int badFlags = flags & ~AT_REMOVEDIR; + if (badFlags != 0) { + SANDBOX_LOG("unsupported flags 0x%x in unlinkat(%d, \"%s\", 0x%x)", + badFlags, fd, path, flags); + return BlockedSyscallTrap(aArgs, nullptr); + } + return (flags & AT_REMOVEDIR) == 0 ? broker->Unlink(path) + : broker->Rmdir(path); + } + + static intptr_t ReadlinkAtTrap(ArgsRef aArgs, void* aux) { + auto broker = static_cast(aux); + auto fd = static_cast(aArgs.args[0]); + auto path = reinterpret_cast(aArgs.args[1]); + auto buf = reinterpret_cast(aArgs.args[2]); + auto size = static_cast(aArgs.args[3]); + if (fd != AT_FDCWD && path[0] != '/') { + SANDBOX_LOG("unsupported fd-relative readlinkat(%d, %s, %p, %d)", fd, + path, buf, size); + return BlockedSyscallTrap(aArgs, nullptr); + } + return broker->Readlink(path, buf, size); + } + + static intptr_t SocketpairDatagramTrap(ArgsRef aArgs, void* aux) { + auto fds = reinterpret_cast(aArgs.args[3]); + // Return sequential packet sockets instead of the expected + // datagram sockets; see bug 1355274 for details. + return ConvertError(socketpair(AF_UNIX, SOCK_SEQPACKET, 0, fds)); + } + + static intptr_t SocketpairUnpackTrap(ArgsRef aArgs, void* aux) { +#ifdef __NR_socketpair + auto argsPtr = reinterpret_cast(aArgs.args[1]); + return DoSyscall(__NR_socketpair, argsPtr[0], argsPtr[1], argsPtr[2], + argsPtr[3]); +#else + MOZ_CRASH("unreachable?"); + return -ENOSYS; +#endif + } + + static intptr_t GetSockOptUnpackTrap(ArgsRef aArgs, void* aux) { +#ifdef __NR_getsockopt + auto argsPtr = reinterpret_cast(aArgs.args[1]); + return DoSyscall(__NR_getsockopt, argsPtr[0], argsPtr[1], argsPtr[2], + argsPtr[3], argsPtr[4]); +#else + MOZ_CRASH("unreachable?"); + return -ENOSYS; +#endif + } + + // This just needs to return something to stand in for the + // unconnected socket until ConnectTrap, below, and keep track of + // the socket type somehow. Half a socketpair *is* a socket, so it + // should result in minimal confusion in the caller. + static intptr_t FakeSocketTrapCommon(int domain, int type, int protocol) { + int fds[2]; + // X11 client libs will still try to getaddrinfo() even for a + // local connection. Also, WebRTC still has vestigial network + // code trying to do things in the content process. Politely tell + // them no. + if (domain != AF_UNIX) { + return -EAFNOSUPPORT; + } + if (socketpair(domain, type, protocol, fds) != 0) { + return -errno; + } + close(fds[1]); + return fds[0]; + } + + static intptr_t FakeSocketTrap(ArgsRef aArgs, void* aux) { + return FakeSocketTrapCommon(static_cast(aArgs.args[0]), + static_cast(aArgs.args[1]), + static_cast(aArgs.args[2])); + } + + static intptr_t FakeSocketTrapLegacy(ArgsRef aArgs, void* aux) { + const auto innerArgs = reinterpret_cast(aArgs.args[1]); + + return FakeSocketTrapCommon(static_cast(innerArgs[0]), + static_cast(innerArgs[1]), + static_cast(innerArgs[2])); + } + + static Maybe DoGetSockOpt(int fd, int optname) { + int optval; + socklen_t optlen = sizeof(optval); + + if (getsockopt(fd, SOL_SOCKET, optname, &optval, &optlen) != 0) { + return Nothing(); + } + MOZ_RELEASE_ASSERT(static_cast(optlen) == sizeof(optval)); + return Some(optval); + } + + // Substitute the newly connected socket from the broker for the + // original socket. This is meant to be used on a fd from + // FakeSocketTrap, above, but it should also work to simulate + // re-connect()ing a real connected socket. + // + // Warning: This isn't quite right if the socket is dup()ed, because + // other duplicates will still be the original socket, but hopefully + // nothing we're dealing with does that. + static intptr_t ConnectTrapCommon(SandboxBrokerClient* aBroker, int aFd, + const struct sockaddr_un* aAddr, + socklen_t aLen) { + if (aFd < 0) { + return -EBADF; + } + const auto maybeDomain = DoGetSockOpt(aFd, SO_DOMAIN); + if (!maybeDomain) { + return -errno; + } + if (*maybeDomain != AF_UNIX) { + return -EAFNOSUPPORT; + } + const auto maybeType = DoGetSockOpt(aFd, SO_TYPE); + if (!maybeType) { + return -errno; + } + const int oldFlags = fcntl(aFd, F_GETFL); + if (oldFlags == -1) { + return -errno; + } + const int newFd = aBroker->Connect(aAddr, aLen, *maybeType); + if (newFd < 0) { + return newFd; + } + // Copy over the nonblocking flag. The connect() won't be + // nonblocking in that case, but that shouldn't matter for + // AF_UNIX. The other fcntl-settable flags are either irrelevant + // for sockets (e.g., O_APPEND) or would be blocked by this + // seccomp-bpf policy, so they're ignored. + if (fcntl(newFd, F_SETFL, oldFlags & O_NONBLOCK) != 0) { + close(newFd); + return -errno; + } + if (dup2(newFd, aFd) < 0) { + close(newFd); + return -errno; + } + close(newFd); + return 0; + } + + static intptr_t ConnectTrap(ArgsRef aArgs, void* aux) { + typedef const struct sockaddr_un* AddrPtr; + + return ConnectTrapCommon(static_cast(aux), + static_cast(aArgs.args[0]), + reinterpret_cast(aArgs.args[1]), + static_cast(aArgs.args[2])); + } + + static intptr_t ConnectTrapLegacy(ArgsRef aArgs, void* aux) { + const auto innerArgs = reinterpret_cast(aArgs.args[1]); + typedef const struct sockaddr_un* AddrPtr; + + return ConnectTrapCommon(static_cast(aux), + static_cast(innerArgs[0]), + reinterpret_cast(innerArgs[1]), + static_cast(innerArgs[2])); + } + + static intptr_t StatFsTrap(ArgsRef aArgs, void* aux) { + // Warning: the kernel interface is not the C interface. The + // structs are different ( vs. ), and + // the statfs64 version takes an additional size parameter. + auto path = reinterpret_cast(aArgs.args[0]); + int fd = open(path, O_RDONLY | O_LARGEFILE); + if (fd < 0) { + return -errno; + } + + intptr_t rv; + switch (aArgs.nr) { + case __NR_statfs: { + auto buf = reinterpret_cast(aArgs.args[1]); + rv = DoSyscall(__NR_fstatfs, fd, buf); + break; + } +#ifdef __NR_statfs64 + case __NR_statfs64: { + auto sz = static_cast(aArgs.args[1]); + auto buf = reinterpret_cast(aArgs.args[2]); + rv = DoSyscall(__NR_fstatfs64, fd, sz, buf); + break; + } +#endif + default: + MOZ_ASSERT(false); + rv = -ENOSYS; + } + + close(fd); + return rv; + } + + public: + ResultExpr InvalidSyscall() const override { + return Trap(BlockedSyscallTrap, nullptr); + } + + virtual ResultExpr ClonePolicy(ResultExpr failPolicy) const { + // Allow use for simple thread creation (pthread_create) only. + + // WARNING: s390 and cris pass the flags in the second arg -- see + // CLONE_BACKWARDS2 in arch/Kconfig in the kernel source -- but we + // don't support seccomp-bpf on those archs yet. + Arg flags(0); + + // The exact flags used can vary. CLONE_DETACHED is used by musl + // and by old versions of Android (<= JB 4.2), but it's been + // ignored by the kernel since the beginning of the Git history. + // + // If we ever need to support Android <= KK 4.4 again, SETTLS + // and the *TID flags will need to be made optional. + static const int flags_required = + CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD | + CLONE_SYSVSEM | CLONE_SETTLS | CLONE_PARENT_SETTID | + CLONE_CHILD_CLEARTID; + static const int flags_optional = CLONE_DETACHED; + + return If((flags & ~flags_optional) == flags_required, Allow()) + .Else(failPolicy); + } + + virtual ResultExpr PrctlPolicy() const { + Arg op(0); + Arg arg2(1); + return Switch(op) + .CASES((PR_SET_VMA), // Tagging of anonymous memory mappings + If(arg2 == PR_SET_VMA_ANON_NAME, Allow()).Else(InvalidSyscall())) + .CASES((PR_GET_SECCOMP, // BroadcastSetThreadSandbox, etc. + PR_SET_NAME, // Thread creation + PR_SET_DUMPABLE, // Crash reporting + PR_SET_PTRACER), // Debug-mode crash handling + Allow()) + .CASES((PR_CAPBSET_READ), // libcap.so.2 loaded by libpulse.so.0 + // queries for capabilities + Error(EINVAL)) + .Default(InvalidSyscall()); + } + + Maybe EvaluateSocketCall(int aCall, + bool aHasArgs) const override { + switch (aCall) { + case SYS_RECVMSG: + case SYS_SENDMSG: + // These next four aren't needed for IPC or other core + // functionality at the time of this writing, but they're + // subsets of recvmsg/sendmsg so there's nothing gained by not + // allowing them here (and simplifying subclasses). + case SYS_RECVFROM: + case SYS_SENDTO: + case SYS_RECV: + case SYS_SEND: + return Some(Allow()); + + case SYS_SOCKETPAIR: { + // We try to allow "safe" (always connected) socketpairs when using the + // file broker, or for content processes, but we may need to fall back + // and allow all socketpairs in some cases, see bug 1066750. + if (!mBroker && !mAllowUnsafeSocketPair) { + return Nothing(); + } + // See bug 1066750. + if (!aHasArgs) { + // If this is a socketcall(2) platform, but the kernel also + // supports separate syscalls (>= 4.2.0), we can unpack the + // arguments and filter them. + if (HasSeparateSocketCalls()) { + return Some(Trap(SocketpairUnpackTrap, nullptr)); + } + // Otherwise, we can't filter the args if the platform passes + // them by pointer. + return Some(Allow()); + } + Arg domain(0), type(1); + return Some( + If(domain == AF_UNIX, + Switch(type & ~(SOCK_CLOEXEC | SOCK_NONBLOCK)) + .Case(SOCK_STREAM, Allow()) + .Case(SOCK_SEQPACKET, Allow()) + // This is used only by content (and only for + // direct PulseAudio, which is deprecated) but it + // doesn't increase attack surface: + .Case(SOCK_DGRAM, Trap(SocketpairDatagramTrap, nullptr)) + .Default(InvalidSyscall())) + .Else(InvalidSyscall())); + } + + case SYS_GETSOCKOPT: { + // Best-effort argument filtering as for socketpair(2), above. + if (!aHasArgs) { + if (HasSeparateSocketCalls()) { + return Some(Trap(GetSockOptUnpackTrap, nullptr)); + } + return Some(Allow()); + } + Arg level(1), optname(2); + // SO_SNDBUF is used by IPC to avoid constructing + // unnecessarily large gather arrays for `sendmsg`. + // + // SO_DOMAIN and SO_TYPE are needed for connect() brokering, + // but they're harmless even when it's not enabled. + return Some(If(AllOf(level == SOL_SOCKET, + AnyOf(optname == SO_SNDBUF, optname == SO_DOMAIN, + optname == SO_TYPE)), + Allow()) + .Else(InvalidSyscall())); + } + + // These two cases are for connect() brokering, if enabled. + case SYS_SOCKET: + if (mBrokeredConnect) { + const auto trapFn = aHasArgs ? FakeSocketTrap : FakeSocketTrapLegacy; + MOZ_ASSERT(mBroker); + return Some(Trap(trapFn, mBroker)); + } + return Nothing(); + + case SYS_CONNECT: + if (mBrokeredConnect) { + const auto trapFn = aHasArgs ? ConnectTrap : ConnectTrapLegacy; + MOZ_ASSERT(mBroker); + return Some(Trap(trapFn, mBroker)); + } + return Nothing(); + + default: + return Nothing(); + } + } + + ResultExpr EvaluateSyscall(int sysno) const override { + // If a file broker client was provided, route syscalls to it; + // otherwise, fall through to the main policy, which will deny + // them. + if (mBroker) { + switch (sysno) { +#ifdef __NR_open + case __NR_open: + return Trap(OpenTrap, mBroker); + case __NR_access: + return Trap(AccessTrap, mBroker); + CASES_FOR_stat: + return Trap(StatTrap, mBroker); + CASES_FOR_lstat: + return Trap(LStatTrap, mBroker); + case __NR_chmod: + return Trap(ChmodTrap, mBroker); + case __NR_link: + return Trap(LinkTrap, mBroker); + case __NR_mkdir: + return Trap(MkdirTrap, mBroker); + case __NR_symlink: + return Trap(SymlinkTrap, mBroker); + case __NR_rename: + return Trap(RenameTrap, mBroker); + case __NR_rmdir: + return Trap(RmdirTrap, mBroker); + case __NR_unlink: + return Trap(UnlinkTrap, mBroker); + case __NR_readlink: + return Trap(ReadlinkTrap, mBroker); +#endif + case __NR_openat: + return Trap(OpenAtTrap, mBroker); + case __NR_faccessat: + return Trap(AccessAtTrap, mBroker); + case __NR_faccessat2: + return Trap(AccessAt2Trap, mBroker); + CASES_FOR_fstatat: + return Trap(StatAtTrap, mBroker); + // Used by new libc and Rust's stdlib, if available. + // We don't have broker support yet so claim it does not exist. + case __NR_statx: + return Error(ENOSYS); + case __NR_fchmodat: + return Trap(ChmodAtTrap, mBroker); + case __NR_linkat: + return Trap(LinkAtTrap, mBroker); + case __NR_mkdirat: + return Trap(MkdirAtTrap, mBroker); + case __NR_symlinkat: + return Trap(SymlinkAtTrap, mBroker); + case __NR_renameat: + return Trap(RenameAtTrap, mBroker); + case __NR_unlinkat: + return Trap(UnlinkAtTrap, mBroker); + case __NR_readlinkat: + return Trap(ReadlinkAtTrap, mBroker); + } + } else { + // In the absence of a broker we still need to handle the + // fstat-equivalent subset of fstatat; see bug 1673770. + switch (sysno) { + // statx may be used for fstat (bug 1867673) + case __NR_statx: + return Error(ENOSYS); + CASES_FOR_fstatat: + return Trap(StatAtTrap, nullptr); + } + } + + switch (sysno) { + // Timekeeping + // + // (Note: the switch needs to start with a literal case, not a + // macro; otherwise clang-format gets confused.) + case __NR_gettimeofday: +#ifdef __NR_time + case __NR_time: +#endif + case __NR_nanosleep: + return Allow(); + + CASES_FOR_clock_gettime: + CASES_FOR_clock_getres: + CASES_FOR_clock_nanosleep: { + // clockid_t can encode a pid or tid to monitor another + // process or thread's CPU usage (see CPUCLOCK_PID and related + // definitions in include/linux/posix-timers.h in the kernel + // source). For threads, the kernel allows only tids within + // the calling process, so it isn't a problem if we don't + // filter those; pids do need to be restricted to the current + // process in order to not leak information. + Arg clk_id(0); + clockid_t this_process = + MAKE_PROCESS_CPUCLOCK(getpid(), CPUCLOCK_SCHED); + return If(clk_id == CLOCK_MONOTONIC, Allow()) +#ifdef CLOCK_MONOTONIC_COARSE + // Used by SandboxReporter, among other things. + .ElseIf(clk_id == CLOCK_MONOTONIC_COARSE, Allow()) +#endif + .ElseIf(clk_id == CLOCK_PROCESS_CPUTIME_ID, Allow()) + .ElseIf(clk_id == CLOCK_REALTIME, Allow()) +#ifdef CLOCK_REALTIME_COARSE + .ElseIf(clk_id == CLOCK_REALTIME_COARSE, Allow()) +#endif + .ElseIf(clk_id == CLOCK_THREAD_CPUTIME_ID, Allow()) +#ifdef MOZ_GECKO_PROFILER + // Allow clock_gettime on the same process. + .ElseIf(clk_id == this_process, Allow()) + // Allow clock_gettime on a thread. + .ElseIf((clk_id & 7u) == (CPUCLOCK_PERTHREAD_MASK | CPUCLOCK_SCHED), + Allow()) +#endif +#ifdef CLOCK_BOOTTIME + .ElseIf(clk_id == CLOCK_BOOTTIME, Allow()) +#endif + .Else(InvalidSyscall()); + } + + // Thread synchronization + CASES_FOR_futex: + // FIXME(bug 1441993): This could be more restrictive. + return Allow(); + + // Asynchronous I/O + CASES_FOR_epoll_create: + CASES_FOR_epoll_wait: + case __NR_epoll_ctl: + CASES_FOR_poll: + return Allow(); + + // Used when requesting a crash dump. + CASES_FOR_pipe: + return Allow(); + + // Metadata of opened files + CASES_FOR_fstat: + return Allow(); + + CASES_FOR_fcntl: { + Arg cmd(1); + Arg flags(2); + // Typical use of F_SETFL is to modify the flags returned by + // F_GETFL and write them back, including some flags that + // F_SETFL ignores. This is a default-deny policy in case any + // new SETFL-able flags are added. (In particular we want to + // forbid O_ASYNC; see bug 1328896, but also see bug 1408438.) + static const int ignored_flags = + O_ACCMODE | O_LARGEFILE_REAL | O_CLOEXEC | FMODE_NONOTIFY; + static const int allowed_flags = ignored_flags | O_APPEND | O_NONBLOCK; + return Switch(cmd) + // Close-on-exec is meaningless when execve isn't allowed, but + // NSPR reads the bit and asserts that it has the expected value. + .Case(F_GETFD, Allow()) + .Case( + F_SETFD, + If((flags & ~FD_CLOEXEC) == 0, Allow()).Else(InvalidSyscall())) + // F_GETFL is also used by fdopen + .Case(F_GETFL, Allow()) + .Case(F_SETFL, If((flags & ~allowed_flags) == 0, Allow()) + .Else(InvalidSyscall())) + // Not much different from other forms of dup(), and commonly used. + .Case(F_DUPFD_CLOEXEC, Allow()) + .Default(SandboxPolicyBase::EvaluateSyscall(sysno)); + } + + // Simple I/O + case __NR_pread64: + case __NR_write: + case __NR_read: + case __NR_readv: + case __NR_writev: // see SandboxLogging.cpp + CASES_FOR_lseek: + return Allow(); + + CASES_FOR_getdents: + return Allow(); + + CASES_FOR_ftruncate: + case __NR_fallocate: + return mMayCreateShmem ? Allow() : InvalidSyscall(); + + // Used by our fd/shm classes + case __NR_dup: + return Allow(); + + // Memory mapping + CASES_FOR_mmap: + case __NR_munmap: + return Allow(); + + // Shared memory + case __NR_memfd_create: + return Allow(); + + // ipc::Shmem; also, glibc when creating threads: + case __NR_mprotect: + return Allow(); + +#if !defined(MOZ_MEMORY) + // No jemalloc means using a system allocator like glibc + // that might use brk. + case __NR_brk: + return Allow(); + + // Similarly, mremap (bugs: 1047620, 1286119, 1860267) + case __NR_mremap: { + Arg flags(3); + return If((flags & ~MREMAP_MAYMOVE) == 0, Allow()) + .Else(SandboxPolicyBase::EvaluateSyscall(sysno)); + } +#endif + + // madvise hints used by malloc; see bug 1303813 and bug 1364533 + case __NR_madvise: { + Arg advice(2); + // The GMP specific sandbox duplicates this logic, so when adding + // allowed values here also add them to the GMP sandbox rules. + return If(advice == MADV_DONTNEED, Allow()) + .ElseIf(advice == MADV_FREE, Allow()) + .ElseIf(advice == MADV_HUGEPAGE, Allow()) + .ElseIf(advice == MADV_NOHUGEPAGE, Allow()) +#ifdef MOZ_ASAN + .ElseIf(advice == MADV_DONTDUMP, Allow()) +#endif + .ElseIf(advice == MADV_MERGEABLE, Error(EPERM)) // bug 1705045 + .Else(InvalidSyscall()); + } + + // musl libc will set this up in pthreads support. + case __NR_membarrier: + return Allow(); + + // Signal handling + case __NR_sigaltstack: + CASES_FOR_sigreturn: + CASES_FOR_sigprocmask: + CASES_FOR_sigaction: + return Allow(); + + // Send signals within the process (raise(), profiling, etc.) + case __NR_tgkill: { + Arg tgid(0); + return If(tgid == getpid(), Allow()).Else(InvalidSyscall()); + } + + // Polyfill with tgkill; see above. + case __NR_tkill: + return Trap(TKillCompatTrap, nullptr); + + // Yield + case __NR_sched_yield: + return Allow(); + + // Thread creation. + case __NR_clone: + return ClonePolicy(InvalidSyscall()); + + case __NR_clone3: + return Error(ENOSYS); + + // More thread creation. +#ifdef __NR_set_robust_list + case __NR_set_robust_list: + return Allow(); +#endif +#ifdef ANDROID + case __NR_set_tid_address: + return Allow(); +#endif + + // prctl + case __NR_prctl: { + // WARNING: do not handle __NR_prctl directly in subclasses; + // override PrctlPolicy instead. The special handling of + // PR_SET_NO_NEW_PRIVS is used to detect that a thread already + // has the policy applied; see also bug 1257361. + + if (SandboxInfo::Get().Test(SandboxInfo::kHasSeccompTSync)) { + return PrctlPolicy(); + } + + Arg option(0); + return If(option == PR_SET_NO_NEW_PRIVS, + Trap(SetNoNewPrivsTrap, nullptr)) + .Else(PrctlPolicy()); + } + + // NSPR can call this when creating a thread, but it will accept a + // polite "no". + case __NR_getpriority: + // But if thread creation races with sandbox startup, that call + // could succeed, and then we get one of these: + case __NR_setpriority: + return Error(EACCES); + + // Stack bounds are obtained via pthread_getattr_np, which calls + // this but doesn't actually need it: + case __NR_sched_getaffinity: + return Error(ENOSYS); + + // Identifies the processor and node where this thread or process is + // running. This is used by "Awake" profiler markers. + case __NR_getcpu: + return Allow(); + + // Read own pid/tid. + case __NR_getpid: + case __NR_gettid: + return Allow(); + + // Discard capabilities + case __NR_close: + return Allow(); + + // Machine-dependent stuff +#ifdef __arm__ + case __ARM_NR_breakpoint: + case __ARM_NR_cacheflush: + case __ARM_NR_usr26: // FIXME: do we actually need this? + case __ARM_NR_usr32: + case __ARM_NR_set_tls: + return Allow(); +#endif + + // Needed when being debugged: + case __NR_restart_syscall: + return Allow(); + + // Terminate threads or the process + case __NR_exit: + case __NR_exit_group: + return Allow(); + + case __NR_getrandom: + return Allow(); + + // Used by almost every process: GMP needs them for Clearkey + // because of bug 1576006 (but may not need them for other + // plugin types; see bug 1737092). Given that fstat is + // allowed, the uid/gid are probably available anyway. + CASES_FOR_getuid: + CASES_FOR_getgid: + CASES_FOR_geteuid: + CASES_FOR_getegid: + return Allow(); + +#ifdef DESKTOP + // Bug 1543858: glibc's qsort calls sysinfo to check the + // memory size; it falls back to assuming there's enough RAM. + case __NR_sysinfo: + return Error(EPERM); +#endif + + // Bug 1651701: an API for restartable atomic sequences and + // per-CPU data; exposing information about CPU numbers and + // when threads are migrated or preempted isn't great but the + // risk should be relatively low. + case __NR_rseq: + return Allow(); + + case __NR_ioctl: { + Arg request(1); +#ifdef MOZ_ASAN + Arg fd(0); +#endif // MOZ_ASAN + // Make isatty() return false, because none of the terminal + // ioctls will be allowed; libraries sometimes call this for + // various reasons (e.g., to decide whether to emit ANSI/VT + // color codes when logging to stderr). glibc uses TCGETS and + // musl uses TIOCGWINSZ. + // + // This is required by ffmpeg + return If(AnyOf(request == TCGETS, request == TIOCGWINSZ), + Error(ENOTTY)) +#ifdef MOZ_ASAN + // ASAN's error reporter wants to know if stderr is a tty. + .ElseIf(fd == STDERR_FILENO, Error(ENOTTY)) +#endif // MOZ_ASAN + .Else(SandboxPolicyBase::EvaluateSyscall(sysno)); + } + + CASES_FOR_dup2: // See ConnectTrapCommon + if (mBrokeredConnect) { + return Allow(); + } + return SandboxPolicyBase::EvaluateSyscall(sysno); + +#ifdef MOZ_ASAN + // ...and before compiler-rt r209773, it will call readlink on + // /proc/self/exe and use the cached value only if that fails: + case __NR_readlink: + case __NR_readlinkat: + return Error(ENOENT); + + // ...and if it found an external symbolizer, it will try to run it: + // (See also bug 1081242 comment #7.) + CASES_FOR_stat: + return Error(ENOENT); +#endif // MOZ_ASAN + + // Replace statfs with open (which may be brokered) and + // fstatfs (which is not allowed in this policy, but may be + // allowed by subclasses if they wish to enable statfs). + CASES_FOR_statfs: + return Trap(StatFsTrap, nullptr); + + default: + return SandboxPolicyBase::EvaluateSyscall(sysno); + } + } +}; + +// The process-type-specific syscall rules start here: + +// The seccomp-bpf filter for content processes is not a true sandbox +// on its own; its purpose is attack surface reduction and syscall +// interception in support of a semantic sandboxing layer. On B2G +// this is the Android process permission model; on desktop, +// namespaces and chroot() will be used. +class ContentSandboxPolicy : public SandboxPolicyCommon { + private: + ContentProcessSandboxParams mParams; + bool mAllowSysV; + bool mUsingRenderDoc; + + bool BelowLevel(int aLevel) const { return mParams.mLevel < aLevel; } + ResultExpr AllowBelowLevel(int aLevel, ResultExpr aOrElse) const { + return BelowLevel(aLevel) ? Allow() : std::move(aOrElse); + } + ResultExpr AllowBelowLevel(int aLevel) const { + return AllowBelowLevel(aLevel, InvalidSyscall()); + } + + static intptr_t GetPPidTrap(ArgsRef aArgs, void* aux) { + // In a pid namespace, getppid() will return 0. We will return 0 instead + // of the real parent pid to see what breaks when we introduce the + // pid namespace (Bug 1151624). + return 0; + } + + public: + ContentSandboxPolicy(SandboxBrokerClient* aBroker, + ContentProcessSandboxParams&& aParams) + : mParams(std::move(aParams)), + mAllowSysV(PR_GetEnv("MOZ_SANDBOX_ALLOW_SYSV") != nullptr), + mUsingRenderDoc(PR_GetEnv("RENDERDOC_CAPTUREOPTS") != nullptr) { + mBroker = aBroker; + mMayCreateShmem = true; + mAllowUnsafeSocketPair = true; + mBrokeredConnect = true; + } + + ~ContentSandboxPolicy() override = default; + + Maybe EvaluateSocketCall(int aCall, + bool aHasArgs) const override { + switch (aCall) { + case SYS_SENDMMSG: // libresolv via libasyncns; see bug 1355274 + return Some(Allow()); + +#ifdef ANDROID + case SYS_SOCKET: + return Some(Error(EACCES)); +#else // #ifdef DESKTOP + case SYS_SOCKET: + case SYS_CONNECT: + if (BelowLevel(4)) { + return Some(Allow()); + } + return SandboxPolicyCommon::EvaluateSocketCall(aCall, aHasArgs); + + // FIXME (bug 1761134): sockopts should be filtered + case SYS_GETSOCKOPT: + case SYS_SETSOCKOPT: + // These next 3 were needed for X11; they may not be needed + // with X11 lockdown, but there's not much attack surface here. + case SYS_GETSOCKNAME: + case SYS_GETPEERNAME: + case SYS_SHUTDOWN: + return Some(Allow()); + + case SYS_ACCEPT: + case SYS_ACCEPT4: + if (mUsingRenderDoc) { + return Some(Allow()); + } + [[fallthrough]]; +#endif + default: + return SandboxPolicyCommon::EvaluateSocketCall(aCall, aHasArgs); + } + } + +#ifdef DESKTOP + Maybe EvaluateIpcCall(int aCall, int aArgShift) const override { + switch (aCall) { + // These are a problem: SysV IPC follows the Unix "same uid + // policy" and can't be restricted/brokered like file access. + // We're not using it directly, but there are some library + // dependencies that do; see ContentNeedsSysVIPC() in + // SandboxLaunch.cpp. Also, Cairo as used by GTK will sometimes + // try to use MIT-SHM, so shmget() is a non-fatal error. See + // also bug 1376910 and bug 1438401. + case SHMGET: + return Some(mAllowSysV ? Allow() : Error(EPERM)); + case SHMCTL: + case SHMAT: + case SHMDT: + case SEMGET: + case SEMCTL: + case SEMOP: + if (mAllowSysV) { + return Some(Allow()); + } + return SandboxPolicyCommon::EvaluateIpcCall(aCall, aArgShift); + default: + return SandboxPolicyCommon::EvaluateIpcCall(aCall, aArgShift); + } + } +#endif + +#ifdef MOZ_PULSEAUDIO + ResultExpr PrctlPolicy() const override { + if (BelowLevel(4)) { + Arg op(0); + return If(op == PR_GET_NAME, Allow()) + .Else(SandboxPolicyCommon::PrctlPolicy()); + } + return SandboxPolicyCommon::PrctlPolicy(); + } +#endif + + ResultExpr EvaluateSyscall(int sysno) const override { + // Straight allow for anything that got overriden via prefs + const auto& whitelist = mParams.mSyscallWhitelist; + if (std::find(whitelist.begin(), whitelist.end(), sysno) != + whitelist.end()) { + if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) { + SANDBOX_LOG("Allowing syscall nr %d via whitelist", sysno); + } + return Allow(); + } + + // Level 1 has been removed. If seccomp-bpf is used, then we're + // necessarily at level >= 2 and filesystem access is brokered. + MOZ_ASSERT(!BelowLevel(2)); + MOZ_ASSERT(mBroker); + + switch (sysno) { +#ifdef DESKTOP + case __NR_getppid: + return Trap(GetPPidTrap, nullptr); + + // GTK's theme parsing tries to getcwd() while sandboxed, but + // only during Talos runs. + case __NR_getcwd: + return Error(ENOENT); + +# ifdef MOZ_PULSEAUDIO + CASES_FOR_fchown: + case __NR_fchmod: + return AllowBelowLevel(4); +# endif + CASES_FOR_fstatfs: // fontconfig, pulseaudio, GIO (see also statfs) + case __NR_flock: // graphics + return Allow(); + + // Bug 1354731: proprietary GL drivers try to mknod() their devices +# ifdef __NR_mknod + case __NR_mknod: +# endif + case __NR_mknodat: { + Arg mode(sysno == __NR_mknodat ? 2 : 1); + return If((mode & S_IFMT) == S_IFCHR, Error(EPERM)) + .Else(InvalidSyscall()); + } + // Bug 1438389: ...and nvidia GL will sometimes try to chown the devices +# ifdef __NR_chown + case __NR_chown: +# endif + case __NR_fchownat: + return Error(EPERM); +#endif + + CASES_FOR_select: + return Allow(); + + case __NR_writev: +#ifdef DESKTOP + case __NR_pwrite64: + case __NR_readahead: +#endif + return Allow(); + + case __NR_ioctl: { +#ifdef MOZ_ALSA + if (BelowLevel(4)) { + return Allow(); + } +#endif + Arg request(1); + auto shifted_type = request & kIoctlTypeMask; + + // Rust's stdlib seems to use FIOCLEX instead of equivalent fcntls. + return If(request == FIOCLEX, Allow()) + // Rust's stdlib also uses FIONBIO instead of equivalent fcntls. + .ElseIf(request == FIONBIO, Allow()) + // Allow anything that isn't a tty ioctl, for now; bug 1302711 + // will cover changing this to a default-deny policy. + .ElseIf(shifted_type != kTtyIoctls, Allow()) + .Else(SandboxPolicyCommon::EvaluateSyscall(sysno)); + } + + CASES_FOR_fcntl: { + Arg cmd(1); + return Switch(cmd) + // Nvidia GL and fontconfig (newer versions) use fcntl file locking. + .Case(F_SETLK, Allow()) +#ifdef F_SETLK64 + .Case(F_SETLK64, Allow()) +#endif + // Pulseaudio uses F_SETLKW, as does fontconfig. + .Case(F_SETLKW, Allow()) +#ifdef F_SETLKW64 + .Case(F_SETLKW64, Allow()) +#endif + // Wayland client libraries use file seals + .Case(F_ADD_SEALS, Allow()) + .Case(F_GET_SEALS, Allow()) + .Default(SandboxPolicyCommon::EvaluateSyscall(sysno)); + } + + case __NR_brk: + // FIXME(bug 1510861) are we using any hints that aren't allowed + // in SandboxPolicyCommon now? + case __NR_madvise: + return Allow(); + + // wasm uses mremap (always with zero flags) + case __NR_mremap: { + Arg flags(3); + return If(flags == 0, Allow()) + .Else(SandboxPolicyCommon::EvaluateSyscall(sysno)); + } + + // Bug 1462640: Mesa libEGL uses mincore to test whether values + // are pointers, for reasons. + case __NR_mincore: { + Arg length(1); + return If(length == getpagesize(), Allow()) + .Else(SandboxPolicyCommon::EvaluateSyscall(sysno)); + } + +#ifdef __NR_set_thread_area + case __NR_set_thread_area: + return Allow(); +#endif + + case __NR_getrusage: + case __NR_times: + return Allow(); + + case __NR_fsync: + case __NR_msync: + return Allow(); + + case __NR_getpriority: + case __NR_setpriority: + case __NR_sched_getattr: + case __NR_sched_setattr: + case __NR_sched_get_priority_min: + case __NR_sched_get_priority_max: + case __NR_sched_getscheduler: + case __NR_sched_setscheduler: + case __NR_sched_getparam: + case __NR_sched_setparam: +#ifdef DESKTOP + case __NR_sched_getaffinity: +#endif + return Allow(); + +#ifdef DESKTOP + case __NR_sched_setaffinity: + return Error(EPERM); +#endif + +#ifdef DESKTOP + case __NR_pipe2: { + // Restrict the flags; O_NOTIFICATION_PIPE in particular + // exposes enough attack surface to be a cause for concern + // (bug 1808320). O_DIRECT isn't known to be used currently + // (Try passes with it blocked), but should be low-risk, and + // Chromium allows it. + static constexpr int allowed_flags = O_CLOEXEC | O_NONBLOCK | O_DIRECT; + Arg flags(1); + return If((flags & ~allowed_flags) == 0, Allow()) + .Else(InvalidSyscall()); + } + + CASES_FOR_getrlimit: + CASES_FOR_getresuid: + CASES_FOR_getresgid: + return Allow(); + + case __NR_prlimit64: { + // Allow only the getrlimit() use case. (glibc seems to use + // only pid 0 to indicate the current process; pid == getpid() + // is equivalent and could also be allowed if needed.) + Arg pid(0); + // This is really a const struct ::rlimit*, but Arg<> doesn't + // work with pointers, only integer types. + Arg new_limit(2); + return If(AllOf(pid == 0, new_limit == 0), Allow()) + .Else(InvalidSyscall()); + } + + // PulseAudio calls umask, even though it's unsafe in + // multithreaded applications. But, allowing it here doesn't + // really do anything one way or the other, now that file + // accesses are brokered to another process. + case __NR_umask: + return AllowBelowLevel(4); + + case __NR_kill: { + if (BelowLevel(4)) { + Arg sig(1); + // PulseAudio uses kill(pid, 0) to check if purported owners of + // shared memory files are still alive; see bug 1397753 for more + // details. + return If(sig == 0, Error(EPERM)).Else(InvalidSyscall()); + } + return InvalidSyscall(); + } + + case __NR_wait4: +# ifdef __NR_waitpid + case __NR_waitpid: +# endif + // NSPR will start a thread to wait for child processes even if + // fork() fails; see bug 227246 and bug 1299581. + return Error(ECHILD); + + case __NR_eventfd2: + return Allow(); + +# ifdef __NR_rt_tgsigqueueinfo + // Only allow to send signals within the process. + case __NR_rt_tgsigqueueinfo: { + Arg tgid(0); + return If(tgid == getpid(), Allow()).Else(InvalidSyscall()); + } +# endif + + case __NR_mlock: + case __NR_munlock: + return Allow(); + + // We can't usefully allow fork+exec, even on a temporary basis; + // the child would inherit the seccomp-bpf policy and almost + // certainly die from an unexpected SIGSYS. We also can't have + // fork() crash, currently, because there are too many system + // libraries/plugins that try to run commands. But they can + // usually do something reasonable on error. + case __NR_clone: + return ClonePolicy(Error(EPERM)); + + case __NR_clone3: + return Error(ENOSYS); + +# ifdef __NR_fadvise64 + case __NR_fadvise64: + return Allow(); +# endif + +# ifdef __NR_fadvise64_64 + case __NR_fadvise64_64: + return Allow(); +# endif + + case __NR_fallocate: + return Allow(); + + case __NR_get_mempolicy: + return Allow(); + + // Required by libnuma for FFmpeg + case __NR_set_mempolicy: + return Error(ENOSYS); + + case __NR_kcmp: + return KcmpPolicyForMesa(); + +#endif // DESKTOP + + // nsSystemInfo uses uname (and we cache an instance, so + // the info remains present even if we block the syscall) + case __NR_uname: +#ifdef DESKTOP + case __NR_sysinfo: +#endif + return Allow(); + +#ifdef MOZ_JPROF + case __NR_setitimer: + return Allow(); +#endif // MOZ_JPROF + + default: + return SandboxPolicyCommon::EvaluateSyscall(sysno); + } + } +}; + +UniquePtr GetContentSandboxPolicy( + SandboxBrokerClient* aMaybeBroker, ContentProcessSandboxParams&& aParams) { + return MakeUnique(aMaybeBroker, std::move(aParams)); +} + +// Unlike for content, the GeckoMediaPlugin seccomp-bpf policy needs +// to be an effective sandbox by itself, because we allow GMP on Linux +// systems where that's the only sandboxing mechanism we can use. +// +// Be especially careful about what this policy allows. +class GMPSandboxPolicy : public SandboxPolicyCommon { + static intptr_t OpenTrap(const sandbox::arch_seccomp_data& aArgs, void* aux) { + const auto files = static_cast(aux); + const char* path; + int flags; + + switch (aArgs.nr) { +#ifdef __NR_open + case __NR_open: + path = reinterpret_cast(aArgs.args[0]); + flags = static_cast(aArgs.args[1]); + break; +#endif + case __NR_openat: + // The path has to be absolute to match the pre-opened file (see + // assertion in ctor) so the dirfd argument is ignored. + path = reinterpret_cast(aArgs.args[1]); + flags = static_cast(aArgs.args[2]); + break; + default: + MOZ_CRASH("unexpected syscall number"); + } + + if ((flags & O_ACCMODE) != O_RDONLY) { + SANDBOX_LOG("non-read-only open of file %s attempted (flags=0%o)", path, + flags); + return -EROFS; + } + int fd = files->GetDesc(path); + if (fd < 0) { + // SandboxOpenedFile::GetDesc already logged about this, if appropriate. + return -ENOENT; + } + return fd; + } + + static intptr_t UnameTrap(const sandbox::arch_seccomp_data& aArgs, + void* aux) { + const auto buf = reinterpret_cast(aArgs.args[0]); + PodZero(buf); + // The real uname() increases fingerprinting risk for no benefit. + // This is close enough. + strcpy(buf->sysname, "Linux"); + strcpy(buf->version, "3"); + return 0; + } + + static intptr_t FcntlTrap(const sandbox::arch_seccomp_data& aArgs, + void* aux) { + const auto cmd = static_cast(aArgs.args[1]); + switch (cmd) { + // This process can't exec, so the actual close-on-exec flag + // doesn't matter; have it always read as true and ignore writes. + case F_GETFD: + return O_CLOEXEC; + case F_SETFD: + return 0; + default: + return -ENOSYS; + } + } + + const SandboxOpenedFiles* mFiles; + + public: + explicit GMPSandboxPolicy(const SandboxOpenedFiles* aFiles) : mFiles(aFiles) { + // Used by the profiler to send data back to the parent process; + // we are not enabling the file broker, so this will only work if + // memfd_create is available. + mMayCreateShmem = true; + } + + ~GMPSandboxPolicy() override = default; + + ResultExpr EvaluateSyscall(int sysno) const override { + switch (sysno) { + // Simulate opening the plugin file. +#ifdef __NR_open + case __NR_open: +#endif + case __NR_openat: + return Trap(OpenTrap, mFiles); + + case __NR_brk: + return Allow(); + case __NR_sched_get_priority_min: + case __NR_sched_get_priority_max: + return Allow(); + case __NR_sched_getparam: + case __NR_sched_getscheduler: + case __NR_sched_setscheduler: { + Arg pid(0); + return If(pid == 0, Allow()).Else(Trap(SchedTrap, nullptr)); + } + + // For clock(3) on older glibcs; bug 1304220. + case __NR_times: + return Allow(); + + // Bug 1372428 + case __NR_uname: + return Trap(UnameTrap, nullptr); + CASES_FOR_fcntl: + return Trap(FcntlTrap, nullptr); + + // Allow the same advice values as the default policy, but return + // Error(ENOSYS) for other values. Because the Widevine CDM may probe + // advice arguments, including invalid values, we don't want to return + // InvalidSyscall(), as this will crash the process. So instead just + // indicate such calls are not available. + case __NR_madvise: { + Arg advice(2); + return If(advice == MADV_DONTNEED, Allow()) + .ElseIf(advice == MADV_FREE, Allow()) + .ElseIf(advice == MADV_HUGEPAGE, Allow()) + .ElseIf(advice == MADV_NOHUGEPAGE, Allow()) +#ifdef MOZ_ASAN + .ElseIf(advice == MADV_DONTDUMP, Allow()) +#endif + .ElseIf(advice == MADV_MERGEABLE, Error(EPERM)) // bug 1705045 + .Else(Error(ENOSYS)); + } + + // The profiler will try to readlink /proc/self/exe for native + // stackwalking, but that's broken for several other reasons; + // see discussion in bug 1770905. (That can be emulated by + // pre-recording the result if/when we need it.) +#ifdef __NR_readlink + case __NR_readlink: +#endif + case __NR_readlinkat: + return Error(EINVAL); + + default: + return SandboxPolicyCommon::EvaluateSyscall(sysno); + } + } +}; + +UniquePtr GetMediaSandboxPolicy( + const SandboxOpenedFiles* aFiles) { + return UniquePtr(new GMPSandboxPolicy(aFiles)); +} + +// The policy for the data decoder process is similar to the one for +// media plugins, but the codec code is all in-tree so it's better +// behaved and doesn't need special exceptions (or the ability to load +// a plugin file). However, it does directly create shared memory +// segments, so it may need file brokering. +class RDDSandboxPolicy final : public SandboxPolicyCommon { + public: + explicit RDDSandboxPolicy(SandboxBrokerClient* aBroker) { + mBroker = aBroker; + mMayCreateShmem = true; + } + +#ifndef ANDROID + Maybe EvaluateIpcCall(int aCall, int aArgShift) const override { + // The Intel media driver uses SysV IPC (semaphores and shared + // memory) on newer hardware models; it always uses this fixed + // key, so we can restrict semget and shmget. Unfortunately, the + // calls that operate on these resources take "identifiers", which + // are unpredictable (by us) but guessable (by an adversary). + static constexpr key_t kIntelKey = 'D' << 24 | 'V' << 8 | 'X' << 0; + + switch (aCall) { + case SEMGET: + case SHMGET: { + Arg key(0 + aArgShift); + return Some(If(key == kIntelKey, Allow()).Else(InvalidSyscall())); + } + + case SEMCTL: + case SEMOP: + case SEMTIMEDOP: + case SHMCTL: + case SHMAT: + case SHMDT: + return Some(Allow()); + + default: + return SandboxPolicyCommon::EvaluateIpcCall(aCall, aArgShift); + } + } +#endif + + Maybe EvaluateSocketCall(int aCall, + bool aHasArgs) const override { + switch (aCall) { + // These are for X11. + case SYS_GETSOCKNAME: + case SYS_GETPEERNAME: + case SYS_SHUTDOWN: + return Some(Allow()); + +#ifdef MOZ_ENABLE_V4L2 + case SYS_SOCKET: + // Hardware-accelerated decode uses EGL to manage hardware surfaces. + // When initialised it tries to connect to the Wayland server over a + // UNIX socket. It still works fine if it can't connect to Wayland, so + // don't let it create the socket (but don't kill the process for + // trying). + return Some(Error(EACCES)); +#endif + + default: + return SandboxPolicyCommon::EvaluateSocketCall(aCall, aHasArgs); + } + } + + ResultExpr EvaluateSyscall(int sysno) const override { + switch (sysno) { + case __NR_getrusage: + return Allow(); + + case __NR_ioctl: { + Arg request(1); + auto shifted_type = request & kIoctlTypeMask; + static constexpr unsigned long kDrmType = + static_cast('d') << _IOC_TYPESHIFT; + // Note: 'b' is also the Binder device on Android. + static constexpr unsigned long kDmaBufType = + static_cast('b') << _IOC_TYPESHIFT; +#ifdef MOZ_ENABLE_V4L2 + // Type 'V' for V4L2, used for hw accelerated decode + static constexpr unsigned long kVideoType = + static_cast('V') << _IOC_TYPESHIFT; +#endif + // nvidia uses some ioctls from this range (but not actual + // fbdev ioctls; nvidia uses values >= 200 for the NR field + // (low 8 bits)) + static constexpr unsigned long kFbDevType = + static_cast('F') << _IOC_TYPESHIFT; + + // Allow DRI and DMA-Buf for VA-API. Also allow V4L2 if enabled + return If(shifted_type == kDrmType, Allow()) + .ElseIf(shifted_type == kDmaBufType, Allow()) +#ifdef MOZ_ENABLE_V4L2 + .ElseIf(shifted_type == kVideoType, Allow()) +#endif + // Hack for nvidia, which isn't supported yet: + .ElseIf(shifted_type == kFbDevType, Error(ENOTTY)) + .Else(SandboxPolicyCommon::EvaluateSyscall(sysno)); + } + + // Mesa/amdgpu + case __NR_kcmp: + return KcmpPolicyForMesa(); + + // We use this in our DMABuf support code. + case __NR_eventfd2: + return Allow(); + + // Allow the sched_* syscalls for the current thread only. + // Mesa attempts to use them to optimize performance; often + // this involves passing other threads' tids, which we can't + // safely allow, but maybe a future Mesa version could fix that. + case __NR_sched_getaffinity: + case __NR_sched_setaffinity: + case __NR_sched_getparam: + case __NR_sched_setparam: + case __NR_sched_getscheduler: + case __NR_sched_setscheduler: + case __NR_sched_getattr: + case __NR_sched_setattr: { + Arg pid(0); + return If(pid == 0, Allow()).Else(Trap(SchedTrap, nullptr)); + } + + // The priority bounds are also used, sometimes (bug 1838675): + case __NR_sched_get_priority_min: + case __NR_sched_get_priority_max: + return Allow(); + + // Mesa sometimes wants to know the OS version. + case __NR_uname: + return Allow(); + + // nvidia tries to mknod(!) its devices; that won't work anyway, + // so quietly reject it. +#ifdef __NR_mknod + case __NR_mknod: +#endif + case __NR_mknodat: + return Error(EPERM); + + // Used by the nvidia GPU driver, including in multi-GPU + // systems when we intend to use a non-nvidia GPU. (Also used + // by Mesa for its shader cache, but we disable that in this + // process.) + CASES_FOR_fstatfs: + return Allow(); + + // Pass through the common policy. + default: + return SandboxPolicyCommon::EvaluateSyscall(sysno); + } + } +}; + +UniquePtr GetDecoderSandboxPolicy( + SandboxBrokerClient* aMaybeBroker) { + return UniquePtr( + new RDDSandboxPolicy(aMaybeBroker)); +} + +// Basically a clone of RDDSandboxPolicy until we know exactly what +// the SocketProcess sandbox looks like. +class SocketProcessSandboxPolicy final : public SandboxPolicyCommon { + public: + explicit SocketProcessSandboxPolicy(SandboxBrokerClient* aBroker) { + mBroker = aBroker; + mMayCreateShmem = true; + } + + static intptr_t FcntlTrap(const sandbox::arch_seccomp_data& aArgs, + void* aux) { + const auto cmd = static_cast(aArgs.args[1]); + switch (cmd) { + // This process can't exec, so the actual close-on-exec flag + // doesn't matter; have it always read as true and ignore writes. + case F_GETFD: + return O_CLOEXEC; + case F_SETFD: + return 0; + default: + return -ENOSYS; + } + } + + Maybe EvaluateSocketCall(int aCall, + bool aHasArgs) const override { + switch (aCall) { + case SYS_SOCKET: + case SYS_CONNECT: + case SYS_BIND: + return Some(Allow()); + + // FIXME(bug 1641401) do we really need this? + case SYS_SENDMMSG: + return Some(Allow()); + + case SYS_GETSOCKOPT: + case SYS_SETSOCKOPT: + case SYS_GETSOCKNAME: + case SYS_GETPEERNAME: + case SYS_SHUTDOWN: + case SYS_ACCEPT: + case SYS_ACCEPT4: + return Some(Allow()); + + default: + return SandboxPolicyCommon::EvaluateSocketCall(aCall, aHasArgs); + } + } + + ResultExpr PrctlPolicy() const override { + Arg op(0); + Arg arg2(1); + return Switch(op) + .CASES((PR_SET_VMA), // Tagging of anonymous memory mappings + If(arg2 == PR_SET_VMA_ANON_NAME, Allow()).Else(InvalidSyscall())) + .CASES((PR_SET_NAME, // Thread creation + PR_SET_DUMPABLE, // Crash reporting + PR_SET_PTRACER), // Debug-mode crash handling + Allow()) + .Default(InvalidSyscall()); + } + + ResultExpr EvaluateSyscall(int sysno) const override { + switch (sysno) { + case __NR_getrusage: + return Allow(); + + case __NR_ioctl: { + Arg request(1); + auto shifted_type = request & kIoctlTypeMask; + + // Rust's stdlib seems to use FIOCLEX instead of equivalent fcntls. + return If(request == FIOCLEX, Allow()) + // Rust's stdlib also uses FIONBIO instead of equivalent fcntls. + .ElseIf(request == FIONBIO, Allow()) + // This is used by PR_Available in nsSocketInputStream::Available. + .ElseIf(request == FIONREAD, Allow()) + // Allow anything that isn't a tty ioctl, for now; bug 1302711 + // will cover changing this to a default-deny policy. + .ElseIf(shifted_type != kTtyIoctls, Allow()) + .Else(SandboxPolicyCommon::EvaluateSyscall(sysno)); + } + + CASES_FOR_fcntl: { + Arg cmd(1); + return Switch(cmd) + .Case(F_DUPFD_CLOEXEC, Allow()) + // Nvidia GL and fontconfig (newer versions) use fcntl file locking. + .Case(F_SETLK, Allow()) +#ifdef F_SETLK64 + .Case(F_SETLK64, Allow()) +#endif + // Pulseaudio uses F_SETLKW, as does fontconfig. + .Case(F_SETLKW, Allow()) +#ifdef F_SETLKW64 + .Case(F_SETLKW64, Allow()) +#endif + .Default(SandboxPolicyCommon::EvaluateSyscall(sysno)); + } + +#ifdef DESKTOP + // This section is borrowed from ContentSandboxPolicy + CASES_FOR_getrlimit: + CASES_FOR_getresuid: + CASES_FOR_getresgid: + return Allow(); + + case __NR_prlimit64: { + // Allow only the getrlimit() use case. (glibc seems to use + // only pid 0 to indicate the current process; pid == getpid() + // is equivalent and could also be allowed if needed.) + Arg pid(0); + // This is really a const struct ::rlimit*, but Arg<> doesn't + // work with pointers, only integer types. + Arg new_limit(2); + return If(AllOf(pid == 0, new_limit == 0), Allow()) + .Else(InvalidSyscall()); + } +#endif // DESKTOP + + // Bug 1640612 + case __NR_uname: + return Allow(); + + default: + return SandboxPolicyCommon::EvaluateSyscall(sysno); + } + } +}; + +UniquePtr GetSocketProcessSandboxPolicy( + SandboxBrokerClient* aMaybeBroker) { + return UniquePtr( + new SocketProcessSandboxPolicy(aMaybeBroker)); +} + +class UtilitySandboxPolicy : public SandboxPolicyCommon { + public: + explicit UtilitySandboxPolicy(SandboxBrokerClient* aBroker) { + mBroker = aBroker; + mMayCreateShmem = true; + } + + ResultExpr PrctlPolicy() const override { + Arg op(0); + Arg arg2(1); + return Switch(op) + .CASES((PR_SET_VMA), // Tagging of anonymous memory mappings + If(arg2 == PR_SET_VMA_ANON_NAME, Allow()).Else(InvalidSyscall())) + .CASES((PR_SET_NAME, // Thread creation + PR_SET_DUMPABLE, // Crash reporting + PR_SET_PTRACER, // Debug-mode crash handling + PR_GET_PDEATHSIG), // PGO profiling, cf + // https://reviews.llvm.org/D29954 + Allow()) + .Default(InvalidSyscall()); + } + + ResultExpr EvaluateSyscall(int sysno) const override { + switch (sysno) { + case __NR_getrusage: + return Allow(); + + // Required by FFmpeg + case __NR_get_mempolicy: + return Allow(); + + // Required by libnuma for FFmpeg + case __NR_sched_getaffinity: { + Arg pid(0); + return If(pid == 0, Allow()).Else(Trap(SchedTrap, nullptr)); + } + + // Required by libnuma for FFmpeg + case __NR_set_mempolicy: + return Error(ENOSYS); + + // Pass through the common policy. + default: + return SandboxPolicyCommon::EvaluateSyscall(sysno); + } + } +}; + +UniquePtr GetUtilitySandboxPolicy( + SandboxBrokerClient* aMaybeBroker) { + return UniquePtr( + new UtilitySandboxPolicy(aMaybeBroker)); +} + +} // namespace mozilla diff --git a/security/sandbox/linux/SandboxFilter.h b/security/sandbox/linux/SandboxFilter.h new file mode 100644 index 0000000000..04a37a32d4 --- /dev/null +++ b/security/sandbox/linux/SandboxFilter.h @@ -0,0 +1,46 @@ +/* -*- 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_SandboxFilter_h +#define mozilla_SandboxFilter_h + +#include +#include "mozilla/Atomics.h" +#include "mozilla/Range.h" +#include "mozilla/UniquePtr.h" + +namespace sandbox { +namespace bpf_dsl { +class Policy; +} +} // namespace sandbox + +namespace mozilla { +class SandboxBrokerClient; + +struct ContentProcessSandboxParams; + +UniquePtr GetContentSandboxPolicy( + SandboxBrokerClient* aMaybeBroker, ContentProcessSandboxParams&& aParams); + +class SandboxOpenedFiles; + +// The SandboxOpenedFiles object must live until the process exits. +UniquePtr GetMediaSandboxPolicy( + const SandboxOpenedFiles* aFiles); + +UniquePtr GetDecoderSandboxPolicy( + SandboxBrokerClient* aMaybeBroker); + +UniquePtr GetSocketProcessSandboxPolicy( + SandboxBrokerClient* aMaybeBroker); + +UniquePtr GetUtilitySandboxPolicy( + SandboxBrokerClient* aMaybeBroker); + +} // namespace mozilla + +#endif diff --git a/security/sandbox/linux/SandboxFilterUtil.cpp b/security/sandbox/linux/SandboxFilterUtil.cpp new file mode 100644 index 0000000000..de065d5483 --- /dev/null +++ b/security/sandbox/linux/SandboxFilterUtil.cpp @@ -0,0 +1,142 @@ +/* -*- 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 "SandboxFilterUtil.h" + +#ifndef ANDROID +# include +#endif +#include +#include +#include +#include + +#include "mozilla/UniquePtr.h" +#include "sandbox/linux/bpf_dsl/bpf_dsl.h" + +// Older kernel headers (mostly Android, but also some older desktop +// distributions) are missing some or all of these: +#ifndef SYS_ACCEPT4 +# define SYS_ACCEPT4 18 +#endif +#ifndef SYS_RECVMMSG +# define SYS_RECVMMSG 19 +#endif +#ifndef SYS_SENDMMSG +# define SYS_SENDMMSG 20 +#endif + +using namespace sandbox::bpf_dsl; +#define CASES SANDBOX_BPF_DSL_CASES + +namespace mozilla { + +sandbox::bpf_dsl::ResultExpr SandboxPolicyBase::EvaluateSyscall( + int aSysno) const { + switch (aSysno) { +#ifdef __NR_socketcall + case __NR_socketcall: { + Arg call(0); + UniquePtr> acc(new Caser(Switch(call))); + for (int i = SYS_SOCKET; i <= SYS_SENDMMSG; ++i) { + auto thisCase = EvaluateSocketCall(i, false); + // Optimize out cases that are equal to the default. + if (thisCase) { + acc.reset(new Caser(acc->Case(i, *thisCase))); + } + } + return acc->Default(InvalidSyscall()); + } +# ifndef ANDROID + case __NR_ipc: { + Arg callAndVersion(0); + auto call = callAndVersion & 0xFFFF; + UniquePtr> acc(new Caser(Switch(call))); + for (int i = SEMOP; i <= DIPC; ++i) { + auto thisCase = EvaluateIpcCall(i, 1); + // Optimize out cases that are equal to the default. + if (thisCase) { + acc.reset(new Caser(acc->Case(i, *thisCase))); + } + } + return acc->Default(InvalidSyscall()); + } +# endif // ANDROID +#endif // __NR_socketcall + // clang-format off +#define DISPATCH_SOCKETCALL(sysnum, socketnum) \ + case sysnum: \ + return EvaluateSocketCall(socketnum, true).valueOr(InvalidSyscall()) + DISPATCH_SOCKETCALL(__NR_socket, SYS_SOCKET); + DISPATCH_SOCKETCALL(__NR_bind, SYS_BIND); + DISPATCH_SOCKETCALL(__NR_connect, SYS_CONNECT); + DISPATCH_SOCKETCALL(__NR_listen, SYS_LISTEN); +#ifdef __NR_accept + DISPATCH_SOCKETCALL(__NR_accept, SYS_ACCEPT); +#endif + DISPATCH_SOCKETCALL(__NR_getsockname, SYS_GETSOCKNAME); + DISPATCH_SOCKETCALL(__NR_getpeername, SYS_GETPEERNAME); + DISPATCH_SOCKETCALL(__NR_socketpair, SYS_SOCKETPAIR); +#ifdef __NR_send + DISPATCH_SOCKETCALL(__NR_send, SYS_SEND); + DISPATCH_SOCKETCALL(__NR_recv, SYS_RECV); +#endif // __NR_send + DISPATCH_SOCKETCALL(__NR_sendto, SYS_SENDTO); + DISPATCH_SOCKETCALL(__NR_recvfrom, SYS_RECVFROM); + DISPATCH_SOCKETCALL(__NR_shutdown, SYS_SHUTDOWN); + DISPATCH_SOCKETCALL(__NR_setsockopt, SYS_SETSOCKOPT); + DISPATCH_SOCKETCALL(__NR_getsockopt, SYS_GETSOCKOPT); + DISPATCH_SOCKETCALL(__NR_sendmsg, SYS_SENDMSG); + DISPATCH_SOCKETCALL(__NR_recvmsg, SYS_RECVMSG); + DISPATCH_SOCKETCALL(__NR_accept4, SYS_ACCEPT4); + DISPATCH_SOCKETCALL(__NR_recvmmsg, SYS_RECVMMSG); + DISPATCH_SOCKETCALL(__NR_sendmmsg, SYS_SENDMMSG); +#undef DISPATCH_SOCKETCALL +#ifndef __NR_socketcall +#ifndef ANDROID +#define DISPATCH_SYSVCALL(sysnum, ipcnum) \ + case sysnum: \ + return EvaluateIpcCall(ipcnum, 0).valueOr(InvalidSyscall()) + DISPATCH_SYSVCALL(__NR_semop, SEMOP); + DISPATCH_SYSVCALL(__NR_semget, SEMGET); + DISPATCH_SYSVCALL(__NR_semctl, SEMCTL); + DISPATCH_SYSVCALL(__NR_semtimedop, SEMTIMEDOP); + DISPATCH_SYSVCALL(__NR_msgsnd, MSGSND); + DISPATCH_SYSVCALL(__NR_msgrcv, MSGRCV); + DISPATCH_SYSVCALL(__NR_msgget, MSGGET); + DISPATCH_SYSVCALL(__NR_msgctl, MSGCTL); + DISPATCH_SYSVCALL(__NR_shmat, SHMAT); + DISPATCH_SYSVCALL(__NR_shmdt, SHMDT); + DISPATCH_SYSVCALL(__NR_shmget, SHMGET); + DISPATCH_SYSVCALL(__NR_shmctl, SHMCTL); +#undef DISPATCH_SYSVCALL +#endif // ANDROID +#endif // __NR_socketcall + // clang-format on + default: + return InvalidSyscall(); + } +} + +/* static */ bool SandboxPolicyBase::HasSeparateSocketCalls() { +#ifdef __NR_socketcall + // If we have both syscalls, dynamically detect (and cache). + static const bool kCache = [] { + int fd = syscall(__NR_socket, AF_LOCAL, SOCK_STREAM, 0); + if (fd < 0) { + MOZ_DIAGNOSTIC_ASSERT(errno == ENOSYS); + return false; + } + close(fd); + return true; + }(); + return kCache; +#else // no socketcall; must be separate syscalls + return true; +#endif +} + +} // namespace mozilla diff --git a/security/sandbox/linux/SandboxFilterUtil.h b/security/sandbox/linux/SandboxFilterUtil.h new file mode 100644 index 0000000000..6e9180bb95 --- /dev/null +++ b/security/sandbox/linux/SandboxFilterUtil.h @@ -0,0 +1,248 @@ +/* -*- 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_SandboxFilterUtil_h +#define mozilla_SandboxFilterUtil_h + +// This header file exists to hold helper code for SandboxFilter.cpp, +// to make that file easier to read for anyone trying to understand +// the filter policy. It's mostly about smoothing out differences +// between different Linux architectures. + +#include "mozilla/Maybe.h" +#include "sandbox/linux/bpf_dsl/policy.h" +#include "sandbox/linux/system_headers/linux_syscalls.h" + +namespace mozilla { + +// This class handles syscalls for BSD socket and SysV IPC operations. +// On 32-bit x86 they're multiplexed via socketcall(2) and ipc(2), +// respectively; on most other architectures they're individual system +// calls. It translates the syscalls into socketcall/ipc selector +// values, because those are defined (even if not used) for all +// architectures. (As of kernel 4.2.0, x86 also has regular system +// calls, but userland will typically still use socketcall.) +// +// This EvaluateSyscall() routine always returns InvalidSyscall() for +// everything else. It's assumed that subclasses will be implementing +// a whitelist policy, so they can handle what they're whitelisting +// and then defer to this class in the default case. +class SandboxPolicyBase : public sandbox::bpf_dsl::Policy { + public: + using ResultExpr = sandbox::bpf_dsl::ResultExpr; + + virtual ResultExpr EvaluateSyscall(int aSysno) const override; + + // aHasArgs is true if this is a normal syscall, where the arguments + // can be inspected by seccomp-bpf, rather than a case of socketcall(). + virtual Maybe EvaluateSocketCall(int aCall, bool aHasArgs) const { + return Nothing(); + } + + // Android doesn't use SysV IPC (and doesn't define the selector + // constants in its headers), so this isn't implemented there. +#ifndef ANDROID + // aArgShift is the offset to add the argument index when + // constructing `Arg` objects: it's 0 for separate syscalls and 1 + // for ipc(). + virtual Maybe EvaluateIpcCall(int aCall, int aArgShift) const { + return Nothing(); + } +#endif + + // Returns true if the running kernel supports separate syscalls for + // socket operations, or false if it supports only socketcall(2). + static bool HasSeparateSocketCalls(); +}; + +} // namespace mozilla + +// "Machine independent" pseudo-syscall numbers, to deal with arch +// dependencies. (Most 32-bit archs started with 32-bit off_t; older +// archs started with 16-bit uid_t/gid_t; 32-bit registers can't hold +// a 64-bit offset for mmap; and so on.) +// +// For some of these, the "old" syscalls are also in use in some +// cases; see, e.g., the handling of RT vs. non-RT signal syscalls. + +#ifdef __NR_mmap2 +# define CASES_FOR_mmap case __NR_mmap2 +#else +# define CASES_FOR_mmap case __NR_mmap +#endif + +#ifdef __NR_fchown32 +# define CASES_FOR_fchown \ + case __NR_fchown32: \ + case __NR_fchown +#else +# define CASES_FOR_fchown case __NR_fchown +#endif + +#ifdef __NR_getuid32 +# define CASES_FOR_getuid case __NR_getuid32 +# define CASES_FOR_getgid case __NR_getgid32 +# define CASES_FOR_geteuid case __NR_geteuid32 +# define CASES_FOR_getegid case __NR_getegid32 +# define CASES_FOR_getresuid \ + case __NR_getresuid32: \ + case __NR_getresuid +# define CASES_FOR_getresgid \ + case __NR_getresgid32: \ + case __NR_getresgid +// The set*id syscalls are omitted; we'll probably never need to allow them. +#else +# define CASES_FOR_getuid case __NR_getuid +# define CASES_FOR_getgid case __NR_getgid +# define CASES_FOR_geteuid case __NR_geteuid +# define CASES_FOR_getegid case __NR_getegid +# define CASES_FOR_getresuid case __NR_getresuid +# define CASES_FOR_getresgid case __NR_getresgid +#endif + +#ifdef __NR_stat64 +# define CASES_FOR_stat case __NR_stat64 +# define CASES_FOR_lstat case __NR_lstat64 +# define CASES_FOR_fstat case __NR_fstat64 +# define CASES_FOR_fstatat case __NR_fstatat64 +# define CASES_FOR_statfs \ + case __NR_statfs64: \ + case __NR_statfs +# define CASES_FOR_fstatfs \ + case __NR_fstatfs64: \ + case __NR_fstatfs +# define CASES_FOR_fcntl case __NR_fcntl64 +// FIXME: we might not need the compat cases for these on non-Android: +# define CASES_FOR_lseek \ + case __NR_lseek: \ + case __NR__llseek +# define CASES_FOR_ftruncate \ + case __NR_ftruncate: \ + case __NR_ftruncate64 +#else +# define CASES_FOR_stat case __NR_stat +# define CASES_FOR_lstat case __NR_lstat +# define CASES_FOR_fstatat case __NR_newfstatat +# define CASES_FOR_fstat case __NR_fstat +# define CASES_FOR_fstatfs case __NR_fstatfs +# define CASES_FOR_statfs case __NR_statfs +# define CASES_FOR_fcntl case __NR_fcntl +# define CASES_FOR_lseek case __NR_lseek +# define CASES_FOR_ftruncate case __NR_ftruncate +#endif + +// getdents is not like the other FS-related syscalls with a "64" variant +#ifdef __NR_getdents +# define CASES_FOR_getdents \ + case __NR_getdents64: \ + case __NR_getdents +#else +# define CASES_FOR_getdents case __NR_getdents64 +#endif + +#ifdef __NR_sigprocmask +# define CASES_FOR_sigprocmask \ + case __NR_sigprocmask: \ + case __NR_rt_sigprocmask +# define CASES_FOR_sigaction \ + case __NR_sigaction: \ + case __NR_rt_sigaction +# define CASES_FOR_sigreturn \ + case __NR_sigreturn: \ + case __NR_rt_sigreturn +#else +# define CASES_FOR_sigprocmask case __NR_rt_sigprocmask +# define CASES_FOR_sigaction case __NR_rt_sigaction +# define CASES_FOR_sigreturn case __NR_rt_sigreturn +#endif + +#ifdef __NR_clock_gettime64 +# define CASES_FOR_clock_gettime \ + case __NR_clock_gettime: \ + case __NR_clock_gettime64 +# define CASES_FOR_clock_getres \ + case __NR_clock_getres: \ + case __NR_clock_getres_time64 +# define CASES_FOR_clock_nanosleep \ + case __NR_clock_nanosleep: \ + case __NR_clock_nanosleep_time64 +# define CASES_FOR_pselect6 \ + case __NR_pselect6: \ + case __NR_pselect6_time64 +# define CASES_FOR_ppoll \ + case __NR_ppoll: \ + case __NR_ppoll_time64 +# define CASES_FOR_futex \ + case __NR_futex: \ + case __NR_futex_time64 +#else +# define CASES_FOR_clock_gettime case __NR_clock_gettime +# define CASES_FOR_clock_getres case __NR_clock_getres +# define CASES_FOR_clock_nanosleep case __NR_clock_nanosleep +# define CASES_FOR_pselect6 case __NR_pselect6 +# define CASES_FOR_ppoll case __NR_ppoll +# define CASES_FOR_futex case __NR_futex +#endif + +#if defined(__NR__newselect) +# define CASES_FOR_select \ + case __NR__newselect: \ + CASES_FOR_pselect6 +#elif defined(__NR_select) +# define CASES_FOR_select \ + case __NR_select: \ + CASES_FOR_pselect6 +#else +# define CASES_FOR_select CASES_FOR_pselect6 +#endif + +#ifdef __NR_poll +# define CASES_FOR_poll \ + case __NR_poll: \ + CASES_FOR_ppoll +#else +# define CASES_FOR_poll CASES_FOR_ppoll +#endif + +#ifdef __NR_epoll_create +# define CASES_FOR_epoll_create \ + case __NR_epoll_create: \ + case __NR_epoll_create1 +#else +# define CASES_FOR_epoll_create case __NR_epoll_create1 +#endif + +#ifdef __NR_epoll_wait +# define CASES_FOR_epoll_wait \ + case __NR_epoll_wait: \ + case __NR_epoll_pwait +#else +# define CASES_FOR_epoll_wait case __NR_epoll_pwait +#endif + +#ifdef __NR_pipe +# define CASES_FOR_pipe \ + case __NR_pipe: \ + case __NR_pipe2 +#else +# define CASES_FOR_pipe case __NR_pipe2 +#endif + +#ifdef __NR_dup2 +# define CASES_FOR_dup2 \ + case __NR_dup2: \ + case __NR_dup3 +#else +# define CASES_FOR_dup2 case __NR_dup3 +#endif + +#ifdef __NR_ugetrlimit +# define CASES_FOR_getrlimit case __NR_ugetrlimit +#else +# define CASES_FOR_getrlimit case __NR_getrlimit +#endif + +#endif // mozilla_SandboxFilterUtil_h diff --git a/security/sandbox/linux/SandboxHooks.cpp b/security/sandbox/linux/SandboxHooks.cpp new file mode 100644 index 0000000000..eb4da9b87b --- /dev/null +++ b/security/sandbox/linux/SandboxHooks.cpp @@ -0,0 +1,110 @@ +/* -*- 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 "mozilla/Assertions.h" +#include "mozilla/Atomics.h" +#include "mozilla/Types.h" + +#include +#include +#include +#include +#include +#include +#ifdef MOZ_X11 +# include +#endif + +// Signal number used to enable seccomp on each thread. +extern mozilla::Atomic gSeccompTsyncBroadcastSignum; + +static bool SigSetNeedsFixup(const sigset_t* aSet) { + int tsyncSignum = gSeccompTsyncBroadcastSignum; + + return aSet != nullptr && + (sigismember(aSet, SIGSYS) || + (tsyncSignum != 0 && sigismember(aSet, tsyncSignum))); +} + +static void SigSetFixup(sigset_t* aSet) { + int tsyncSignum = gSeccompTsyncBroadcastSignum; + int rv = sigdelset(aSet, SIGSYS); + MOZ_RELEASE_ASSERT(rv == 0); + if (tsyncSignum != 0) { + rv = sigdelset(aSet, tsyncSignum); + MOZ_RELEASE_ASSERT(rv == 0); + } +} + +// This file defines a hook for sigprocmask() and pthread_sigmask(). +// Bug 1176099: some threads block SIGSYS signal which breaks our seccomp-bpf +// sandbox. To avoid this, we intercept the call and remove SIGSYS. +// +// ENOSYS indicates an error within the hook function itself. +static int HandleSigset(int (*aRealFunc)(int, const sigset_t*, sigset_t*), + int aHow, const sigset_t* aSet, sigset_t* aOldSet, + bool aUseErrno) { + if (!aRealFunc) { + if (aUseErrno) { + errno = ENOSYS; + return -1; + } + + return ENOSYS; + } + + // Avoid unnecessary work + if (aHow == SIG_UNBLOCK || !SigSetNeedsFixup(aSet)) { + return aRealFunc(aHow, aSet, aOldSet); + } + + sigset_t newSet = *aSet; + SigSetFixup(&newSet); + return aRealFunc(aHow, &newSet, aOldSet); +} + +extern "C" MOZ_EXPORT int sigprocmask(int how, const sigset_t* set, + sigset_t* oldset) { + static auto sRealFunc = + (int (*)(int, const sigset_t*, sigset_t*))dlsym(RTLD_NEXT, "sigprocmask"); + + return HandleSigset(sRealFunc, how, set, oldset, true); +} + +extern "C" MOZ_EXPORT int pthread_sigmask(int how, const sigset_t* set, + sigset_t* oldset) { + static auto sRealFunc = (int (*)(int, const sigset_t*, sigset_t*))dlsym( + RTLD_NEXT, "pthread_sigmask"); + + return HandleSigset(sRealFunc, how, set, oldset, false); +} + +extern "C" MOZ_EXPORT int sigaction(int signum, const struct sigaction* act, + struct sigaction* oldact) { + static auto sRealFunc = + (int (*)(int, const struct sigaction*, struct sigaction*))dlsym( + RTLD_NEXT, "sigaction"); + + if (!sRealFunc) { + errno = ENOSYS; + return -1; + } + + if (act == nullptr || !SigSetNeedsFixup(&act->sa_mask)) { + return sRealFunc(signum, act, oldact); + } + + struct sigaction newact = *act; + SigSetFixup(&newact.sa_mask); + return sRealFunc(signum, &newact, oldact); +} + +extern "C" MOZ_EXPORT int inotify_init(void) { return inotify_init1(0); } + +extern "C" MOZ_EXPORT int inotify_init1(int flags) { + errno = ENOSYS; + return -1; +} diff --git a/security/sandbox/linux/SandboxInfo.cpp b/security/sandbox/linux/SandboxInfo.cpp new file mode 100644 index 0000000000..3d71e55921 --- /dev/null +++ b/security/sandbox/linux/SandboxInfo.cpp @@ -0,0 +1,201 @@ +/* -*- 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 "SandboxInfo.h" +#include "SandboxLogging.h" +#include "LinuxSched.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "base/posix/eintr_wrapper.h" +#include "mozilla/Assertions.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/SandboxSettings.h" +#include "sandbox/linux/system_headers/linux_seccomp.h" +#include "sandbox/linux/system_headers/linux_syscalls.h" + +#ifdef MOZ_VALGRIND +# include +#endif + +// A note about assertions: in general, the worst thing this module +// should be able to do is disable sandboxing features, so release +// asserts or MOZ_CRASH should be avoided, even for seeming +// impossibilities like an unimplemented syscall returning success +// (which has happened: https://crbug.com/439795 ). +// +// MOZ_DIAGNOSTIC_ASSERT (debug builds, plus Nightly/Aurora non-debug) +// is probably the best choice for conditions that shouldn't be able +// to fail without the help of bugs in the kernel or system libraries. +// +// Regardless of assertion type, whatever condition caused it to fail +// should generally also disable the corresponding feature on builds +// that omit the assertion. + +namespace mozilla { + +static bool HasSeccompBPF() { + // Allow simulating the absence of seccomp-bpf support, for testing. + if (getenv("MOZ_FAKE_NO_SANDBOX")) { + return false; + } + + // Valgrind and the sandbox don't interact well, probably because Valgrind + // does various system calls which aren't allowed, even if Firefox itself + // is playing by the rules. +#if defined(MOZ_VALGRIND) + if (RUNNING_ON_VALGRIND) { + return false; + } +#endif + + // Determine whether seccomp-bpf is supported by trying to + // enable it with an invalid pointer for the filter. This will + // fail with EFAULT if supported and EINVAL if not, without + // changing the process's state. + + int rv = prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, nullptr); + MOZ_DIAGNOSTIC_ASSERT(rv == -1, + "prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER," + " nullptr) didn't fail"); + MOZ_DIAGNOSTIC_ASSERT(errno == EFAULT || errno == EINVAL); + return rv == -1 && errno == EFAULT; +} + +static bool HasSeccompTSync() { + // Similar to above, but for thread-sync mode. See also Chromium's + // sandbox::SandboxBPF::SupportsSeccompThreadFilterSynchronization + if (getenv("MOZ_FAKE_NO_SECCOMP_TSYNC")) { + return false; + } + int rv = syscall(__NR_seccomp, SECCOMP_SET_MODE_FILTER, + SECCOMP_FILTER_FLAG_TSYNC, nullptr); + MOZ_DIAGNOSTIC_ASSERT(rv == -1, + "seccomp(..., SECCOMP_FILTER_FLAG_TSYNC," + " nullptr) didn't fail"); + MOZ_DIAGNOSTIC_ASSERT(errno == EFAULT || errno == EINVAL || errno == ENOSYS); + return rv == -1 && errno == EFAULT; +} + +static bool HasUserNamespaceSupport() { + // Note: the /proc//ns/* files track setns(2) support, which in + // some cases (e.g., pid) significantly postdates kernel support for + // the namespace type, so in general this type of check could be a + // false negative. However, for user namespaces, any kernel new + // enough for the feature to be usable for us has setns support + // (v3.8), so this is okay. + // + // The non-user namespaces all default to "y" in init/Kconfig, but + // check them explicitly in case someone has a weird custom config. + static const char* const paths[] = { + "/proc/self/ns/user", + "/proc/self/ns/pid", + "/proc/self/ns/net", + "/proc/self/ns/ipc", + }; + for (size_t i = 0; i < ArrayLength(paths); ++i) { + if (access(paths[i], F_OK) == -1) { + MOZ_ASSERT(errno == ENOENT); + return false; + } + } + return true; +} + +static bool CanCreateUserNamespace() { + // Unfortunately, the only way to verify that this process can + // create a new user namespace is to actually create one; because + // this process's namespaces shouldn't be side-effected (yet), it's + // necessary to clone (and collect) a child process. See also + // Chromium's sandbox::Credentials::SupportsNewUserNS. + // + // This is somewhat more expensive than the other tests, so it's + // cached in the environment to prevent child processes from having + // to re-run the test. + // + // This is run at static initializer time, while single-threaded, so + // locking isn't needed to access the environment. + static const char kCacheEnvName[] = "MOZ_ASSUME_USER_NS"; + const char* cached = getenv(kCacheEnvName); + if (cached) { + return cached[0] > '0'; + } + + // Bug 1434528: In addition to CLONE_NEWUSER, do something that uses + // the new capabilities (in this case, cloning another namespace) to + // detect AppArmor policies that allow CLONE_NEWUSER but don't allow + // doing anything useful with it. + pid_t pid = syscall(__NR_clone, SIGCHLD | CLONE_NEWUSER | CLONE_NEWPID, + nullptr, nullptr, nullptr, nullptr); + if (pid == 0) { + // In the child. Do as little as possible. + _exit(0); + } + if (pid == -1) { + // Failure. + MOZ_ASSERT(errno == EINVAL || // unsupported + errno == EPERM || // root-only, or we're already chrooted + errno == EUSERS); // already at user namespace nesting limit + setenv(kCacheEnvName, "0", 1); + return false; + } + // Otherwise, in the parent and successful. + bool waitpid_ok = HANDLE_EINTR(waitpid(pid, nullptr, 0)) == pid; + MOZ_ASSERT(waitpid_ok); + if (!waitpid_ok) { + return false; + } + setenv(kCacheEnvName, "1", 1); + return true; +} + +/* static */ +const SandboxInfo SandboxInfo::sSingleton = SandboxInfo(); + +SandboxInfo::SandboxInfo() { + int flags = 0; + static_assert(sizeof(flags) >= sizeof(Flags), "enum Flags fits in an int"); + + if (HasSeccompBPF()) { + flags |= kHasSeccompBPF; + if (HasSeccompTSync()) { + flags |= kHasSeccompTSync; + } + } + + if (HasUserNamespaceSupport()) { + flags |= kHasPrivilegedUserNamespaces; + if (CanCreateUserNamespace()) { + flags |= kHasUserNamespaces; + } + } + + // We can't use mozilla::IsContentSandboxEnabled() here because a) + // libmozsandbox can't depend on libxul, and b) this is called in a static + // initializer before the prefences service is ready. + if (!getenv("MOZ_DISABLE_CONTENT_SANDBOX")) { + flags |= kEnabledForContent; + } + if (getenv("MOZ_PERMISSIVE_CONTENT_SANDBOX")) { + flags |= kPermissive; + } + if (!getenv("MOZ_DISABLE_GMP_SANDBOX")) { + flags |= kEnabledForMedia; + } + if (getenv("MOZ_SANDBOX_LOGGING")) { + flags |= kVerbose; + } + + mFlags = static_cast(flags); +} + +} // namespace mozilla diff --git a/security/sandbox/linux/SandboxInfo.h b/security/sandbox/linux/SandboxInfo.h new file mode 100644 index 0000000000..fb20d206a3 --- /dev/null +++ b/security/sandbox/linux/SandboxInfo.h @@ -0,0 +1,70 @@ +/* -*- 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_SandboxInfo_h +#define mozilla_SandboxInfo_h + +#include "mozilla/Types.h" + +// Information on what parts of sandboxing are enabled in this build +// and/or supported by the system. + +namespace mozilla { + +class SandboxInfo { + public: + // No need to prevent copying; this is essentially just a const int. + SandboxInfo(const SandboxInfo& aOther) = default; + + // Flags are checked at initializer time; this returns them. + static const SandboxInfo& Get() { return sSingleton; } + + enum Flags { + // System call filtering; kernel config option CONFIG_SECCOMP_FILTER. + kHasSeccompBPF = 1 << 0, + // Whether to use a sandbox for content processes; env var + // MOZ_DISABLE_CONTENT_SANDBOX + kEnabledForContent = 1 << 1, + // Whether to use a sandbox for GMP processes; env var + // MOZ_DISABLE_GMP_SANDBOX. + kEnabledForMedia = 1 << 2, + // Env var MOZ_SANDBOX_LOGGING. + kVerbose = 1 << 3, + // Kernel can atomically set system call filtering on entire thread group. + kHasSeccompTSync = 1 << 4, + // Can this process create user namespaces? (Man page user_namespaces(7).) + kHasUserNamespaces = 1 << 5, + // Could a more privileged process have user namespaces, even if we can't? + kHasPrivilegedUserNamespaces = 1 << 6, + // Env var MOZ_PERMISSIVE_CONTENT_SANDBOX + kPermissive = 1 << 7, + // (1 << 8) was kUnexpectedThreads + }; + + bool Test(Flags aFlag) const { return (mFlags & aFlag) == aFlag; } + + // Returns true if SetContentProcessSandbox may be called. + bool CanSandboxContent() const { + return !Test(kEnabledForContent) || Test(kHasSeccompBPF); + } + + // Returns true if SetMediaPluginSandbox may be called. + bool CanSandboxMedia() const { + return !Test(kEnabledForMedia) || Test(kHasSeccompBPF); + } + + // For telemetry / crash annotation uses. + uint32_t AsInteger() const { return mFlags; } + + private: + enum Flags mFlags; + static const MOZ_EXPORT SandboxInfo sSingleton; + SandboxInfo(); +}; + +} // namespace mozilla + +#endif // mozilla_SandboxInfo_h diff --git a/security/sandbox/linux/SandboxInternal.h b/security/sandbox/linux/SandboxInternal.h new file mode 100644 index 0000000000..27934f9a8e --- /dev/null +++ b/security/sandbox/linux/SandboxInternal.h @@ -0,0 +1,28 @@ +/* -*- 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_SandboxInternal_h +#define mozilla_SandboxInternal_h + +#include + +#include "mozilla/Types.h" + +struct sock_fprog; + +namespace mozilla { + +// SandboxCrash() has to be in libxul to use internal interfaces, but +// its caller in libmozsandbox. +// See also bug 1101170. + +typedef void (*SandboxCrashFunc)(int, siginfo_t*, void*, const void*); +extern MOZ_EXPORT SandboxCrashFunc gSandboxCrashFunc; +extern const sock_fprog* gSetSandboxFilter; + +} // namespace mozilla + +#endif // mozilla_SandboxInternal_h diff --git a/security/sandbox/linux/SandboxLogging.cpp b/security/sandbox/linux/SandboxLogging.cpp new file mode 100644 index 0000000000..dc9ac0c062 --- /dev/null +++ b/security/sandbox/linux/SandboxLogging.cpp @@ -0,0 +1,148 @@ +/* -*- 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 "SandboxLogging.h" + +#ifdef ANDROID +# include +#endif +#include +#include +#include +#include +#include + +#include "base/posix/eintr_wrapper.h" + +#ifdef __GLIBC_PREREQ +# if __GLIBC_PREREQ(2, 32) +# define HAVE_STRERRORNAME_NP 1 +# endif +#endif + +namespace mozilla { + +// Alters an iovec array to remove the first `toDrop` bytes. This +// complexity is necessary because writev can return a short write +// (e.g., if stderr is a pipe and the buffer is almost full). +static void IOVecDrop(struct iovec* iov, int iovcnt, size_t toDrop) { + while (toDrop > 0 && iovcnt > 0) { + size_t toDropHere = std::min(toDrop, iov->iov_len); + iov->iov_base = static_cast(iov->iov_base) + toDropHere; + iov->iov_len -= toDropHere; + toDrop -= toDropHere; + ++iov; + --iovcnt; + } +} + +void SandboxLogError(const char* message) { +#ifdef ANDROID + // This uses writev internally and appears to be async signal safe. + __android_log_write(ANDROID_LOG_ERROR, "Sandbox", message); +#endif + static char logPrefixProcess[16]; + static const ssize_t printfSize = + ::base::strings::SafeSPrintf(logPrefixProcess, "[%d] ", getpid()); + static const size_t pidSize = + std::clamp(static_cast(printfSize), static_cast(0), + static_cast(sizeof(logPrefixProcess) - 1)); + static const char logPrefix[] = "Sandbox: ", logSuffix[] = "\n"; + struct iovec iovs[4] = { + {const_cast(logPrefixProcess), pidSize}, + {const_cast(logPrefix), sizeof(logPrefix) - 1}, + {const_cast(message), strlen(message)}, + {const_cast(logSuffix), sizeof(logSuffix) - 1}, + }; + while (iovs[2].iov_len > 0) { + ssize_t written = HANDLE_EINTR(writev(STDERR_FILENO, iovs, 4)); + if (written <= 0) { + break; + } + IOVecDrop(iovs, 4, static_cast(written)); + } +} + +ssize_t GetLibcErrorName(char* aBuf, size_t aSize, int aErr) { + const char* errStr; + +#ifdef HAVE_STRERRORNAME_NP + // This is the function we'd like to have, but it's a glibc + // extension and present only in newer versions. + errStr = strerrorname_np(aErr); +#else + // Otherwise, handle most of the basic / common errors with a big + // switch statement. + switch (aErr) { + +# define HANDLE_ERR(x) \ + case x: \ + errStr = #x; \ + break + + // errno-base.h + HANDLE_ERR(EPERM); + HANDLE_ERR(ENOENT); + HANDLE_ERR(ESRCH); + HANDLE_ERR(EINTR); + HANDLE_ERR(EIO); + HANDLE_ERR(ENXIO); + HANDLE_ERR(E2BIG); + HANDLE_ERR(ENOEXEC); + HANDLE_ERR(EBADF); + HANDLE_ERR(ECHILD); + HANDLE_ERR(EAGAIN); + HANDLE_ERR(ENOMEM); + HANDLE_ERR(EACCES); + HANDLE_ERR(EFAULT); + HANDLE_ERR(ENOTBLK); + HANDLE_ERR(EBUSY); + HANDLE_ERR(EEXIST); + HANDLE_ERR(EXDEV); + HANDLE_ERR(ENODEV); + HANDLE_ERR(ENOTDIR); + HANDLE_ERR(EISDIR); + HANDLE_ERR(EINVAL); + HANDLE_ERR(ENFILE); + HANDLE_ERR(EMFILE); + HANDLE_ERR(ENOTTY); + HANDLE_ERR(ETXTBSY); + HANDLE_ERR(EFBIG); + HANDLE_ERR(ENOSPC); + HANDLE_ERR(ESPIPE); + HANDLE_ERR(EROFS); + HANDLE_ERR(EMLINK); + HANDLE_ERR(EPIPE); + HANDLE_ERR(EDOM); + HANDLE_ERR(ERANGE); + + // selected other errors + HANDLE_ERR(ENAMETOOLONG); + HANDLE_ERR(ENOSYS); + HANDLE_ERR(ENOTEMPTY); + HANDLE_ERR(ELOOP); + HANDLE_ERR(ENOTSOCK); + HANDLE_ERR(EMSGSIZE); + HANDLE_ERR(ECONNRESET); + HANDLE_ERR(ECONNREFUSED); + HANDLE_ERR(EHOSTUNREACH); + HANDLE_ERR(ESTALE); + +# undef HANDLE_ERR + + default: + errStr = nullptr; + } +#endif // no strerrorname_np + + if (errStr) { + return base::strings::SafeSNPrintf(aBuf, aSize, "%s", errStr); + } + + return base::strings::SafeSNPrintf(aBuf, aSize, "error %d", aErr); +} + +} // namespace mozilla diff --git a/security/sandbox/linux/SandboxLogging.h b/security/sandbox/linux/SandboxLogging.h new file mode 100644 index 0000000000..94467ececc --- /dev/null +++ b/security/sandbox/linux/SandboxLogging.h @@ -0,0 +1,81 @@ +/* -*- 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_SandboxLogging_h +#define mozilla_SandboxLogging_h + +// This header defines the SANDBOX_LOG macro used in the Linux +// sandboxing code. It uses Android logging on Android and writes to +// stderr otherwise. Android logging has severity levels; currently +// only "error" severity is exposed here, and this isn't marked when +// writing to stderr. +// +// The format strings are processed by Chromium SafeSPrintf, which +// doesn't accept size modifiers or %u because it uses C++11 variadic +// templates to obtain the actual argument types; all decimal integer +// formatting uses %d. See safe_sprintf.h for more details. + +// Build SafeSPrintf without assertions to avoid a dependency on +// Chromium logging. This doesn't affect safety; it just means that +// type mismatches (pointer vs. integer) always result in unexpanded +// %-directives instead of crashing. See also the moz.build files, +// which apply NDEBUG to the .cc file. +#ifndef NDEBUG +# define NDEBUG 1 +# include "base/strings/safe_sprintf.h" +# undef NDEBUG +#else +# include "base/strings/safe_sprintf.h" +#endif + +#include + +namespace mozilla { +// Logs the formatted string (marked with "error" severity, if supported). +void SandboxLogError(const char* aMessage); + +// Writes into aBuf the identifier for an error number (e.g., "EINVAL" +// rather than "Invalid argument"); may fall back to "error N" (with +// the number) for unhandled errors. +// +// Bounds are handled like snprintf: the return value is the length +// the string would have (not counting the null terminator) ignoring +// buffer size, and the string written into the buffer may be +// truncated to fit but is always null terminated. +ssize_t GetLibcErrorName(char* aBuf, size_t aSize, int aErr); +} // namespace mozilla + +#define SANDBOX_LOG_LEN 256 + +// Formats a log message and logs it (with "error" severity, if supported). +// +// Note that SafeSPrintf doesn't accept size modifiers or %u; all +// decimal integers are %d, because it uses C++11 variadic templates +// to use the actual argument type. +#define SANDBOX_LOG(fmt, args...) \ + do { \ + char _sandboxLogBuf[SANDBOX_LOG_LEN]; \ + ::base::strings::SafeSPrintf(_sandboxLogBuf, fmt, ##args); \ + ::mozilla::SandboxLogError(_sandboxLogBuf); \ + } while (0) + +#define SANDBOX_LOG_WITH_ERROR(errnum, fmt, args...) \ + do { \ + char _sandboxLogBuf[SANDBOX_LOG_LEN]; \ + ssize_t _sandboxLogOff = \ + ::base::strings::SafeSPrintf(_sandboxLogBuf, fmt ": ", ##args); \ + if (static_cast(_sandboxLogOff) < sizeof(_sandboxLogBuf)) { \ + ::mozilla::GetLibcErrorName(_sandboxLogBuf + _sandboxLogOff, \ + sizeof(_sandboxLogBuf) - _sandboxLogOff, \ + errnum); \ + } \ + ::mozilla::SandboxLogError(_sandboxLogBuf); \ + } while (0) + +#define SANDBOX_LOG_ERRNO(fmt, args...) \ + SANDBOX_LOG_WITH_ERROR(errno, fmt, ##args) + +#endif // mozilla_SandboxLogging_h diff --git a/security/sandbox/linux/SandboxOpenedFiles.cpp b/security/sandbox/linux/SandboxOpenedFiles.cpp new file mode 100644 index 0000000000..8c26f7f206 --- /dev/null +++ b/security/sandbox/linux/SandboxOpenedFiles.cpp @@ -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/. */ + +#include "SandboxOpenedFiles.h" + +#include +#include +#include + +#include + +#include "SandboxLogging.h" + +namespace mozilla { + +// The default move constructor almost works, but Atomic isn't +// move-constructable and the fd needs some special handling. +SandboxOpenedFile::SandboxOpenedFile(SandboxOpenedFile&& aMoved) + : mPath(std::move(aMoved.mPath)), + mMaybeFd(aMoved.TakeDesc()), + mDup(aMoved.mDup), + mExpectError(aMoved.mExpectError) {} + +SandboxOpenedFile::SandboxOpenedFile(const char* aPath, Dup aDup) + : mPath(aPath), mDup(aDup == Dup::YES), mExpectError(false) { + MOZ_ASSERT(aPath[0] == '/', "path should be absolute"); + + int fd = open(aPath, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + mExpectError = true; + } + mMaybeFd = fd; +} + +SandboxOpenedFile::SandboxOpenedFile(const char* aPath, Error) + : mPath(aPath), mMaybeFd(-1), mDup(false), mExpectError(true) {} + +int SandboxOpenedFile::GetDesc() const { + int fd; + if (mDup) { + fd = mMaybeFd; + if (fd >= 0) { + fd = dup(fd); + if (fd < 0) { + SANDBOX_LOG_ERRNO("dup"); + } + } + } else { + fd = TakeDesc(); + } + if (fd < 0 && !mExpectError) { + SANDBOX_LOG("unexpected multiple open of file %s", Path()); + } + return fd; +} + +SandboxOpenedFile::~SandboxOpenedFile() { + int fd = TakeDesc(); + if (fd >= 0) { + close(fd); + } +} + +int SandboxOpenedFiles::GetDesc(const char* aPath) const { + for (const auto& file : mFiles) { + if (strcmp(file.Path(), aPath) == 0) { + return file.GetDesc(); + } + } + SANDBOX_LOG("attempt to open unexpected file %s", aPath); + return -1; +} + +} // namespace mozilla diff --git a/security/sandbox/linux/SandboxOpenedFiles.h b/security/sandbox/linux/SandboxOpenedFiles.h new file mode 100644 index 0000000000..24881b93f0 --- /dev/null +++ b/security/sandbox/linux/SandboxOpenedFiles.h @@ -0,0 +1,97 @@ +/* -*- 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_SandboxOpenedFiles_h +#define mozilla_SandboxOpenedFiles_h + +#include "mozilla/Atomics.h" +#include "mozilla/Range.h" +#include "mozilla/UniquePtr.h" + +#include +#include + +// The use of C++ standard library containers here should be safe; the +// standard (section container.requirements.dataraces) requires that +// using const methods/pointers not introduce data races (e.g., from +// interior mutability or global state). +// +// Reentrancy isn't guaranteed, and the library could use async signal +// unsafe mutexes for "read-only" operations, but I'm assuming that that's +// not the case at least for simple containers like string and vector. + +namespace mozilla { + +// This class represents a file that's been pre-opened for a media +// plugin. It can be move-constructed but not copied. +class SandboxOpenedFile final { + public: + enum class Dup { NO, YES }; + struct Error {}; + + // This constructor opens the named file and saves the descriptor. + // If the open fails, IsOpen() will return false and GetDesc() will + // quietly return -1. If aDup is Dup::YES, GetDesc() will return a + // dup() of the descriptor every time it's called; otherwise, the + // first call will return the descriptor and any further calls will + // log an error message and return -1. + explicit SandboxOpenedFile(const char* aPath, Dup aDup = Dup::NO); + + // This constructor is for files which the process will try to open + // but we don't want to grant access: using it will always fail + // (GetDesc will return -1) without logging. + SandboxOpenedFile(const char* aPath, Error); + + // Simulates opening the pre-opened file; see the constructor's + // comment for details. Does not set errno on error, but may modify + // it as a side-effect. Thread-safe and intended to be async signal safe. + int GetDesc() const; + + const char* Path() const { return mPath.c_str(); } + + bool IsOpen() const { return mMaybeFd >= 0; } + + ~SandboxOpenedFile(); + + MOZ_IMPLICIT SandboxOpenedFile(SandboxOpenedFile&& aMoved); + + private: + std::string mPath; + mutable Atomic mMaybeFd; + bool mDup; + bool mExpectError; + + int TakeDesc() const { return mMaybeFd.exchange(-1); } +}; + +// This class represents a collection of files to be used to handle +// open() calls from the media plugin (and the dynamic loader). +// Because the seccomp-bpf policy exists until the process exits, this +// object must not be destroyed after the syscall filter is installed. +class SandboxOpenedFiles { + public: + SandboxOpenedFiles() = default; + + template + void Add(Args&&... aArgs) { + mFiles.emplace_back(std::forward(aArgs)...); + } + + int GetDesc(const char* aPath) const; + + private: + std::vector mFiles; + + // We could allow destroying instances of this class that aren't + // used with seccomp-bpf (e.g., for unit testing) by having the + // destructor check a flag set by the syscall policy and crash, + // but let's not write that code until we actually need it. + ~SandboxOpenedFiles() = delete; +}; + +} // namespace mozilla + +#endif // mozilla_SandboxOpenedFiles_h diff --git a/security/sandbox/linux/SandboxReporterClient.cpp b/security/sandbox/linux/SandboxReporterClient.cpp new file mode 100644 index 0000000000..6359c2cb9a --- /dev/null +++ b/security/sandbox/linux/SandboxReporterClient.cpp @@ -0,0 +1,88 @@ +/* -*- 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 "SandboxReporterClient.h" +#include "SandboxLogging.h" + +#include +#include +#include +#include +#include +#include + +#include "mozilla/Assertions.h" +#include "mozilla/PodOperations.h" +#include "prenv.h" +#include "sandbox/linux/bpf_dsl/seccomp_macros.h" +#ifdef ANDROID +# include "sandbox/linux/system_headers/linux_ucontext.h" +#else +# include +#endif + +namespace mozilla { + +SandboxReporterClient::SandboxReporterClient(SandboxReport::ProcType aProcType, + int aFd) + : mProcType(aProcType), mFd(aFd) { + // Unfortunately, there isn't a good way to check that the fd is a + // socket connected to the right thing without attempting some kind + // of in-band handshake. However, the crash reporter (which also + // uses a "magic number" fd) doesn't do any kind of checking either, + // so it's probably okay to skip it here. +} + +SandboxReporterClient::SandboxReporterClient(SandboxReport::ProcType aProcType) + : SandboxReporterClient(aProcType, kSandboxReporterFileDesc) { + MOZ_RELEASE_ASSERT(PR_GetEnv("MOZ_SANDBOXED") != nullptr); +} + +SandboxReport SandboxReporterClient::MakeReport(const void* aContext) { + SandboxReport report; + const auto ctx = static_cast(aContext); + + // Zero the entire struct; some memory safety analyses care about + // sending uninitialized alignment padding to another process. + PodZero(&report); + + clock_gettime(CLOCK_MONOTONIC_COARSE, &report.mTime); + report.mPid = getpid(); + report.mTid = syscall(__NR_gettid); + report.mProcType = mProcType; + report.mSyscall = SECCOMP_SYSCALL(ctx); + report.mArgs[0] = SECCOMP_PARM1(ctx); + report.mArgs[1] = SECCOMP_PARM2(ctx); + report.mArgs[2] = SECCOMP_PARM3(ctx); + report.mArgs[3] = SECCOMP_PARM4(ctx); + report.mArgs[4] = SECCOMP_PARM5(ctx); + report.mArgs[5] = SECCOMP_PARM6(ctx); + // Named Return Value Optimization allows the compiler to optimize + // out the copy here (and the one in MakeReportAndSend). + return report; +} + +void SandboxReporterClient::SendReport(const SandboxReport& aReport) { + // The "common" seccomp-bpf policy allows sendmsg but not send(to), + // so just use sendmsg even though send would suffice for this. + struct iovec iov; + struct msghdr msg; + + iov.iov_base = const_cast(static_cast(&aReport)); + iov.iov_len = sizeof(SandboxReport); + PodZero(&msg); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + const auto sent = sendmsg(mFd, &msg, MSG_NOSIGNAL); + + if (sent != sizeof(SandboxReport)) { + MOZ_DIAGNOSTIC_ASSERT(sent == -1); + SANDBOX_LOG_ERRNO("Failed to report rejected syscall"); + } +} + +} // namespace mozilla diff --git a/security/sandbox/linux/SandboxReporterClient.h b/security/sandbox/linux/SandboxReporterClient.h new file mode 100644 index 0000000000..335472a7f9 --- /dev/null +++ b/security/sandbox/linux/SandboxReporterClient.h @@ -0,0 +1,47 @@ +/* -*- 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_SandboxReporterClient_h +#define mozilla_SandboxReporterClient_h + +#include "reporter/SandboxReporterCommon.h" + +namespace mozilla { + +// This class is instantiated in child processes in Sandbox.cpp to +// send reports from the SIGSYS handler to the SandboxReporter +// instance in the parent. +class SandboxReporterClient { + public: + // Note: this does not take ownership of the file descriptor; if + // it's not kSandboxReporterFileDesc (e.g., for unit testing), the + // caller will need to close it to avoid leaks. + SandboxReporterClient(SandboxReport::ProcType aProcType, int aFd); + + // This constructor uses the default fd (kSandboxReporterFileDesc) + // for a sandboxed child process. + explicit SandboxReporterClient(SandboxReport::ProcType aProcType); + + // Constructs a report from a signal context (the ucontext_t* passed + // as void* to an sa_sigaction handler); uses the caller's pid and tid. + SandboxReport MakeReport(const void* aContext); + + void SendReport(const SandboxReport& aReport); + + SandboxReport MakeReportAndSend(const void* aContext) { + SandboxReport report = MakeReport(aContext); + SendReport(report); + return report; + } + + private: + SandboxReport::ProcType mProcType; + int mFd; +}; + +} // namespace mozilla + +#endif // mozilla_SandboxReporterClient_h diff --git a/security/sandbox/linux/broker/SandboxBroker.cpp b/security/sandbox/linux/broker/SandboxBroker.cpp new file mode 100644 index 0000000000..75ab52979e --- /dev/null +++ b/security/sandbox/linux/broker/SandboxBroker.cpp @@ -0,0 +1,1104 @@ +/* -*- 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 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 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 Create(UniquePtr aPolicy, + int aChildPid, + ipc::FileDescriptor& aClientFdOut); + virtual ~SandboxBroker(); + + private: + PlatformThreadHandle mThread; + int mFileDesc; + const int mChildPid; + const UniquePtr mPolicy; +#if defined(MOZ_CONTENT_TEMP_DIR) + nsCString mTempPath; + nsCString mContentTempPath; +#endif + + typedef nsTHashMap PathMap; + PathMap mSymlinkMap; + + SandboxBroker(UniquePtr 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..baba0b25a5 --- /dev/null +++ b/security/sandbox/linux/broker/SandboxBrokerCommon.cpp @@ -0,0 +1,159 @@ +/* -*- 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 +#include +#include +#include +#include + +#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(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(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 = EPROTO; + 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 attached fd was dropped due to fd + // exhaustion in the receiving process, so map that to `EMFILE`. + // (It could also happen if the other process maliciously sends + // too many fds.) + // + // MSG_TRUNC (truncation of the data part) shouldn't ever happen. + // However, it has happened in the past, due to accidentally + // sending more data than the receiver was expecting. We assert + // that that doesn't happen (and, if it does, try to map it to a + // vaguely sensible error code). + MOZ_DIAGNOSTIC_ASSERT((msg.msg_flags & MSG_TRUNC) == 0); + errno = (msg.msg_flags & MSG_CTRUNC) ? EMFILE : EPROTO; + 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(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(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 + +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..0e5358e68e --- /dev/null +++ b/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp @@ -0,0 +1,1083 @@ +/* -*- 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 +#endif + +#ifdef MOZ_ENABLE_V4L2 +# include +# include +# include +#endif // MOZ_ENABLE_V4L2 + +#include +#include +#include +#include +#ifndef ANDROID +# include +#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; +using FileCacheT = nsTArray; + +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 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 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 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 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 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 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 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 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 extraConfDirsAllow = { + ".themes", + ".fonts", + ".cache/fontconfig", + }; + + // Fallback if XDG_CONFIG_HOME isn't set + if (xdgConfigHome.IsEmpty()) { + extraConfDirsAllow.emplace_back(".config"); + } + + nsCOMPtr homeDir; + nsresult rv = + GetSpecialSystemDirectory(Unix_HomeDirectory, getter_AddRefs(homeDir)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr 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 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 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 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 profileDir; + rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(profileDir)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr 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 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 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; +} + +#ifdef MOZ_ENABLE_V4L2 +static void AddV4l2Dependencies(SandboxBroker::Policy* policy) { + // For V4L2 hardware-accelerated video decode, RDD needs access to certain + // /dev/video* devices but don't want to allow it access to webcams etc. + // So we only allow it access to M2M video devices (encoders and decoders). + DIR* dir = opendir("/dev"); + if (!dir) { + SANDBOX_LOG("Couldn't list /dev"); + return; + } + + struct dirent* dir_entry; + while ((dir_entry = readdir(dir))) { + if (strncmp(dir_entry->d_name, "video", 5)) { + // Not a /dev/video* device, so ignore it + continue; + } + + nsCString path = "/dev/"_ns; + path += nsDependentCString(dir_entry->d_name); + + int fd = open(path.get(), O_RDWR | O_NONBLOCK, 0); + if (fd < 0) { + // Couldn't open this device, so ignore it. + SANDBOX_LOG("Couldn't open video device %s", path.get()); + continue; + } + + // Query device capabilities + struct v4l2_capability cap; + int result = ioctl(fd, VIDIOC_QUERYCAP, &cap); + if (result < 0) { + // Couldn't query capabilities of this device, so ignore it + SANDBOX_LOG("Couldn't query capabilities of video device %s", path.get()); + close(fd); + continue; + } + + if ((cap.device_caps & V4L2_CAP_VIDEO_M2M) || + (cap.device_caps & V4L2_CAP_VIDEO_M2M_MPLANE)) { + // This is an M2M device (i.e. not a webcam), so allow access + policy->AddPath(rdwr, path.get()); + } + + close(fd); + } + closedir(dir); + + // FFmpeg V4L2 needs to list /dev to find V4L2 devices. + policy->AddPath(rdonly, "/dev"); +} +#endif // MOZ_ENABLE_V4L2 + +/* static */ UniquePtr +SandboxBrokerPolicyFactory::GetRDDPolicy(int aPid) { + auto policy = MakeUnique(); + + 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 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()); + +#ifdef MOZ_ENABLE_V4L2 + AddV4l2Dependencies(policy.get()); +#endif // MOZ_ENABLE_V4L2 + + if (policy->IsEmpty()) { + policy = nullptr; + } + return policy; +} + +/* static */ UniquePtr +SandboxBrokerPolicyFactory::GetSocketProcessPolicy(int aPid) { + auto policy = MakeUnique(); + + 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 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 +SandboxBrokerPolicyFactory::GetUtilityProcessPolicy(int aPid) { + auto policy = MakeUnique(); + + 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"); + // Required to make sure ffmpeg loads properly, this is already existing on + // Content and RDD + policy->AddDir(rdonly, "/nix/store"); + + // 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 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 + +namespace mozilla { + +class SandboxBrokerPolicyFactory { + public: + SandboxBrokerPolicyFactory() = default; + + UniquePtr GetContentPolicy(int aPid, + bool aFileProcess); + + static UniquePtr GetRDDPolicy(int aPid); + static UniquePtr GetSocketProcessPolicy(int aPid); + static UniquePtr GetUtilityProcessPolicy(int aPid); + + private: + UniquePtr 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 + * + * 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 +#include + +#include +#include +#include +#include + +#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 +#include +#include +#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" diff --git a/security/sandbox/linux/glue/SandboxCrash.cpp b/security/sandbox/linux/glue/SandboxCrash.cpp new file mode 100644 index 0000000000..9942925dbb --- /dev/null +++ b/security/sandbox/linux/glue/SandboxCrash.cpp @@ -0,0 +1,118 @@ +/* -*- 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/. */ + +// This file needs to be linked into libxul, so it can access the JS +// stack and the crash reporter. Everything else in this directory +// should be able to be linked into its own shared library, in order +// to be able to isolate sandbox/chromium from ipc/chromium. + +#include "SandboxInternal.h" +#include "SandboxLogging.h" + +#include +#include + +#include "mozilla/StackWalk.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/Exceptions.h" +#include "nsContentUtils.h" +#include "nsExceptionHandler.h" +#include "nsIException.h" // for nsIStackFrame +#include "nsString.h" +#include "nsThreadUtils.h" + +namespace mozilla { + +// Log JS stack info in the same place as the sandbox violation +// message. Useful in case the responsible code is JS and all we have +// are logs and a minidump with the C++ stacks (e.g., on TBPL). +static void SandboxLogJSStack(void) { + if (!NS_IsMainThread()) { + // This might be a worker thread... or it might be a non-JS + // thread, or a non-NSPR thread. There's isn't a good API for + // dealing with this, yet. + return; + } + if (!nsContentUtils::XPConnect()) { + // There is no content (e.g., the process is a media plugin), in + // which case this will probably crash and definitely not work. + return; + } + nsCOMPtr frame = dom::GetCurrentJSStack(); + // If we got a stack, we must have a current JSContext. This is icky. :( + // Would be better if GetCurrentJSStack() handed out the JSContext it ended up + // using or something. + JSContext* cx = frame ? nsContentUtils::GetCurrentJSContext() : nullptr; + for (int i = 0; frame != nullptr; ++i) { + nsAutoString fileName, funName; + int32_t lineNumber; + + // Don't stop unwinding if an attribute can't be read. + fileName.SetIsVoid(true); + frame->GetFilename(cx, fileName); + lineNumber = frame->GetLineNumber(cx); + funName.SetIsVoid(true); + frame->GetName(cx, funName); + + if (!funName.IsVoid() || !fileName.IsVoid()) { + SANDBOX_LOG("JS frame %d: %s %s line %d", i, + funName.IsVoid() ? "(anonymous)" + : NS_ConvertUTF16toUTF8(funName).get(), + fileName.IsVoid() ? "(no file)" + : NS_ConvertUTF16toUTF8(fileName).get(), + lineNumber); + } + + frame = frame->GetCaller(cx); + } +} + +static void SandboxPrintStackFrame(uint32_t aFrameNumber, void* aPC, void* aSP, + void* aClosure) { + char buf[1024]; + MozCodeAddressDetails details; + + MozDescribeCodeAddress(aPC, &details); + MozFormatCodeAddressDetails(buf, sizeof(buf), aFrameNumber, aPC, &details); + SANDBOX_LOG("frame %s", buf); +} + +static void SandboxLogCStack(const void* aFirstFramePC) { + // Warning: this might not print any stack frames. MozStackWalk + // can't walk past the signal trampoline on ARM (bug 968531), and + // x86 frame pointer walking may or may not work (bug 1082276). + + MozStackWalk(SandboxPrintStackFrame, aFirstFramePC, /* max */ 0, nullptr); + SANDBOX_LOG("end of stack."); +} + +static void SandboxCrash(int nr, siginfo_t* info, void* void_context, + const void* aFirstFramePC) { + pid_t pid = getpid(), tid = syscall(__NR_gettid); + bool dumped = CrashReporter::WriteMinidumpForSigInfo(nr, info, void_context); + + if (!dumped) { + SANDBOX_LOG( + "crash reporter is disabled (or failed);" + " trying stack trace:"); + SandboxLogCStack(aFirstFramePC); + } + + // Do this last, in case it crashes or deadlocks. + SandboxLogJSStack(); + + // Try to reraise, so the parent sees that this process crashed. + // (If tgkill is forbidden, then seccomp will raise SIGSYS, which + // also accomplishes that goal.) + signal(SIGSYS, SIG_DFL); + syscall(__NR_tgkill, pid, tid, nr); +} + +static void __attribute__((constructor)) SandboxSetCrashFunc() { + gSandboxCrashFunc = SandboxCrash; +} + +} // namespace mozilla diff --git a/security/sandbox/linux/glue/SandboxPrefBridge.cpp b/security/sandbox/linux/glue/SandboxPrefBridge.cpp new file mode 100644 index 0000000000..9782e58817 --- /dev/null +++ b/security/sandbox/linux/glue/SandboxPrefBridge.cpp @@ -0,0 +1,50 @@ +/* -*- 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 "Sandbox.h" + +#include "mozilla/Preferences.h" +#include "mozilla/SandboxSettings.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentParent.h" // for FILE_REMOTE_TYPE + +namespace mozilla { + +/* static */ ContentProcessSandboxParams +ContentProcessSandboxParams::ForThisProcess( + const Maybe& aBroker) { + ContentProcessSandboxParams params; + params.mLevel = GetEffectiveContentSandboxLevel(); + + if (aBroker.isSome()) { + auto fd = aBroker.value().ClonePlatformHandle(); + params.mBrokerFd = fd.release(); + // brokerFd < 0 means to allow direct filesystem access, so + // make absolutely sure that doesn't happen if the parent + // didn't intend it. + MOZ_RELEASE_ASSERT(params.mBrokerFd >= 0); + } + // (Otherwise, mBrokerFd will remain -1 from the default ctor.) + + auto* cc = dom::ContentChild::GetSingleton(); + params.mFileProcess = cc->GetRemoteType() == FILE_REMOTE_TYPE; + + nsAutoCString extraSyscalls; + nsresult rv = Preferences::GetCString( + "security.sandbox.content.syscall_whitelist", extraSyscalls); + if (NS_SUCCEEDED(rv)) { + for (const nsACString& callNrString : extraSyscalls.Split(',')) { + int callNr = PromiseFlatCString(callNrString).ToInteger(&rv); + if (NS_SUCCEEDED(rv)) { + params.mSyscallWhitelist.push_back(callNr); + } + } + } + + return params; +} + +} // namespace mozilla diff --git a/security/sandbox/linux/glue/moz.build b/security/sandbox/linux/glue/moz.build new file mode 100644 index 0000000000..e44a561d69 --- /dev/null +++ b/security/sandbox/linux/glue/moz.build @@ -0,0 +1,35 @@ +# -*- 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/. + +UNIFIED_SOURCES += [ + "../SandboxLogging.cpp", + "SandboxCrash.cpp", + "SandboxPrefBridge.cpp", +] + +SOURCES += [ + "../../chromium/base/strings/safe_sprintf.cc", +] + +# Avoid Chromium logging dependency, because this is going into +# libxul. See also the comment in SandboxLogging.h. +SOURCES["../../chromium/base/strings/safe_sprintf.cc"].flags += ["-DNDEBUG"] + +# Need this for mozilla::ipc::FileDescriptor etc. +include("/ipc/chromium/chromium-config.mozbuild") + +LOCAL_INCLUDES += [ + # Need this for safe_sprintf.h used by SandboxLogging.h, + # but it has to be after ipc/chromium/src. + "/security/sandbox/chromium", + "/security/sandbox/linux", +] + +USE_LIBS += [ + "mozsandbox", +] + +FINAL_LIBRARY = "xul" diff --git a/security/sandbox/linux/gtest/TestBroker.cpp b/security/sandbox/linux/gtest/TestBroker.cpp new file mode 100644 index 0000000000..07fcaa889a --- /dev/null +++ b/security/sandbox/linux/gtest/TestBroker.cpp @@ -0,0 +1,689 @@ +/* -*- 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 "gtest/gtest.h" + +#include "broker/SandboxBroker.h" +#include "broker/SandboxBrokerUtils.h" +#include "SandboxBrokerClient.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mozilla/Atomics.h" +#include "mozilla/PodOperations.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/ipc/FileDescriptor.h" + +namespace mozilla { + +class SandboxBrokerTest : public ::testing::Test { + static const int MAY_ACCESS = SandboxBroker::MAY_ACCESS; + static const int MAY_READ = SandboxBroker::MAY_READ; + static const int MAY_WRITE = SandboxBroker::MAY_WRITE; + static const int MAY_CREATE = SandboxBroker::MAY_CREATE; + static const auto AddAlways = SandboxBroker::Policy::AddAlways; + + UniquePtr mServer; + UniquePtr mClient; + + UniquePtr GetPolicy() const; + + template + static void* ThreadMain(void* arg) { + (static_cast(arg)->*Main)(); + return nullptr; + } + + protected: + int Open(const char* aPath, int aFlags) { + return mClient->Open(aPath, aFlags); + } + int Access(const char* aPath, int aMode) { + return mClient->Access(aPath, aMode); + } + int Stat(const char* aPath, statstruct* aStat) { + return mClient->Stat(aPath, aStat); + } + int LStat(const char* aPath, statstruct* aStat) { + return mClient->LStat(aPath, aStat); + } + int Chmod(const char* aPath, int aMode) { + return mClient->Chmod(aPath, aMode); + } + int Link(const char* aPath, const char* bPath) { + return mClient->Link(aPath, bPath); + } + int Mkdir(const char* aPath, int aMode) { + return mClient->Mkdir(aPath, aMode); + } + int Symlink(const char* aPath, const char* bPath) { + return mClient->Symlink(aPath, bPath); + } + int Rename(const char* aPath, const char* bPath) { + return mClient->Rename(aPath, bPath); + } + int Rmdir(const char* aPath) { return mClient->Rmdir(aPath); } + int Unlink(const char* aPath) { return mClient->Unlink(aPath); } + ssize_t Readlink(const char* aPath, char* aBuff, size_t aSize) { + return mClient->Readlink(aPath, aBuff, aSize); + } + + void SetUp() override { + ipc::FileDescriptor fd; + + mServer = SandboxBroker::Create(GetPolicy(), getpid(), fd); + ASSERT_NE(mServer, nullptr); + ASSERT_TRUE(fd.IsValid()); + auto rawFD = fd.TakePlatformHandle(); + mClient.reset(new SandboxBrokerClient(rawFD.release())); + } + + template + void StartThread(pthread_t* aThread) { + ASSERT_EQ(0, pthread_create(aThread, nullptr, ThreadMain, + static_cast(this))); + } + + template + void RunOnManyThreads() { + static const int kNumThreads = 5; + pthread_t threads[kNumThreads]; + for (pthread_t& thread : threads) { + StartThread(&thread); + } + for (pthread_t thread : threads) { + void* retval; + ASSERT_EQ(pthread_join(thread, &retval), 0); + ASSERT_EQ(retval, static_cast(nullptr)); + } + } + + public: + void MultiThreadOpenWorker(); + void MultiThreadStatWorker(); +}; + +UniquePtr SandboxBrokerTest::GetPolicy() const { + UniquePtr policy(new SandboxBroker::Policy()); + + policy->AddPath(MAY_READ | MAY_WRITE, "/dev/null", AddAlways); + policy->AddPath(MAY_READ, "/dev/zero", AddAlways); + policy->AddPath(MAY_READ, "/var/empty/qwertyuiop", AddAlways); + policy->AddPath(MAY_ACCESS, "/proc/self", + AddAlways); // Warning: Linux-specific. + policy->AddPath(MAY_READ | MAY_WRITE, "/tmp", AddAlways); + policy->AddPath(MAY_READ | MAY_WRITE | MAY_CREATE, "/tmp/blublu", AddAlways); + policy->AddPath(MAY_READ | MAY_WRITE | MAY_CREATE, "/tmp/blublublu", + AddAlways); + // This should be non-writable by the user running the test: + policy->AddPath(MAY_READ | MAY_WRITE, "/etc", AddAlways); + + return std::move(policy); +} + +TEST_F(SandboxBrokerTest, OpenForRead) { + int fd; + + fd = Open("/dev/null", O_RDONLY); + ASSERT_GE(fd, 0) << "Opening /dev/null failed."; + close(fd); + fd = Open("/dev/zero", O_RDONLY); + ASSERT_GE(fd, 0) << "Opening /dev/zero failed."; + close(fd); + fd = Open("/var/empty/qwertyuiop", O_RDONLY); + EXPECT_EQ(-ENOENT, fd) << "Opening allowed but nonexistent file succeeded."; + fd = Open("/proc/self", O_RDONLY); + EXPECT_EQ(-EACCES, fd) << "Opening stat-only file for read succeeded."; + fd = Open("/proc/self/stat", O_RDONLY); + EXPECT_EQ(-EACCES, fd) << "Opening disallowed file succeeded."; +} + +TEST_F(SandboxBrokerTest, OpenForWrite) { + int fd; + + fd = Open("/dev/null", O_WRONLY); + ASSERT_GE(fd, 0) << "Opening /dev/null write-only failed."; + close(fd); + fd = Open("/dev/null", O_RDWR); + ASSERT_GE(fd, 0) << "Opening /dev/null read/write failed."; + close(fd); + fd = Open("/dev/zero", O_WRONLY); + ASSERT_EQ(-EACCES, fd) + << "Opening read-only-by-policy file write-only succeeded."; + fd = Open("/dev/zero", O_RDWR); + ASSERT_EQ(-EACCES, fd) + << "Opening read-only-by-policy file read/write succeeded."; +} + +TEST_F(SandboxBrokerTest, SimpleRead) { + int fd; + char c; + + fd = Open("/dev/null", O_RDONLY); + ASSERT_GE(fd, 0); + EXPECT_EQ(0, read(fd, &c, 1)); + close(fd); + fd = Open("/dev/zero", O_RDONLY); + ASSERT_GE(fd, 0); + ASSERT_EQ(1, read(fd, &c, 1)); + EXPECT_EQ(c, '\0'); +} + +TEST_F(SandboxBrokerTest, BadFlags) { + int fd; + + fd = Open("/dev/null", O_RDWR | O_ASYNC); + EXPECT_EQ(-EACCES, fd) << "O_ASYNC is banned."; + + fd = Open("/dev/null", O_RDWR | 0x40000000); + EXPECT_EQ(-EACCES, fd) << "Unknown flag 0x40000000 is banned."; +} + +TEST_F(SandboxBrokerTest, Access) { + EXPECT_EQ(0, Access("/dev/null", F_OK)); + EXPECT_EQ(0, Access("/dev/null", R_OK)); + EXPECT_EQ(0, Access("/dev/null", W_OK)); + EXPECT_EQ(0, Access("/dev/null", R_OK | W_OK)); + EXPECT_EQ(-EACCES, Access("/dev/null", X_OK)); + EXPECT_EQ(-EACCES, Access("/dev/null", R_OK | X_OK)); + + EXPECT_EQ(0, Access("/dev/zero", R_OK)); + EXPECT_EQ(-EACCES, Access("/dev/zero", W_OK)); + EXPECT_EQ(-EACCES, Access("/dev/zero", R_OK | W_OK)); + + EXPECT_EQ(-ENOENT, Access("/var/empty/qwertyuiop", R_OK)); + EXPECT_EQ(-EACCES, Access("/var/empty/qwertyuiop", W_OK)); + + EXPECT_EQ(0, Access("/proc/self", F_OK)); + EXPECT_EQ(-EACCES, Access("/proc/self", R_OK)); + + EXPECT_EQ(-EACCES, Access("/proc/self/stat", F_OK)); + + EXPECT_EQ(0, Access("/tmp", X_OK)); + EXPECT_EQ(0, Access("/tmp", R_OK | X_OK)); + EXPECT_EQ(0, Access("/tmp", R_OK | W_OK | X_OK)); + EXPECT_EQ(0, Access("/proc/self", X_OK)); + + EXPECT_EQ(0, Access("/etc", R_OK | X_OK)); + EXPECT_EQ(-EACCES, Access("/etc", W_OK)); +} + +TEST_F(SandboxBrokerTest, Stat) { + statstruct realStat, brokeredStat; + ASSERT_EQ(0, statsyscall("/dev/null", &realStat)) << "Shouldn't ever fail!"; + EXPECT_EQ(0, Stat("/dev/null", &brokeredStat)); + EXPECT_EQ(realStat.st_ino, brokeredStat.st_ino); + EXPECT_EQ(realStat.st_rdev, brokeredStat.st_rdev); + +#if defined(__clang__) || defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wnonnull" +#endif + EXPECT_EQ(-1, statsyscall(nullptr, &realStat)); + EXPECT_EQ(errno, EFAULT); + + EXPECT_EQ(-EFAULT, Stat(nullptr, &brokeredStat)); +#if defined(__clang__) || defined(__GNUC__) +# pragma GCC diagnostic pop +#endif + + EXPECT_EQ(-ENOENT, Stat("/var/empty/qwertyuiop", &brokeredStat)); + EXPECT_EQ(-EACCES, Stat("/dev", &brokeredStat)); + + EXPECT_EQ(0, Stat("/proc/self", &brokeredStat)); + EXPECT_TRUE(S_ISDIR(brokeredStat.st_mode)); +} + +TEST_F(SandboxBrokerTest, LStat) { + statstruct realStat, brokeredStat; + ASSERT_EQ(0, lstatsyscall("/dev/null", &realStat)); + EXPECT_EQ(0, LStat("/dev/null", &brokeredStat)); + EXPECT_EQ(realStat.st_ino, brokeredStat.st_ino); + EXPECT_EQ(realStat.st_rdev, brokeredStat.st_rdev); + + EXPECT_EQ(-ENOENT, LStat("/var/empty/qwertyuiop", &brokeredStat)); + EXPECT_EQ(-EACCES, LStat("/dev", &brokeredStat)); + + EXPECT_EQ(0, LStat("/proc/self", &brokeredStat)); + EXPECT_TRUE(S_ISLNK(brokeredStat.st_mode)); +} + +static void PrePostTestCleanup(void) { + unlink("/tmp/blublu"); + rmdir("/tmp/blublu"); + unlink("/tmp/nope"); + rmdir("/tmp/nope"); + unlink("/tmp/blublublu"); + rmdir("/tmp/blublublu"); +} + +TEST_F(SandboxBrokerTest, Chmod) { + PrePostTestCleanup(); + + int fd = Open("/tmp/blublu", O_WRONLY | O_CREAT); + ASSERT_GE(fd, 0) << "Opening /tmp/blublu for writing failed."; + close(fd); + // Set read only. SandboxBroker enforces 0600 mode flags. + ASSERT_EQ(0, Chmod("/tmp/blublu", S_IRUSR)); + EXPECT_EQ(-EACCES, Access("/tmp/blublu", W_OK)); + statstruct realStat; + EXPECT_EQ(0, statsyscall("/tmp/blublu", &realStat)); + EXPECT_EQ((mode_t)S_IRUSR, realStat.st_mode & 0777); + + ASSERT_EQ(0, Chmod("/tmp/blublu", S_IRUSR | S_IWUSR)); + EXPECT_EQ(0, statsyscall("/tmp/blublu", &realStat)); + EXPECT_EQ((mode_t)(S_IRUSR | S_IWUSR), realStat.st_mode & 0777); + EXPECT_EQ(0, unlink("/tmp/blublu")); + + PrePostTestCleanup(); +} + +TEST_F(SandboxBrokerTest, Link) { + PrePostTestCleanup(); + + int fd = Open("/tmp/blublu", O_WRONLY | O_CREAT); + ASSERT_GE(fd, 0) << "Opening /tmp/blublu for writing failed."; + close(fd); + ASSERT_EQ(0, Link("/tmp/blublu", "/tmp/blublublu")); + EXPECT_EQ(0, Access("/tmp/blublublu", F_OK)); + // Not whitelisted target path + EXPECT_EQ(-EACCES, Link("/tmp/blublu", "/tmp/nope")); + EXPECT_EQ(0, unlink("/tmp/blublublu")); + EXPECT_EQ(0, unlink("/tmp/blublu")); + + PrePostTestCleanup(); +} + +TEST_F(SandboxBrokerTest, Symlink) { + PrePostTestCleanup(); + + int fd = Open("/tmp/blublu", O_WRONLY | O_CREAT); + ASSERT_GE(fd, 0) << "Opening /tmp/blublu for writing failed."; + close(fd); + ASSERT_EQ(0, Symlink("/tmp/blublu", "/tmp/blublublu")); + EXPECT_EQ(0, Access("/tmp/blublublu", F_OK)); + statstruct aStat; + ASSERT_EQ(0, lstatsyscall("/tmp/blublublu", &aStat)); + EXPECT_EQ((mode_t)S_IFLNK, aStat.st_mode & S_IFMT); + // Not whitelisted target path + EXPECT_EQ(-EACCES, Symlink("/tmp/blublu", "/tmp/nope")); + EXPECT_EQ(0, unlink("/tmp/blublublu")); + EXPECT_EQ(0, unlink("/tmp/blublu")); + + PrePostTestCleanup(); +} + +TEST_F(SandboxBrokerTest, Mkdir) { + PrePostTestCleanup(); + + ASSERT_EQ(0, mkdir("/tmp/blublu", 0600)) + << "Creating dir /tmp/blublu failed."; + EXPECT_EQ(0, Access("/tmp/blublu", F_OK)); + // Not whitelisted target path + EXPECT_EQ(-EACCES, Mkdir("/tmp/nope", 0600)) + << "Creating dir without MAY_CREATE succeed."; + EXPECT_EQ(0, rmdir("/tmp/blublu")); + EXPECT_EQ(-EEXIST, Mkdir("/proc/self", 0600)) + << "Creating uncreatable dir that already exists didn't fail correctly."; + EXPECT_EQ(-EEXIST, Mkdir("/dev/zero", 0600)) + << "Creating uncreatable dir over preexisting file didn't fail " + "correctly."; + + PrePostTestCleanup(); +} + +TEST_F(SandboxBrokerTest, Rename) { + PrePostTestCleanup(); + + ASSERT_EQ(0, mkdir("/tmp/blublu", 0600)) + << "Creating dir /tmp/blublu failed."; + EXPECT_EQ(0, Access("/tmp/blublu", F_OK)); + ASSERT_EQ(0, Rename("/tmp/blublu", "/tmp/blublublu")); + EXPECT_EQ(0, Access("/tmp/blublublu", F_OK)); + EXPECT_EQ(-ENOENT, Access("/tmp/blublu", F_OK)); + // Not whitelisted target path + EXPECT_EQ(-EACCES, Rename("/tmp/blublublu", "/tmp/nope")) + << "Renaming dir without write access succeed."; + EXPECT_EQ(0, rmdir("/tmp/blublublu")); + + PrePostTestCleanup(); +} + +TEST_F(SandboxBrokerTest, Rmdir) { + PrePostTestCleanup(); + + ASSERT_EQ(0, mkdir("/tmp/blublu", 0600)) + << "Creating dir /tmp/blublu failed."; + EXPECT_EQ(0, Access("/tmp/blublu", F_OK)); + ASSERT_EQ(0, Rmdir("/tmp/blublu")); + EXPECT_EQ(-ENOENT, Access("/tmp/blublu", F_OK)); + // Bypass sandbox to create a non-deletable dir + ASSERT_EQ(0, mkdir("/tmp/nope", 0600)); + EXPECT_EQ(-EACCES, Rmdir("/tmp/nope")); + + PrePostTestCleanup(); +} + +TEST_F(SandboxBrokerTest, Unlink) { + PrePostTestCleanup(); + + int fd = Open("/tmp/blublu", O_WRONLY | O_CREAT); + ASSERT_GE(fd, 0) << "Opening /tmp/blublu for writing failed."; + close(fd); + EXPECT_EQ(0, Access("/tmp/blublu", F_OK)); + EXPECT_EQ(0, Unlink("/tmp/blublu")); + EXPECT_EQ(-ENOENT, Access("/tmp/blublu", F_OK)); + // Bypass sandbox to write a non-deletable file + fd = open("/tmp/nope", O_WRONLY | O_CREAT, 0600); + ASSERT_GE(fd, 0) << "Opening /tmp/nope for writing failed."; + close(fd); + EXPECT_EQ(-EACCES, Unlink("/tmp/nope")); + + PrePostTestCleanup(); +} + +TEST_F(SandboxBrokerTest, Readlink) { + PrePostTestCleanup(); + + int fd = Open("/tmp/blublu", O_WRONLY | O_CREAT); + ASSERT_GE(fd, 0) << "Opening /tmp/blublu for writing failed."; + close(fd); + ASSERT_EQ(0, Symlink("/tmp/blublu", "/tmp/blublublu")); + EXPECT_EQ(0, Access("/tmp/blublublu", F_OK)); + char linkBuff[256]; + EXPECT_EQ(11, Readlink("/tmp/blublublu", linkBuff, sizeof(linkBuff))); + linkBuff[11] = '\0'; + EXPECT_EQ(0, strcmp(linkBuff, "/tmp/blublu")); + + PrePostTestCleanup(); +} + +TEST_F(SandboxBrokerTest, MultiThreadOpen) { + RunOnManyThreads(); +} +void SandboxBrokerTest::MultiThreadOpenWorker() { + static const int kNumLoops = 10000; + + for (int i = 1; i <= kNumLoops; ++i) { + int nullfd = Open("/dev/null", O_RDONLY); + int zerofd = Open("/dev/zero", O_RDONLY); + ASSERT_GE(nullfd, 0) << "Loop " << i << "/" << kNumLoops; + ASSERT_GE(zerofd, 0) << "Loop " << i << "/" << kNumLoops; + char c; + ASSERT_EQ(0, read(nullfd, &c, 1)) << "Loop " << i << "/" << kNumLoops; + ASSERT_EQ(1, read(zerofd, &c, 1)) << "Loop " << i << "/" << kNumLoops; + ASSERT_EQ('\0', c) << "Loop " << i << "/" << kNumLoops; + close(nullfd); + close(zerofd); + } +} + +TEST_F(SandboxBrokerTest, MultiThreadStat) { + RunOnManyThreads(); +} +void SandboxBrokerTest::MultiThreadStatWorker() { + static const int kNumLoops = 7500; + statstruct nullStat, zeroStat, selfStat; + dev_t realNullDev, realZeroDev; + ino_t realSelfInode; + + ASSERT_EQ(0, statsyscall("/dev/null", &nullStat)) << "Shouldn't ever fail!"; + ASSERT_EQ(0, statsyscall("/dev/zero", &zeroStat)) << "Shouldn't ever fail!"; + ASSERT_EQ(0, lstatsyscall("/proc/self", &selfStat)) << "Shouldn't ever fail!"; + ASSERT_TRUE(S_ISLNK(selfStat.st_mode)) + << "Shouldn't ever fail!"; + realNullDev = nullStat.st_rdev; + realZeroDev = zeroStat.st_rdev; + realSelfInode = selfStat.st_ino; + for (int i = 1; i <= kNumLoops; ++i) { + ASSERT_EQ(0, Stat("/dev/null", &nullStat)) + << "Loop " << i << "/" << kNumLoops; + ASSERT_EQ(0, Stat("/dev/zero", &zeroStat)) + << "Loop " << i << "/" << kNumLoops; + ASSERT_EQ(0, LStat("/proc/self", &selfStat)) + << "Loop " << i << "/" << kNumLoops; + + ASSERT_EQ(realNullDev, nullStat.st_rdev) + << "Loop " << i << "/" << kNumLoops; + ASSERT_EQ(realZeroDev, zeroStat.st_rdev) + << "Loop " << i << "/" << kNumLoops; + ASSERT_TRUE(S_ISLNK(selfStat.st_mode)) + << "Loop " << i << "/" << kNumLoops; + ASSERT_EQ(realSelfInode, selfStat.st_ino) + << "Loop " << i << "/" << kNumLoops; + } +} + +#if 0 +class SandboxBrokerSigStress : public SandboxBrokerTest +{ + int mSigNum; + struct sigaction mOldAction; + Atomic mVoidPtr; + + static void SigHandler(int aSigNum, siginfo_t* aSigInfo, void *aCtx) { + ASSERT_EQ(SI_QUEUE, aSigInfo->si_code); + SandboxBrokerSigStress* that = + static_cast(aSigInfo->si_value.sival_ptr); + ASSERT_EQ(that->mSigNum, aSigNum); + that->DoSomething(); + } + +protected: + Atomic mTestIter; + sem_t mSemaphore; + + void SignalThread(pthread_t aThread) { + union sigval sv; + sv.sival_ptr = this; + ASSERT_NE(0, mSigNum); + ASSERT_EQ(0, pthread_sigqueue(aThread, mSigNum, sv)); + } + + virtual void SetUp() { + ASSERT_EQ(0, sem_init(&mSemaphore, 0, 0)); + mVoidPtr = nullptr; + mSigNum = 0; + for (int sigNum = SIGRTMIN; sigNum < SIGRTMAX; ++sigNum) { + ASSERT_EQ(0, sigaction(sigNum, nullptr, &mOldAction)); + if ((mOldAction.sa_flags & SA_SIGINFO) == 0 && + mOldAction.sa_handler == SIG_DFL) { + struct sigaction newAction; + PodZero(&newAction); + newAction.sa_flags = SA_SIGINFO; + newAction.sa_sigaction = SigHandler; + ASSERT_EQ(0, sigaction(sigNum, &newAction, nullptr)); + mSigNum = sigNum; + break; + } + } + ASSERT_NE(mSigNum, 0); + + SandboxBrokerTest::SetUp(); + } + + virtual void TearDown() { + ASSERT_EQ(0, sem_destroy(&mSemaphore)); + if (mSigNum != 0) { + ASSERT_EQ(0, sigaction(mSigNum, &mOldAction, nullptr)); + } + if (mVoidPtr) { + free(mVoidPtr); + } + } + + void DoSomething(); + +public: + void MallocWorker(); + void FreeWorker(); +}; + +TEST_F(SandboxBrokerSigStress, StressTest) +{ + static const int kIters = 6250; + static const int kNsecPerIterPerIter = 4; + struct timespec delay = { 0, 0 }; + pthread_t threads[2]; + + mTestIter = kIters; + + StartThread(&threads[0]); + StartThread(&threads[1]); + + for (int i = kIters; i > 0; --i) { + SignalThread(threads[i % 2]); + while (sem_wait(&mSemaphore) == -1 && errno == EINTR) + /* retry */; + ASSERT_EQ(i - 1, mTestIter); + delay.tv_nsec += kNsecPerIterPerIter; + struct timespec req = delay, rem; + while (nanosleep(&req, &rem) == -1 && errno == EINTR) { + req = rem; + } + } + void *retval; + ASSERT_EQ(0, pthread_join(threads[0], &retval)); + ASSERT_EQ(nullptr, retval); + ASSERT_EQ(0, pthread_join(threads[1], &retval)); + ASSERT_EQ(nullptr, retval); +} + +void +SandboxBrokerSigStress::MallocWorker() +{ + static const size_t kSize = 64; + + void* mem = malloc(kSize); + while (mTestIter > 0) { + ASSERT_NE(mem, mVoidPtr); + mem = mVoidPtr.exchange(mem); + if (mem) { + sched_yield(); + } else { + mem = malloc(kSize); + } + } + if (mem) { + free(mem); + } +} + +void +SandboxBrokerSigStress::FreeWorker() +{ + void *mem = nullptr; + while (mTestIter > 0) { + mem = mVoidPtr.exchange(mem); + if (mem) { + free(mem); + mem = nullptr; + } else { + sched_yield(); + } + } +} + +void +SandboxBrokerSigStress::DoSomething() +{ + int fd; + char c; + struct stat st; + + //fprintf(stderr, "Don't try this at home: %d\n", static_cast(mTestIter)); + switch (mTestIter % 5) { + case 0: + fd = Open("/dev/null", O_RDWR); + ASSERT_GE(fd, 0); + ASSERT_EQ(0, read(fd, &c, 1)); + close(fd); + break; + case 1: + fd = Open("/dev/zero", O_RDONLY); + ASSERT_GE(fd, 0); + ASSERT_EQ(1, read(fd, &c, 1)); + ASSERT_EQ('\0', c); + close(fd); + break; + case 2: + ASSERT_EQ(0, Access("/dev/null", W_OK)); + break; + case 3: + ASSERT_EQ(0, Stat("/proc/self", &st)); + ASSERT_TRUE(S_ISDIR(st.st_mode)); + break; + case 4: + ASSERT_EQ(0, LStat("/proc/self", &st)); + ASSERT_TRUE(S_ISLNK(st.st_mode)); + break; + } + mTestIter--; + sem_post(&mSemaphore); +} +#endif + +// Check for fd leaks when creating/destroying a broker instance (bug +// 1719391). +// +// (This uses a different test group because it doesn't use the +// fixture class, and gtest doesn't allow mixing TEST and TEST_F in +// the same group.) +TEST(SandboxBrokerMisc, LeakCheck) +{ + // If this value is increased in the future, check that it won't + // cause the test to take an excessive amount of time: + static constexpr size_t kCycles = 4096; + struct rlimit oldLimit; + bool changedLimit = false; + + // At the time of this writing, we raise the fd soft limit to 4096 + // (or to the hard limit, if less than that), but we don't lower it + // if it was higher than 4096. To allow for that case, or if + // Gecko's preferred limit changes, the limit is reduced while this + // test is running: + ASSERT_EQ(getrlimit(RLIMIT_NOFILE, &oldLimit), 0) << strerror(errno); + if (oldLimit.rlim_cur == RLIM_INFINITY || + oldLimit.rlim_cur > static_cast(kCycles)) { + struct rlimit newLimit = oldLimit; + newLimit.rlim_cur = static_cast(kCycles); + ASSERT_EQ(setrlimit(RLIMIT_NOFILE, &newLimit), 0) << strerror(errno); + changedLimit = true; + } + + pid_t pid = getpid(); + for (size_t i = 0; i < kCycles; ++i) { + auto policy = MakeUnique(); + // Currently nothing in `Create` tries to check for or + // special-case an empty policy, but just in case: + policy->AddPath(SandboxBroker::MAY_READ, "/dev/null", + SandboxBroker::Policy::AddAlways); + ipc::FileDescriptor fd; + auto broker = SandboxBroker::Create(std::move(policy), pid, fd); + ASSERT_TRUE(broker); + ASSERT_TRUE(fd.IsValid()); + } + + if (changedLimit) { + ASSERT_EQ(setrlimit(RLIMIT_NOFILE, &oldLimit), 0) << strerror(errno); + } +} + +} // namespace mozilla diff --git a/security/sandbox/linux/gtest/TestBrokerPolicy.cpp b/security/sandbox/linux/gtest/TestBrokerPolicy.cpp new file mode 100644 index 0000000000..d881e6d290 --- /dev/null +++ b/security/sandbox/linux/gtest/TestBrokerPolicy.cpp @@ -0,0 +1,95 @@ +/* -*- 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 "gtest/gtest.h" + +#include "broker/SandboxBroker.h" + +namespace mozilla { + +static const int MAY_ACCESS = SandboxBroker::MAY_ACCESS; +static const int MAY_READ = SandboxBroker::MAY_READ; +static const int MAY_WRITE = SandboxBroker::MAY_WRITE; +// static const int MAY_CREATE = SandboxBroker::MAY_CREATE; +// static const int RECURSIVE = SandboxBroker::RECURSIVE; +static const auto AddAlways = SandboxBroker::Policy::AddAlways; + +TEST(SandboxBrokerPolicyLookup, Simple) +{ + SandboxBroker::Policy p; + p.AddPath(MAY_READ, "/dev/urandom", AddAlways); + + EXPECT_NE(0, p.Lookup("/dev/urandom")) << "Added path not found."; + EXPECT_EQ(MAY_ACCESS | MAY_READ, p.Lookup("/dev/urandom")) + << "Added path found with wrong perms."; + EXPECT_EQ(0, p.Lookup("/etc/passwd")) << "Non-added path was found."; +} + +TEST(SandboxBrokerPolicyLookup, CopyCtor) +{ + SandboxBroker::Policy psrc; + psrc.AddPath(MAY_READ | MAY_WRITE, "/dev/null", AddAlways); + SandboxBroker::Policy pdst(psrc); + psrc.AddPath(MAY_READ, "/dev/zero", AddAlways); + pdst.AddPath(MAY_READ, "/dev/urandom", AddAlways); + + EXPECT_EQ(MAY_ACCESS | MAY_READ | MAY_WRITE, psrc.Lookup("/dev/null")) + << "Common path absent in copy source."; + EXPECT_EQ(MAY_ACCESS | MAY_READ | MAY_WRITE, pdst.Lookup("/dev/null")) + << "Common path absent in copy destination."; + + EXPECT_EQ(MAY_ACCESS | MAY_READ, psrc.Lookup("/dev/zero")) + << "Source-only path is absent."; + EXPECT_EQ(0, pdst.Lookup("/dev/zero")) + << "Source-only path is present in copy destination."; + + EXPECT_EQ(0, psrc.Lookup("/dev/urandom")) + << "Destination-only path is present in copy source."; + EXPECT_EQ(MAY_ACCESS | MAY_READ, pdst.Lookup("/dev/urandom")) + << "Destination-only path is absent."; + + EXPECT_EQ(0, psrc.Lookup("/etc/passwd")) + << "Non-added path is present in copy source."; + EXPECT_EQ(0, pdst.Lookup("/etc/passwd")) + << "Non-added path is present in copy source."; +} + +TEST(SandboxBrokerPolicyLookup, Recursive) +{ + SandboxBroker::Policy psrc; + psrc.AddPath(MAY_READ | MAY_WRITE, "/dev/null", AddAlways); + psrc.AddPath(MAY_READ, "/dev/zero", AddAlways); + psrc.AddPath(MAY_READ, "/dev/urandom", AddAlways); + + EXPECT_EQ(MAY_ACCESS | MAY_READ | MAY_WRITE, psrc.Lookup("/dev/null")) + << "Basic path is present."; + EXPECT_EQ(MAY_ACCESS | MAY_READ, psrc.Lookup("/dev/zero")) + << "Basic path has no extra flags"; + + psrc.AddDir(MAY_READ | MAY_WRITE, "/dev/"); + + EXPECT_EQ(MAY_ACCESS | MAY_READ | MAY_WRITE, psrc.Lookup("/dev/random")) + << "Permission via recursive dir."; + EXPECT_EQ(MAY_ACCESS | MAY_READ | MAY_WRITE, psrc.Lookup("/dev/sd/0")) + << "Permission via recursive dir, nested deeper"; + EXPECT_EQ(0, psrc.Lookup("/dev/sd/0/")) << "Invalid path format."; + EXPECT_EQ(0, psrc.Lookup("/usr/dev/sd")) << "Match must be a prefix."; + + psrc.AddDir(MAY_READ, "/dev/sd/"); + EXPECT_EQ(MAY_ACCESS | MAY_READ | MAY_WRITE, psrc.Lookup("/dev/sd/0")) + << "Extra permissions from parent path granted."; + EXPECT_EQ(0, psrc.Lookup("/dev/..")) << "Refuse attempted subdir escape."; + + psrc.AddDir(MAY_READ, "/tmp"); + EXPECT_EQ(MAY_ACCESS | MAY_READ, psrc.Lookup("/tmp/good/a")) + << "Check whether dir add with no trailing / was sucessful."; + EXPECT_EQ(0, psrc.Lookup("/tmp_good_but_bad")) + << "Enforce terminator on directories."; + EXPECT_EQ(0, psrc.Lookup("/tmp/.")) + << "Do not allow opening a directory handle."; +} + +} // namespace mozilla diff --git a/security/sandbox/linux/gtest/TestLogging.cpp b/security/sandbox/linux/gtest/TestLogging.cpp new file mode 100644 index 0000000000..eb0c9b747e --- /dev/null +++ b/security/sandbox/linux/gtest/TestLogging.cpp @@ -0,0 +1,56 @@ +/* -*- 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 "gtest/gtest.h" + +#include "SandboxLogging.h" + +#include + +namespace mozilla { + +TEST(SandboxLogging, ErrorName1) +{ + char buf[32]; + ssize_t n = GetLibcErrorName(buf, sizeof(buf), EINVAL); + EXPECT_EQ(n, 6); + EXPECT_STREQ(buf, "EINVAL"); +} + +TEST(SandboxLogging, ErrorName2) +{ + char buf[32]; + ssize_t n = GetLibcErrorName(buf, sizeof(buf), EIO); + EXPECT_EQ(n, 3); + EXPECT_STREQ(buf, "EIO"); +} + +TEST(SandboxLogging, ErrorName3) +{ + char buf[32]; + ssize_t n = GetLibcErrorName(buf, sizeof(buf), ESTALE); + EXPECT_EQ(n, 6); + EXPECT_STREQ(buf, "ESTALE"); +} + +TEST(SandboxLogging, ErrorNameFail) +{ + char buf[32]; + ssize_t n = GetLibcErrorName(buf, sizeof(buf), 4095); + EXPECT_EQ(n, 10); + EXPECT_STREQ(buf, "error 4095"); +} + +TEST(SandboxLogging, ErrorNameTrunc) +{ + char buf[] = "++++++++"; + ssize_t n = GetLibcErrorName(buf, 3, EINVAL); + EXPECT_EQ(n, 6); + EXPECT_STREQ(buf, "EI"); + EXPECT_STREQ(buf + 3, "+++++"); +} + +} // namespace mozilla diff --git a/security/sandbox/linux/gtest/moz.build b/security/sandbox/linux/gtest/moz.build new file mode 100644 index 0000000000..f80482aee3 --- /dev/null +++ b/security/sandbox/linux/gtest/moz.build @@ -0,0 +1,26 @@ +# -*- Mode: python; 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/. + +Library("sandboxtest") + +UNIFIED_SOURCES = [ + "../SandboxBrokerClient.cpp", + "TestBroker.cpp", + "TestBrokerPolicy.cpp", + "TestLogging.cpp", +] + +LOCAL_INCLUDES += [ + "/security/sandbox/linux", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +LOCAL_INCLUDES += [ + "/security/sandbox/chromium", +] + +FINAL_LIBRARY = "xul-gtest" diff --git a/security/sandbox/linux/interfaces/moz.build b/security/sandbox/linux/interfaces/moz.build new file mode 100644 index 0000000000..4ff62e8aba --- /dev/null +++ b/security/sandbox/linux/interfaces/moz.build @@ -0,0 +1,11 @@ +# -*- 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/. + +XPIDL_MODULE = "sandbox" + +XPIDL_SOURCES += [ + "mozISandboxReporter.idl", +] diff --git a/security/sandbox/linux/interfaces/mozISandboxReporter.idl b/security/sandbox/linux/interfaces/mozISandboxReporter.idl new file mode 100644 index 0000000000..82f3ab7a72 --- /dev/null +++ b/security/sandbox/linux/interfaces/mozISandboxReporter.idl @@ -0,0 +1,65 @@ +/* -*- Mode: IDL; 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 "nsISupports.idl" + +// A wrapper for the C++ class SandboxReport, representing one system +// call that was rejected by policy. +[scriptable, builtinclass, uuid(ed1e84d3-3346-42e1-b28c-e76a77f549f0)] +interface mozISandboxReport : nsISupports +{ + // The timestamp relative to the time when this property is read. + // This is mainly meant for distinguishing recent events that might + // be related to an observable failure from older ones that may be + // unrelated, not for exact timing. + readonly attribute uint64_t msecAgo; + readonly attribute int32_t pid; + readonly attribute int32_t tid; + readonly attribute ACString procType; + readonly attribute uint32_t syscall; + // Currently numArgs is effectively a constant and indicates the + // maximum number of arguments possible on the platform; the actual + // system call may use fewer. + readonly attribute uint32_t numArgs; + // The argument values are presented as strings because JS doesn't + // have 64-bit integers and data would be lost on 64-bit platforms + // if the XPIDL type uint64_t were used. The string may be decimal + // or hex (with leading "0x"). + ACString getArg(in uint32_t aIndex); +}; + +// A wrapper for SandboxReporter::Snapshot, representing the most +// recent SandboxReport events. Index 0 is the first report in the +// session, and so on; exposing the indices like this lets us see how +// many reports have been received even though only a limited number +// of them are stored. +[scriptable, builtinclass, uuid(6e8ff6e5-05c9-42d3-853d-40523fd86a50)] +interface mozISandboxReportArray : nsISupports +{ + readonly attribute uint64_t begin; + readonly attribute uint64_t end; + // (aIndex >= begin && aIndex < end) must be true. + mozISandboxReport getElement(in uint64_t aIndex); +}; + +// A wrapper for the SandboxReporter; use the component/contract IDs +// below to access the SandboxReporter singleton. The component +// constructor will fail if called in a child process. +[scriptable, builtinclass, uuid(8535bdf7-6d9e-4853-acf9-a146449c4a3b)] +interface mozISandboxReporter : nsISupports +{ + mozISandboxReportArray snapshot(); +}; + +%{ C++ + +#define MOZ_SANDBOX_REPORTER_CID \ +{0x5118a6f9, 0x2493, 0x4f97, {0x95, 0x52, 0x62, 0x06, 0x63, 0xe0, 0x3c, 0xb3}} + +#define MOZ_SANDBOX_REPORTER_CONTRACTID \ + "@mozilla.org/sandbox/syscall-reporter;1" + +%} diff --git a/security/sandbox/linux/launch/LinuxCapabilities.cpp b/security/sandbox/linux/launch/LinuxCapabilities.cpp new file mode 100644 index 0000000000..6a1c73ed37 --- /dev/null +++ b/security/sandbox/linux/launch/LinuxCapabilities.cpp @@ -0,0 +1,26 @@ +/* -*- 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 "LinuxCapabilities.h" + +#include +#include + +namespace mozilla { + +bool LinuxCapabilities::GetCurrent() { + __user_cap_header_struct header = {_LINUX_CAPABILITY_VERSION_3, 0}; + return syscall(__NR_capget, &header, &mBits) == 0 && + header.version == _LINUX_CAPABILITY_VERSION_3; +} + +bool LinuxCapabilities::SetCurrentRaw() const { + __user_cap_header_struct header = {_LINUX_CAPABILITY_VERSION_3, 0}; + return syscall(__NR_capset, &header, &mBits) == 0 && + header.version == _LINUX_CAPABILITY_VERSION_3; +} + +} // namespace mozilla diff --git a/security/sandbox/linux/launch/LinuxCapabilities.h b/security/sandbox/linux/launch/LinuxCapabilities.h new file mode 100644 index 0000000000..c8db719811 --- /dev/null +++ b/security/sandbox/linux/launch/LinuxCapabilities.h @@ -0,0 +1,122 @@ +/* -*- 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_LinuxCapabilities_h +#define mozilla_LinuxCapabilities_h + +#include +#include + +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/PodOperations.h" + +// This class is a relatively simple interface to manipulating the +// capabilities of a Linux process/thread; see the capabilities(7) man +// page for background information. + +// Unfortunately, Android's kernel headers omit some definitions +// needed for the low-level capability interface. They're part of the +// stable syscall ABI, so it's safe to include them here. +#ifndef _LINUX_CAPABILITY_VERSION_3 +# define _LINUX_CAPABILITY_VERSION_3 0x20080522 +# define _LINUX_CAPABILITY_U32S_3 2 +#endif +#ifndef CAP_TO_INDEX +# define CAP_TO_INDEX(x) ((x) >> 5) +# define CAP_TO_MASK(x) (1 << ((x) & 31)) +#endif + +namespace mozilla { + +class LinuxCapabilities final { + public: + // A class to represent a bit within the capability sets as an lvalue. + class BitRef { + __u32& mWord; + __u32 mMask; + friend class LinuxCapabilities; + BitRef(__u32& aWord, uint32_t aMask) : mWord(aWord), mMask(aMask) {} + BitRef(const BitRef& aBit) = default; + + public: + MOZ_IMPLICIT operator bool() const { return mWord & mMask; } + BitRef& operator=(bool aSetTo) { + if (aSetTo) { + mWord |= mMask; + } else { + mWord &= mMask; + } + return *this; + } + }; + + // The default value is the empty set. + LinuxCapabilities() { PodArrayZero(mBits); } + + // Get the current thread's capability sets and assign them to this + // object. Returns whether it succeeded and sets errno on failure. + // Shouldn't fail unless the kernel is very old. + bool GetCurrent(); + + // Try to set the current thread's capability sets to those + // specified in this object. Returns whether it succeeded and sets + // errno on failure. + bool SetCurrentRaw() const; + + // The capability model requires that the permitted set always be a + // superset of the effective and inheritable sets. This method + // expands the permitted set as needed and then sets the current + // thread's capabilities, as described above. + bool SetCurrent() { + Normalize(); + return SetCurrentRaw(); + } + + void Normalize() { + for (size_t i = 0; i < _LINUX_CAPABILITY_U32S_3; ++i) { + mBits[i].permitted |= mBits[i].effective | mBits[i].inheritable; + } + } + + bool AnyEffective() const { + for (size_t i = 0; i < _LINUX_CAPABILITY_U32S_3; ++i) { + if (mBits[i].effective != 0) { + return true; + } + } + return false; + } + + // These three methods expose individual bits in the three + // capability sets as objects that can be used as bool lvalues. + // The argument is the capability number, as defined in + // the header. + BitRef Effective(unsigned aCap) { + return GenericBitRef(&__user_cap_data_struct::effective, aCap); + } + + BitRef Permitted(unsigned aCap) { + return GenericBitRef(&__user_cap_data_struct::permitted, aCap); + } + + BitRef Inheritable(unsigned aCap) { + return GenericBitRef(&__user_cap_data_struct::inheritable, aCap); + } + + private: + __user_cap_data_struct mBits[_LINUX_CAPABILITY_U32S_3]; + + BitRef GenericBitRef(__u32 __user_cap_data_struct::*aField, unsigned aCap) { + // Please don't pass untrusted data as the capability number. + MOZ_ASSERT(CAP_TO_INDEX(aCap) < _LINUX_CAPABILITY_U32S_3); + return BitRef(mBits[CAP_TO_INDEX(aCap)].*aField, CAP_TO_MASK(aCap)); + } +}; + +} // namespace mozilla + +#endif // mozilla_LinuxCapabilities_h diff --git a/security/sandbox/linux/launch/SandboxLaunch.cpp b/security/sandbox/linux/launch/SandboxLaunch.cpp new file mode 100644 index 0000000000..bec94f3c4c --- /dev/null +++ b/security/sandbox/linux/launch/SandboxLaunch.cpp @@ -0,0 +1,675 @@ +/* -*- 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 "SandboxLaunch.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "LinuxCapabilities.h" +#include "LinuxSched.h" +#include "SandboxChrootProto.h" +#include "SandboxInfo.h" +#include "SandboxLogging.h" +#include "base/eintr_wrapper.h" +#include "base/strings/safe_sprintf.h" +#include "mozilla/Array.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/Preferences.h" +#include "mozilla/SandboxReporter.h" +#include "mozilla/SandboxSettings.h" +#include "mozilla/Components.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/StaticPrefs_security.h" +#include "mozilla/Unused.h" +#include "mozilla/ipc/UtilityProcessSandboxing.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsIGfxInfo.h" +#include "nsString.h" +#include "nsThreadUtils.h" +#include "prenv.h" +#include "sandbox/linux/system_headers/linux_syscalls.h" + +#ifdef MOZ_X11 +# ifndef MOZ_WIDGET_GTK +# error "Unknown toolkit" +# endif +# include "mozilla/WidgetUtilsGtk.h" +# include +# include +# include "X11UndefineNone.h" +# include "gfxPlatform.h" +#endif + +namespace mozilla { + +// Returns true if graphics will work from a content process +// started in a new network namespace. Specifically, named +// Unix-domain sockets will work, but TCP/IP will not, even if it's a +// connection to localhost: the child process has its own private +// loopback interface. +// +// (Longer-term we intend to either proxy or remove X11 access from +// content processes, at which point this will stop being an issue.) +static bool IsGraphicsOkWithoutNetwork() { + // For X11, check whether the parent's connection is a Unix-domain + // socket. This is done instead of trying to parse the display name + // because an empty hostname (e.g., ":0") will fall back to TCP in + // case of failure to connect using Unix-domain sockets. +#ifdef MOZ_X11 + // First, ensure that the parent process's graphics are initialized. + DebugOnly gfxPlatform = gfxPlatform::GetPlatform(); + + const auto display = gdk_display_get_default(); + if (!display) { + // In this case, the browser is headless, but WebGL could still + // try to use X11. However, WebGL isn't supported with remote + // X11, and in any case these connections are made after sandbox + // startup (lazily when WebGL is used), so they aren't being done + // directly by the process anyway. (For local X11, they're + // brokered.) + MOZ_ASSERT(gfxPlatform->IsHeadless()); + return true; + } + if (mozilla::widget::GdkIsX11Display(display)) { + const int xSocketFd = ConnectionNumber(GDK_DISPLAY_XDISPLAY(display)); + if (NS_WARN_IF(xSocketFd < 0)) { + return false; + } + + int domain; + socklen_t optlen = static_cast(sizeof(domain)); + int rv = getsockopt(xSocketFd, SOL_SOCKET, SO_DOMAIN, &domain, &optlen); + if (NS_WARN_IF(rv != 0)) { + return false; + } + MOZ_RELEASE_ASSERT(static_cast(optlen) == sizeof(domain)); + if (domain != AF_LOCAL) { + return false; + } + // There's one more complication: Xorg listens on named sockets + // (actual filesystem nodes) as well as abstract addresses (opaque + // octet strings scoped to the network namespace; this is a Linux + // extension). + // + // Inside a container environment (e.g., when running as a Snap + // package), it's possible that only the abstract addresses are + // accessible. In that case, the display must be considered + // remote. See also bug 1450740. + // + // Unfortunately, the Xorg client libraries prefer the abstract + // addresses, so this isn't directly detectable by inspecting the + // parent process's socket. Instead, parse the DISPLAY env var + // (which was updated if necessary in nsAppRunner.cpp) to get the + // display number and construct the socket path, falling back to + // testing the directory in case that doesn't work. (See bug + // 1565972 and bug 1559368 for cases where we need to test the + // specific socket.) + const char* const displayStr = PR_GetEnv("DISPLAY"); + nsAutoCString socketPath("/tmp/.X11-unix"); + int accessFlags = X_OK; + int displayNum; + // sscanf ignores trailing text, so display names with a screen + // number (e.g., ":0.2") will parse correctly. + if (displayStr && (sscanf(displayStr, ":%d", &displayNum) == 1 || + sscanf(displayStr, "unix:%d", &displayNum) == 1)) { + socketPath.AppendPrintf("/X%d", displayNum); + accessFlags = R_OK | W_OK; + } + if (access(socketPath.get(), accessFlags) != 0) { + SANDBOX_LOG_ERRNO( + "%s is inaccessible; can't isolate network namespace in" + " content processes", + socketPath.get()); + return false; + } + } +#endif + + // Assume that other backends (e.g., Wayland) will not use the + // network namespace. + return true; +} + +bool HasAtiDrivers() { + nsCOMPtr gfxInfo = components::GfxInfo::Service(); + nsAutoString vendorID; + static const Array kMethods = { + &nsIGfxInfo::GetAdapterVendorID, + &nsIGfxInfo::GetAdapterVendorID2, + }; + for (const auto method : kMethods) { + if (NS_SUCCEEDED((gfxInfo->*method)(vendorID))) { + // This test is based on telemetry data. The proprietary ATI + // drivers seem to use this vendor string, including for some + // newer devices that have AMD branding in the device name, such + // as those using AMDGPU-PRO drivers. + // The open-source drivers integrated into Mesa appear to use + // the vendor ID "X.Org" instead. + if (vendorID.EqualsLiteral("ATI Technologies Inc.")) { + return true; + } + } + } + + return false; +} + +// Content processes may need direct access to SysV IPC in certain +// uncommon use cases. +static bool ContentNeedsSysVIPC() { + // The ALSA dmix plugin uses SysV semaphores and shared memory to + // coordinate software mixing. +#ifdef MOZ_ALSA + if (!StaticPrefs::media_cubeb_sandbox()) { + return true; + } +#endif + + if (!StaticPrefs::security_sandbox_content_headless_AtStartup()) { + // Bug 1438391: VirtualGL uses SysV shm for images and configuration. + if (PR_GetEnv("VGL_ISACTIVE") != nullptr) { + return true; + } + + // The fglrx (ATI Catalyst) GPU drivers use SysV IPC. + if (HasAtiDrivers()) { + return true; + } + } + + return false; +} + +static void PreloadSandboxLib(base::environment_map* aEnv) { + // Preload libmozsandbox.so so that sandbox-related interpositions + // can be defined there instead of in the executable. + // (This could be made conditional on intent to use sandboxing, but + // it's harmless for non-sandboxed processes.) + nsAutoCString preload; + // Prepend this, because people can and do preload libpthread. + // (See bug 1222500.) + preload.AssignLiteral("libmozsandbox.so"); + if (const char* oldPreload = PR_GetEnv("LD_PRELOAD")) { + // Doesn't matter if oldPreload is ""; extra separators are ignored. + preload.Append(' '); + preload.Append(oldPreload); + (*aEnv)["MOZ_ORIG_LD_PRELOAD"] = oldPreload; + } + MOZ_ASSERT(aEnv->count("LD_PRELOAD") == 0); + (*aEnv)["LD_PRELOAD"] = preload.get(); +} + +static void AttachSandboxReporter(base::file_handle_mapping_vector* aFdMap) { + int srcFd, dstFd; + SandboxReporter::Singleton()->GetClientFileDescriptorMapping(&srcFd, &dstFd); + aFdMap->push_back({srcFd, dstFd}); +} + +static int GetEffectiveSandboxLevel(GeckoProcessType aType, + ipc::SandboxingKind aKind) { + auto info = SandboxInfo::Get(); + switch (aType) { +#ifdef MOZ_ENABLE_FORKSERVER + // With this env MOZ_SANDBOXED will be set, and mozsandbox will + // be preloaded for the fork server. Sandboxed child processes + // rely on wrappers defined by mozsandbox to work properly. + case GeckoProcessType_ForkServer: + return 1; + break; +#endif + case GeckoProcessType_Content: + // GetEffectiveContentSandboxLevel is main-thread-only due to prefs. + MOZ_ASSERT(NS_IsMainThread()); + if (info.Test(SandboxInfo::kEnabledForContent)) { + return GetEffectiveContentSandboxLevel(); + } + return 0; + case GeckoProcessType_GMPlugin: + if (info.Test(SandboxInfo::kEnabledForMedia)) { + return 1; + } + return 0; + case GeckoProcessType_RDD: + return PR_GetEnv("MOZ_DISABLE_RDD_SANDBOX") == nullptr ? 1 : 0; + case GeckoProcessType_Socket: + // GetEffectiveSocketProcessSandboxLevel is main-thread-only due to prefs. + MOZ_ASSERT(NS_IsMainThread()); + return GetEffectiveSocketProcessSandboxLevel(); + case GeckoProcessType_Utility: + return IsUtilitySandboxEnabled(aKind); + default: + return 0; + } +} + +// static +void SandboxLaunch::Configure(GeckoProcessType aType, SandboxingKind aKind, + LaunchOptions* aOptions) { + MOZ_ASSERT(aOptions->fork_flags == 0 && !aOptions->sandbox_chroot); + auto info = SandboxInfo::Get(); + + // We won't try any kind of sandboxing without seccomp-bpf. + if (!info.Test(SandboxInfo::kHasSeccompBPF)) { + return; + } + + // Check prefs (and env vars) controlling sandbox use. + int level = GetEffectiveSandboxLevel(aType, aKind); + if (level == 0) { + return; + } + + // At this point, we know we'll be using sandboxing; generic + // sandboxing support goes here. The MOZ_SANDBOXED env var tells + // the child process whether this is the case. + aOptions->env_map["MOZ_SANDBOXED"] = "1"; + PreloadSandboxLib(&aOptions->env_map); + AttachSandboxReporter(&aOptions->fds_to_remap); + + bool canChroot = false; + int flags = 0; + + if (aType == GeckoProcessType_Content && level >= 1) { + static const bool needSysV = ContentNeedsSysVIPC(); + if (needSysV) { + // Tell the child process so it can adjust its seccomp-bpf + // policy. + aOptions->env_map["MOZ_SANDBOX_ALLOW_SYSV"] = "1"; + } else { + flags |= CLONE_NEWIPC; + } + + if (StaticPrefs::security_sandbox_content_headless_AtStartup()) { + aOptions->env_map["MOZ_HEADLESS"] = "1"; + } + } + + // Anything below this requires unprivileged user namespaces. + if (!info.Test(SandboxInfo::kHasUserNamespaces)) { + return; + } + + switch (aType) { + case GeckoProcessType_Socket: + if (level >= 1) { + canChroot = true; + flags |= CLONE_NEWIPC; + } + break; + case GeckoProcessType_GMPlugin: + case GeckoProcessType_RDD: + if (level >= 1) { + canChroot = true; + // Can't use CLONE_NEWIPC because of intel-media-driver. + flags |= CLONE_NEWNET; + } + break; + case GeckoProcessType_Content: + if (level >= 4) { + canChroot = true; + + // Unshare network namespace if allowed by graphics; see + // function definition above for details. (The display + // local-ness is cached because it won't change.) + static const bool canCloneNet = + StaticPrefs::security_sandbox_content_headless_AtStartup() || + (IsGraphicsOkWithoutNetwork() && + !PR_GetEnv("RENDERDOC_CAPTUREOPTS")); + + if (canCloneNet) { + flags |= CLONE_NEWNET; + } + } + // Hidden pref to allow testing user namespaces separately, even + // if there's nothing that would require them. + if (Preferences::GetBool("security.sandbox.content.force-namespace", + false)) { + flags |= CLONE_NEWUSER; + } + break; + default: + // Nothing yet. + break; + } + + if (canChroot || flags != 0) { + flags |= CLONE_NEWUSER; + } + + aOptions->env_map[kSandboxChrootEnvFlag] = std::to_string(canChroot ? 1 : 0); + + aOptions->sandbox_chroot = canChroot; + aOptions->fork_flags = flags; +} + +SandboxLaunch::SandboxLaunch() + : mFlags(0), mChrootServer(-1), mChrootClient(-1) {} + +SandboxLaunch::~SandboxLaunch() { + if (mChrootClient >= 0) { + close(mChrootClient); + } + if (mChrootServer >= 0) { + close(mChrootServer); + } +} + +bool SandboxLaunch::Prepare(LaunchOptions* aOptions) { + MOZ_ASSERT(mChrootClient < 0 && mChrootServer < 0); + + mFlags = aOptions->fork_flags; + + // Create the socket for communication between the child process and + // the chroot helper process. The client end is passed to the child + // via `fds_to_remap` and the server end is inherited and used in + // `StartChrootServer`. + if (aOptions->sandbox_chroot) { + int fds[2]; + int rv = socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, fds); + if (rv != 0) { + SANDBOX_LOG_ERRNO("socketpair"); + return false; + } + mChrootClient = fds[0]; + mChrootServer = fds[1]; + + aOptions->fds_to_remap.push_back({mChrootClient, kSandboxChrootClientFd}); + } + + return true; +} + +static void BlockAllSignals(sigset_t* aOldSigs) { + sigset_t allSigs; + int rv = sigfillset(&allSigs); + MOZ_RELEASE_ASSERT(rv == 0); + rv = pthread_sigmask(SIG_BLOCK, &allSigs, aOldSigs); + if (rv != 0) { + SANDBOX_LOG_WITH_ERROR(rv, "pthread_sigmask (block all)"); + MOZ_CRASH("pthread_sigmask"); + } +} + +static void RestoreSignals(const sigset_t* aOldSigs) { + // Assuming that pthread_sigmask is a thin layer over rt_sigprocmask + // and doesn't try to touch TLS, which may be in an "interesting" + // state right now: + int rv = pthread_sigmask(SIG_SETMASK, aOldSigs, nullptr); + if (rv != 0) { + SANDBOX_LOG_WITH_ERROR(rv, "pthread_sigmask (restore)"); + MOZ_CRASH("pthread_sigmask"); + } +} + +static bool IsSignalIgnored(int aSig) { + struct sigaction sa {}; + + if (sigaction(aSig, nullptr, &sa) != 0) { + if (errno != EINVAL) { + SANDBOX_LOG_ERRNO("sigaction(%d)", aSig); + } + return false; + } + return sa.sa_handler == SIG_IGN; +} + +static void ResetSignalHandlers() { + for (int signum = 1; signum <= SIGRTMAX; ++signum) { + if (IsSignalIgnored(signum)) { + continue; + } + if (signal(signum, SIG_DFL) == SIG_ERR) { + MOZ_DIAGNOSTIC_ASSERT(errno == EINVAL); + } + } +} + +namespace { + +// The libc clone() routine insists on calling a provided function on +// a new stack, even if the address space isn't shared and it would be +// safe to expose the underlying system call's fork()-like behavior. +// So, we work around this by longjmp()ing back onto the original stack; +// this technique is also used by Chromium. +// +// In theory, the clone syscall could be used directly if we ensure +// that functions like raise() are never used in the child, including +// by inherited signal handlers, but the longjmp approach isn't much +// extra code and avoids a class of potential bugs. +static int CloneCallee(void* aPtr) { + auto ctxPtr = reinterpret_cast(aPtr); + longjmp(*ctxPtr, 1); + MOZ_CRASH("unreachable"); + return 1; +} + +// According to the Chromium developers, builds with FORTIFY_SOURCE +// require that longjump move the stack pointer towards the root +// function of the call stack. Therefore, we must ensure that the +// clone callee stack is leafward of the stack pointer captured in +// setjmp() below by using this no-inline helper function. +// +// ASan apparently also causes problems, by the combination of +// allocating the large stack-allocated buffer outside of the actual +// stack and then assuming that longjmp is used only to unwind a +// stack, not switch stacks. +// +// Valgrind would disapprove of using clone() without CLONE_VM; +// Chromium uses the raw syscall as a workaround in that case, but +// we don't currently support sandboxing under valgrind. +MOZ_NEVER_INLINE MOZ_ASAN_IGNORE static pid_t DoClone(int aFlags, + jmp_buf* aCtx) { + static constexpr size_t kStackAlignment = 16; + uint8_t miniStack[4096] __attribute__((aligned(kStackAlignment))); +#ifdef __hppa__ + void* stackPtr = miniStack; +#else + void* stackPtr = ArrayEnd(miniStack); +#endif + return clone(CloneCallee, stackPtr, aFlags, aCtx); +} + +} // namespace + +// Similar to fork(), but allows passing flags to clone() and does not +// run pthread_atfork hooks. +static pid_t ForkWithFlags(int aFlags) { + // Don't allow flags that would share the address space, or + // require clone() arguments we're not passing: + static const int kBadFlags = CLONE_VM | CLONE_VFORK | CLONE_SETTLS | + CLONE_PARENT_SETTID | CLONE_CHILD_SETTID | + CLONE_CHILD_CLEARTID; + MOZ_RELEASE_ASSERT((aFlags & kBadFlags) == 0); + + // Block signals due to small stack in DoClone. + sigset_t oldSigs; + BlockAllSignals(&oldSigs); + + int ret = 0; + jmp_buf ctx; + if (setjmp(ctx) == 0) { + // In the parent and just called setjmp: + ret = DoClone(aFlags | SIGCHLD, &ctx); + } + RestoreSignals(&oldSigs); + // In the child and have longjmp'ed: + return ret; +} + +static bool WriteStringToFile(const char* aPath, const char* aStr, + const size_t aLen) { + int fd = open(aPath, O_WRONLY); + if (fd < 0) { + return false; + } + ssize_t written = write(fd, aStr, aLen); + if (close(fd) != 0 || written != ssize_t(aLen)) { + return false; + } + return true; +} + +// This function sets up uid/gid mappings that preserve the +// process's previous ids. Mapping the uid/gid to something is +// necessary in order to nest user namespaces (not currently being +// used, but could be useful), and leaving the ids unchanged is +// likely to minimize unexpected side-effects. +static void ConfigureUserNamespace(uid_t uid, gid_t gid) { + using base::strings::SafeSPrintf; + char buf[sizeof("18446744073709551615 18446744073709551615 1")]; + size_t len; + + len = static_cast(SafeSPrintf(buf, "%d %d 1", uid, uid)); + MOZ_RELEASE_ASSERT(len < sizeof(buf)); + if (!WriteStringToFile("/proc/self/uid_map", buf, len)) { + MOZ_CRASH("Failed to write /proc/self/uid_map"); + } + + // In recent kernels (3.19, 3.18.2, 3.17.8), for security reasons, + // establishing gid mappings will fail unless the process first + // revokes its ability to call setgroups() by using a /proc node + // added in the same set of patches. + Unused << WriteStringToFile("/proc/self/setgroups", "deny", 4); + + len = static_cast(SafeSPrintf(buf, "%d %d 1", gid, gid)); + MOZ_RELEASE_ASSERT(len < sizeof(buf)); + if (!WriteStringToFile("/proc/self/gid_map", buf, len)) { + MOZ_CRASH("Failed to write /proc/self/gid_map"); + } +} + +static void DropAllCaps() { + if (!LinuxCapabilities().SetCurrent()) { + SANDBOX_LOG_ERRNO("capset (drop all)"); + } +} + +pid_t SandboxLaunch::Fork() { + if (mFlags == 0) { + MOZ_ASSERT(mChrootServer < 0); + return fork(); + } + + uid_t uid = getuid(); + gid_t gid = getgid(); + + // Block signals so that the handlers can be safely reset in the + // child process without races, and so that repeated SIGPROF from + // the profiler won't prevent clone() from making progress. (The + // profiler uses pthread_atfork to do that, but ForkWithFlags + // can't run atfork hooks.) + sigset_t oldSigs; + BlockAllSignals(&oldSigs); + pid_t pid = ForkWithFlags(mFlags); + if (pid != 0) { + RestoreSignals(&oldSigs); + return pid; + } + + // WARNING: all code from this point on (and in StartChrootServer) + // must be async signal safe. In particular, it cannot do anything + // that could allocate heap memory or use mutexes. + prctl(PR_SET_NAME, "Sandbox Forked"); + + // Clear signal handlers in the child, under the assumption that any + // actions they would take (running the crash reporter, manipulating + // the Gecko profile, etc.) wouldn't work correctly in the child. + ResetSignalHandlers(); + RestoreSignals(&oldSigs); + ConfigureUserNamespace(uid, gid); + + if (mChrootServer >= 0) { + StartChrootServer(); + // Don't close the client fd when this object is destroyed. At + // this point we're in the child process proper, so it's "owned" + // by the FileDescriptorShuffle / CloseSuperfluous code (i.e., + // that's what will consume it and close it). + mChrootClient = -1; + } + + // execve() will drop capabilities, but the fork server case doesn't + // exec so we need to do this directly. (Also, it's a good idea to + // follow the principle of least privilege even when not strictly + // necessary.) + // + // Note that, while capabilities within an unprivileged user + // namespace are constrained in theory, in practice they expose a + // lot of attack surface and there have been exploitable kernel bugs + // related to that in the past, so we really want to drop them + // before doing anything that needs sandboxing. + DropAllCaps(); + return 0; +} + +void SandboxLaunch::StartChrootServer() { + // Run the rest of this function in a separate process that can + // chroot() on behalf of this process after it's sandboxed. + pid_t pid = ForkWithFlags(CLONE_FS); + if (pid < 0) { + MOZ_CRASH("failed to clone chroot helper process"); + } + if (pid > 0) { + return; + } + prctl(PR_SET_NAME, "Chroot Helper"); + + LinuxCapabilities caps; + caps.Effective(CAP_SYS_CHROOT) = true; + if (!caps.SetCurrent()) { + SANDBOX_LOG_ERRNO("capset (chroot helper)"); + MOZ_DIAGNOSTIC_ASSERT(false); + } + + base::CloseSuperfluousFds(this, [](void* aCtx, int aFd) { + return aFd == static_cast(aCtx)->mChrootServer; + }); + + char msg; + ssize_t msgLen = HANDLE_EINTR(read(mChrootServer, &msg, 1)); + if (msgLen == 0) { + // Process exited before chrooting (or chose not to chroot?). + _exit(0); + } + MOZ_RELEASE_ASSERT(msgLen == 1); + MOZ_RELEASE_ASSERT(msg == kSandboxChrootRequest); + + // This chroots both processes to this process's procfs fdinfo + // directory, which becomes empty and unlinked when this process + // exits at the end of this function, and which is always + // unwriteable. + int rv = chroot("/proc/self/fdinfo"); + MOZ_RELEASE_ASSERT(rv == 0); + + // Drop CAP_SYS_CHROOT ASAP. This must happen before responding; + // the main child won't be able to waitpid(), so it could start + // handling hostile content before this process finishes exiting. + DropAllCaps(); + + // The working directory still grant access to the real filesystem; + // remove that. (Note: if the process can obtain directory fds, for + // example via SandboxBroker, it must be blocked from using fchdir.) + rv = chdir("/"); + MOZ_RELEASE_ASSERT(rv == 0); + + msg = kSandboxChrootResponse; + msgLen = HANDLE_EINTR(write(mChrootServer, &msg, 1)); + MOZ_RELEASE_ASSERT(msgLen == 1); + _exit(0); +} + +} // namespace mozilla diff --git a/security/sandbox/linux/launch/SandboxLaunch.h b/security/sandbox/linux/launch/SandboxLaunch.h new file mode 100644 index 0000000000..988709dcdb --- /dev/null +++ b/security/sandbox/linux/launch/SandboxLaunch.h @@ -0,0 +1,71 @@ +/* -*- 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_SandboxLaunch_h +#define mozilla_SandboxLaunch_h + +#include "base/process_util.h" +#include "mozilla/ipc/UtilityProcessSandboxing.h" +#include "nsXULAppAPI.h" +#include + +namespace mozilla { + +class SandboxLaunch final { + public: + SandboxLaunch(); + ~SandboxLaunch(); + + SandboxLaunch(const SandboxLaunch&) = delete; + SandboxLaunch& operator=(const SandboxLaunch&) = delete; + + using LaunchOptions = base::LaunchOptions; + using SandboxingKind = ipc::SandboxingKind; + + // Decide what sandboxing features will be used for a process, and + // modify `*aOptions` accordingly. This does not allocate fds or + // other OS resources (other than memory for strings). + // + // This is meant to be called in the parent process (even if the + // fork server will be used), and if `aType` is Content then it must + // be called on the main thread in order to access prefs. + static void Configure(GeckoProcessType aType, SandboxingKind aKind, + LaunchOptions* aOptions); + + // Finish setting up for process launch, based on the information + // from `Configure(...)`. Called in the process that will do the + // launch (fork server if applicable, otherwise parent), and before + // calling `FileDescriptorShuffle::Init`. + // + // This can allocate fds (owned by `*this`) and modify + // `aOptions->fds_to_remap`, but does not access the + // environment-related fields of `*aOptions`. + bool Prepare(LaunchOptions* aOptions); + + // Launch the child process, similarly to `::fork()`; called after + // `Configure` and `Prepare`. + // + // If launch-time sandboxing features are used, `pthread_atfork` + // hooks are not currently supported in that case, and signal + // handlers are reset in the child process. If sandboxing is not + // used, this is equivalent to `::fork()`. + pid_t Fork(); + + private: + int mFlags; + int mChrootServer; + int mChrootClient; + + void StartChrootServer(); +}; + +// This doesn't really belong in this header but it's used in both +// SandboxLaunch and SandboxBrokerPolicyFactory. +bool HasAtiDrivers(); + +} // namespace mozilla + +#endif // mozilla_SandboxLaunch_h diff --git a/security/sandbox/linux/launch/moz.build b/security/sandbox/linux/launch/moz.build new file mode 100644 index 0000000000..09f483f519 --- /dev/null +++ b/security/sandbox/linux/launch/moz.build @@ -0,0 +1,33 @@ +# -*- 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 += [ + "SandboxLaunch.h", +] + +UNIFIED_SOURCES += [ + "LinuxCapabilities.cpp", + "SandboxLaunch.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +LOCAL_INCLUDES += [ + # Need this for safe_sprintf.h used by SandboxLogging.h, + # but it has to be after ipc/chromium/src. + "/security/sandbox/chromium", + "/security/sandbox/linux", +] + +USE_LIBS += [ + "mozsandbox", +] + +# For the X11 socket domain inspection in SandboxLaunch: +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] + +FINAL_LIBRARY = "xul" diff --git a/security/sandbox/linux/moz.build b/security/sandbox/linux/moz.build new file mode 100644 index 0000000000..cbb99e514c --- /dev/null +++ b/security/sandbox/linux/moz.build @@ -0,0 +1,139 @@ +# -*- Mode: python; 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/. + +SharedLibrary("mozsandbox") + +# Depend on mozglue if and only if it's a shared library; +# this needs to match mozglue/build/moz.build: +if CONFIG["OS_TARGET"] == "Android": + USE_LIBS += [ + "mozglue", + ] + +USE_LIBS += [ + # For PR_GetEnv + "nspr", +] + +EXPORTS.mozilla += [ + "Sandbox.h", + "SandboxInfo.h", +] + +UNIFIED_SOURCES += [ + "../chromium-shim/base/logging.cpp", + "../chromium-shim/base/threading/platform_thread_linux.cpp", + "../chromium/base/at_exit.cc", + "../chromium/base/callback_internal.cc", + "../chromium/base/lazy_instance_helpers.cc", + "../chromium/base/location.cc", + "../chromium/base/memory/ref_counted.cc", + "../chromium/base/posix/can_lower_nice_to.cc", + "../chromium/base/posix/safe_strerror.cc", + "../chromium/base/strings/string16.cc", + "../chromium/base/strings/string_number_conversions.cc", + "../chromium/base/strings/string_piece.cc", + "../chromium/base/strings/string_util.cc", + "../chromium/base/strings/string_util_constants.cc", + "../chromium/base/strings/stringprintf.cc", + "../chromium/base/strings/utf_string_conversion_utils.cc", + "../chromium/base/strings/utf_string_conversions.cc", + "../chromium/base/synchronization/condition_variable_posix.cc", + "../chromium/base/synchronization/lock.cc", + "../chromium/base/synchronization/lock_impl_posix.cc", + "../chromium/base/synchronization/waitable_event_posix.cc", + "../chromium/base/threading/platform_thread.cc", + "../chromium/base/threading/platform_thread_internal_posix.cc", + "../chromium/base/threading/platform_thread_posix.cc", + "../chromium/base/threading/thread_collision_warner.cc", + "../chromium/base/threading/thread_id_name_manager.cc", + "../chromium/base/threading/thread_local_storage.cc", + "../chromium/base/threading/thread_local_storage_posix.cc", + "../chromium/base/threading/thread_restrictions.cc", + "../chromium/base/time/time.cc", + "../chromium/base/time/time_exploded_posix.cc", + "../chromium/base/time/time_now_posix.cc", + "../chromium/sandbox/linux/bpf_dsl/bpf_dsl.cc", + "../chromium/sandbox/linux/bpf_dsl/codegen.cc", + "../chromium/sandbox/linux/bpf_dsl/dump_bpf.cc", + "../chromium/sandbox/linux/bpf_dsl/policy.cc", + "../chromium/sandbox/linux/bpf_dsl/policy_compiler.cc", + "../chromium/sandbox/linux/bpf_dsl/syscall_set.cc", + "../chromium/sandbox/linux/seccomp-bpf/die.cc", + "../chromium/sandbox/linux/seccomp-bpf/syscall.cc", + "/ipc/glue/UtilityProcessSandboxing.cpp", + "broker/SandboxBrokerCommon.cpp", + "Sandbox.cpp", + "SandboxBrokerClient.cpp", + "SandboxFilter.cpp", + "SandboxFilterUtil.cpp", + "SandboxHooks.cpp", + "SandboxInfo.cpp", + "SandboxLogging.cpp", + "SandboxOpenedFiles.cpp", + "SandboxReporterClient.cpp", +] + +SOURCES += [ + "../chromium/base/strings/safe_sprintf.cc", + "../chromium/base/third_party/icu/icu_utf.cc", + "../chromium/sandbox/linux/seccomp-bpf/trap.cc", + "../chromium/sandbox/linux/services/syscall_wrappers.cc", +] + +# This copy of SafeSPrintf doesn't need to avoid the Chromium logging +# dependency like the one in libxul does, but this way the behavior is +# consistent. See also the comment in SandboxLogging.h. +SOURCES["../chromium/base/strings/safe_sprintf.cc"].flags += ["-DNDEBUG"] + +if CONFIG["CC_TYPE"] in ("clang", "gcc"): + # Keep clang from warning about intentional 'switch' fallthrough in icu_utf.cc: + SOURCES["../chromium/base/third_party/icu/icu_utf.cc"].flags += [ + "-Wno-implicit-fallthrough" + ] + SOURCES["../chromium/sandbox/linux/seccomp-bpf/trap.cc"].flags += [ + "-Wno-unreachable-code-return" + ] + +if CONFIG["CC_TYPE"] in ("clang", "gcc"): + CXXFLAGS += ["-Wno-error=stack-protector"] + SOURCES["../chromium/sandbox/linux/services/syscall_wrappers.cc"].flags += [ + "-Wno-empty-body", + ] + +# gcc lto likes to put the top level asm in syscall.cc in a different partition +# from the function using it which breaks the build. Work around that by +# forcing there to be only one partition. +for f in CONFIG["OS_CXXFLAGS"]: + if f.startswith("-flto") and CONFIG["CC_TYPE"] != "clang": + LDFLAGS += ["--param lto-partitions=1"] + +DEFINES["NS_NO_XPCOM"] = True +DisableStlWrapping() + +LOCAL_INCLUDES += ["/security/sandbox/linux"] +LOCAL_INCLUDES += ["/security/sandbox/chromium-shim"] +LOCAL_INCLUDES += ["/security/sandbox/chromium"] +LOCAL_INCLUDES += ["/nsprpub"] + + +if CONFIG["OS_TARGET"] != "Android": + # Needed for clock_gettime with glibc < 2.17: + OS_LIBS += [ + "rt", + ] + +DIRS += [ + "broker", + "glue", + "interfaces", + "launch", + "reporter", +] + +TEST_DIRS += [ + "gtest", +] diff --git a/security/sandbox/linux/reporter/SandboxReporter.cpp b/security/sandbox/linux/reporter/SandboxReporter.cpp new file mode 100644 index 0000000000..a7c71cd5c9 --- /dev/null +++ b/security/sandbox/linux/reporter/SandboxReporter.cpp @@ -0,0 +1,299 @@ +/* -*- 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 "SandboxReporter.h" +#include "SandboxLogging.h" + +#include +#include +#include +#include +#include // for clockid_t + +#include "GeckoProfiler.h" +#include "mozilla/Assertions.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/PodOperations.h" +#include "nsThreadUtils.h" +#include "mozilla/Telemetry.h" +#include "sandbox/linux/system_headers/linux_syscalls.h" + +// Distinguish architectures for the telemetry key. +#if defined(__i386__) +# define SANDBOX_ARCH_NAME "x86" +#elif defined(__x86_64__) +# define SANDBOX_ARCH_NAME "amd64" +#elif defined(__arm__) +# define SANDBOX_ARCH_NAME "arm" +#elif defined(__aarch64__) +# define SANDBOX_ARCH_NAME "arm64" +#else +# error "unrecognized architecture" +#endif + +namespace mozilla { + +StaticAutoPtr SandboxReporter::sSingleton; + +SandboxReporter::SandboxReporter() + : mClientFd(-1), + mServerFd(-1), + mMutex("SandboxReporter"), + mBuffer(MakeUnique(kSandboxReporterBufferSize)), + mCount(0) {} + +bool SandboxReporter::Init() { + int fds[2]; + + if (0 != socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, fds)) { + SANDBOX_LOG_ERRNO("SandboxReporter: socketpair failed"); + return false; + } + mClientFd = fds[0]; + mServerFd = fds[1]; + + if (!PlatformThread::Create(0, this, &mThread)) { + SANDBOX_LOG_ERRNO("SandboxReporter: thread creation failed"); + close(mClientFd); + close(mServerFd); + mClientFd = mServerFd = -1; + return false; + } + + return true; +} + +SandboxReporter::~SandboxReporter() { + if (mServerFd < 0) { + return; + } + shutdown(mServerFd, SHUT_RD); + PlatformThread::Join(mThread); + close(mServerFd); + close(mClientFd); +} + +/* static */ +SandboxReporter* SandboxReporter::Singleton() { + static StaticMutex sMutex MOZ_UNANNOTATED; + StaticMutexAutoLock lock(sMutex); + + if (sSingleton == nullptr) { + sSingleton = new SandboxReporter(); + if (!sSingleton->Init()) { + // If socketpair or thread creation failed, trying to continue + // with child process creation is unlikely to succeed; crash + // instead of trying to handle that case. + MOZ_CRASH("SandboxRepoter::Singleton: initialization failed"); + } + // ClearOnShutdown must be called on the main thread and will + // destroy the object on the main thread. That *should* be safe; + // the destructor will shut down the reporter's socket reader + // thread before freeing anything, IPC should already be shut down + // by that point (so it won't race by calling Singleton()), all + // non-main XPCOM threads will also be shut down, and currently + // the only other user is the main-thread-only Troubleshoot.sys.mjs. + NS_DispatchToMainThread(NS_NewRunnableFunction( + "SandboxReporter::Singleton", [] { ClearOnShutdown(&sSingleton); })); + } + return sSingleton.get(); +} + +void SandboxReporter::GetClientFileDescriptorMapping(int* aSrcFd, + int* aDstFd) const { + MOZ_ASSERT(mClientFd >= 0); + *aSrcFd = mClientFd; + *aDstFd = kSandboxReporterFileDesc; +} + +// This function is mentioned in Histograms.json; keep that in mind if +// it's renamed or moved to a different file. +static void SubmitToTelemetry(const SandboxReport& aReport) { + nsAutoCString key; + // The key contains the process type, something that uniquely + // identifies the syscall, and in some cases arguments (see below + // for details). Arbitrary formatting choice: fields in the key are + // separated by ':', except that (arch, syscall#) pairs are + // separated by '/'. + // + // Examples: + // * "content:x86/64" (bug 1285768) + // * "content:x86_64/110" (bug 1285768) + // * "gmp:madvise:8" (bug 1303813) + // * "content:clock_gettime:4" (bug 1334687) + + switch (aReport.mProcType) { + case SandboxReport::ProcType::CONTENT: + key.AppendLiteral("content"); + break; + case SandboxReport::ProcType::FILE: + key.AppendLiteral("file"); + break; + case SandboxReport::ProcType::MEDIA_PLUGIN: + key.AppendLiteral("gmp"); + break; + case SandboxReport::ProcType::RDD: + key.AppendLiteral("rdd"); + break; + case SandboxReport::ProcType::SOCKET_PROCESS: + key.AppendLiteral("socket"); + break; + case SandboxReport::ProcType::UTILITY: + key.AppendLiteral("utility"); + break; + default: + MOZ_ASSERT(false); + } + key.Append(':'); + + switch (aReport.mSyscall) { + // Syscalls that are filtered by arguments in one or more of the + // policies in SandboxFilter.cpp should generally have those + // arguments included here, but don't include irrelevant + // information that would cause large numbers of distinct keys for + // the same issue -- for example, pids or pointers. When in + // doubt, include arguments only if they would typically be + // constants (or asm immediates) in the code making the syscall. + // + // Also, keep in mind that this is opt-out data collection and + // privacy is critical. While it's unlikely that information in + // the register values alone could personally identify a user + // (see also crash reports, where register contents are public), + // and the guidelines in the previous paragraph should rule out + // any value that's capable of holding PII, please be careful. + // + // When making changes here, please consult with a data steward + // (https://wiki.mozilla.org/Firefox/Data_Collection) and ask for + // a review if you are unsure about anything. + + // This macro includes one argument as a decimal number; it should + // be enough for most cases. +#define ARG_DECIMAL(name, idx) \ + case __NR_##name: \ + key.AppendLiteral(#name ":"); \ + key.AppendInt(aReport.mArgs[idx]); \ + break + + // This may be more convenient if the argument is a set of bit flags. +#define ARG_HEX(name, idx) \ + case __NR_##name: \ + key.AppendLiteral(#name ":0x"); \ + key.AppendInt(aReport.mArgs[idx], 16); \ + break + + // clockid_t is annoying: there are a small set of fixed timers, + // but it can also encode a pid/tid (or a fd for a hardware clock + // device); in this case the value is negative. +#define ARG_CLOCKID(name, idx) \ + case __NR_##name: \ + key.AppendLiteral(#name ":"); \ + if (static_cast(aReport.mArgs[idx]) < 0) { \ + key.AppendLiteral("dynamic"); \ + } else { \ + key.AppendInt(aReport.mArgs[idx]); \ + } \ + break + + // The syscalls handled specially: + + ARG_HEX(clone, 0); // flags + ARG_DECIMAL(prctl, 0); // option + ARG_HEX(ioctl, 1); // request + ARG_DECIMAL(fcntl, 1); // cmd + ARG_DECIMAL(madvise, 2); // advice + ARG_CLOCKID(clock_gettime, 0); // clk_id + +#ifdef __NR_socketcall + ARG_DECIMAL(socketcall, 0); // call +#endif +#ifdef __NR_ipc + ARG_DECIMAL(ipc, 0); // call +#endif + +#undef ARG_DECIMAL +#undef ARG_HEX +#undef ARG_CLOCKID + + default: + // Otherwise just use the number, with the arch name to disambiguate. + key.Append(SANDBOX_ARCH_NAME "/"); + key.AppendInt(aReport.mSyscall); + } + + Telemetry::Accumulate(Telemetry::SANDBOX_REJECTED_SYSCALLS, key); +} + +void SandboxReporter::AddOne(const SandboxReport& aReport) { + SubmitToTelemetry(aReport); + + MutexAutoLock lock(mMutex); + mBuffer[mCount % kSandboxReporterBufferSize] = aReport; + ++mCount; +} + +void SandboxReporter::ThreadMain(void) { + // Create a nsThread wrapper for the current platform thread, and register it + // with the thread manager. + (void)NS_GetCurrentThread(); + + PlatformThread::SetName("SandboxReporter"); + AUTO_PROFILER_REGISTER_THREAD("SandboxReporter"); + + for (;;) { + SandboxReport rep; + struct iovec iov; + struct msghdr msg; + + iov.iov_base = &rep; + iov.iov_len = sizeof(rep); + PodZero(&msg); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + const auto recvd = recvmsg(mServerFd, &msg, 0); + if (recvd < 0) { + if (errno == EINTR) { + continue; + } + SANDBOX_LOG_ERRNO("SandboxReporter: recvmsg"); + } + if (recvd <= 0) { + break; + } + + if (static_cast(recvd) < sizeof(rep)) { + SANDBOX_LOG("SandboxReporter: packet too short (%d < %d)", recvd, + sizeof(rep)); + continue; + } + if (msg.msg_flags & MSG_TRUNC) { + SANDBOX_LOG("SandboxReporter: packet too long"); + continue; + } + + AddOne(rep); + } +} + +SandboxReporter::Snapshot SandboxReporter::GetSnapshot() { + Snapshot snapshot; + MutexAutoLock lock(mMutex); + + const uint64_t bufSize = static_cast(kSandboxReporterBufferSize); + const uint64_t start = std::max(mCount, bufSize) - bufSize; + snapshot.mOffset = start; + snapshot.mReports.Clear(); + snapshot.mReports.SetCapacity(mCount - start); + for (size_t i = start; i < mCount; ++i) { + const SandboxReport* rep = &mBuffer[i % kSandboxReporterBufferSize]; + MOZ_ASSERT(rep->IsValid()); + snapshot.mReports.AppendElement(*rep); + } + return snapshot; +} + +} // namespace mozilla diff --git a/security/sandbox/linux/reporter/SandboxReporter.h b/security/sandbox/linux/reporter/SandboxReporter.h new file mode 100644 index 0000000000..0969111c9c --- /dev/null +++ b/security/sandbox/linux/reporter/SandboxReporter.h @@ -0,0 +1,86 @@ +/* -*- 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_SandboxReporter_h +#define mozilla_SandboxReporter_h + +#include "SandboxReporterCommon.h" + +#include "base/platform_thread.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/Mutex.h" +#include "mozilla/Types.h" +#include "mozilla/UniquePtr.h" +#include "nsTArray.h" + +namespace mozilla { + +// This object collects the SandboxReport messages from all of the +// child processes, submits them to Telemetry, and maintains a ring +// buffer of the last kSandboxReporterBufferSize reports. +class SandboxReporter final : public PlatformThread::Delegate { + public: + // For normal use, don't construct this directly; use the + // Singleton() method. + // + // For unit testing, use this constructor followed by the Init + // method; the object isn't usable unless Init returns true. + explicit SandboxReporter(); + ~SandboxReporter(); + + // See above; this method is not thread-safe. + bool Init(); + + // Used in GeckoChildProcessHost to connect the child process's + // client to this report collector. + void GetClientFileDescriptorMapping(int* aSrcFd, int* aDstFd) const; + + // A snapshot of the report ring buffer; element 0 of `mReports` is + // the `mOffset`th report to be received, and so on. + struct Snapshot { + // The buffer has to fit in memory, but the total number of + // reports received in the session can increase without bound and + // could potentially overflow a uint32_t, so this is 64-bit. + // (It's exposed to JS as a 53-bit int, effectively, but that + // should also be large enough.) + uint64_t mOffset; + nsTArray mReports; + }; + + // Read the ring buffer contents; this method is thread-safe. + Snapshot GetSnapshot(); + + // Gets or creates the singleton report collector. Crashes if + // initialization fails (if a socketpair and/or thread can't be + // created, there was almost certainly about to be a crash anyway). + // Thread-safe as long as the pointer isn't used during/after XPCOM + // shutdown. + static SandboxReporter* Singleton(); + + private: + // These are constant over the life of the object: + int mClientFd; + int mServerFd; + PlatformThreadHandle mThread; + + Mutex mMutex MOZ_UNANNOTATED; + // These are protected by mMutex: + UniquePtr mBuffer; + uint64_t mCount; + + static StaticAutoPtr sSingleton; + + void ThreadMain(void) override; + void AddOne(const SandboxReport& aReport); +}; + +// This is a constant so the % operations can be optimized. This is +// exposed in the header so that unit tests can see it. +static const size_t kSandboxReporterBufferSize = 32; + +} // namespace mozilla + +#endif // mozilla_SandboxReporter_h diff --git a/security/sandbox/linux/reporter/SandboxReporterCommon.h b/security/sandbox/linux/reporter/SandboxReporterCommon.h new file mode 100644 index 0000000000..9bfb40bb98 --- /dev/null +++ b/security/sandbox/linux/reporter/SandboxReporterCommon.h @@ -0,0 +1,66 @@ +/* -*- 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_SandboxReporterCommon_h +#define mozilla_SandboxReporterCommon_h + +#include "mozilla/IntegerTypeTraits.h" +#include "mozilla/Types.h" + +#include + +// Note: this is also used in libmozsandbox, so dependencies on +// symbols from libxul probably won't work. + +namespace mozilla { +static const size_t kSandboxSyscallArguments = 6; +// fds 0-2: stdio; fd 3: IPC; fd 4: crash reporter. (The IPC child +// process launching code will check that we don't try to use the same +// fd twice.) +static const int kSandboxReporterFileDesc = 5; + +// This struct represents a system call that was rejected by a +// seccomp-bpf policy. +struct SandboxReport { + // In the future this may include finer distinctions than + // GeckoProcessType -- e.g., whether a content process can load + // file:/// URLs, or if it's reserved for content with certain + // user-granted permissions. + enum class ProcType : uint8_t { + CONTENT, + FILE, + MEDIA_PLUGIN, + RDD, + SOCKET_PROCESS, + UTILITY, + }; + + // The syscall number and arguments are usually `unsigned long`, but + // that causes ambiguous overload errors with nsACString::AppendInt. + using ULong = UnsignedStdintTypeForSize::Type; + + // This time uses CLOCK_MONOTONIC_COARSE. Displaying or reporting + // it should usually be done relative to the current value of that + // clock (or the time at some other event of interest, like a + // subsequent crash). + struct timespec mTime; + + // The pid/tid values, like every other field in this struct, aren't + // authenticated and a compromised process could send anything, so + // use the values with caution. + pid_t mPid; + pid_t mTid; + ProcType mProcType; + ULong mSyscall; + ULong mArgs[kSandboxSyscallArguments]; + + SandboxReport() : mPid(0) {} + bool IsValid() const { return mPid > 0; } +}; + +} // namespace mozilla + +#endif // mozilla_SandboxReporterCommon_h diff --git a/security/sandbox/linux/reporter/SandboxReporterWrappers.cpp b/security/sandbox/linux/reporter/SandboxReporterWrappers.cpp new file mode 100644 index 0000000000..755e8e858d --- /dev/null +++ b/security/sandbox/linux/reporter/SandboxReporterWrappers.cpp @@ -0,0 +1,199 @@ +/* -*- 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 "mozISandboxReporter.h" +#include "SandboxReporter.h" + +#include + +#include "mozilla/Assertions.h" +#include "mozilla/Components.h" +#include "nsCOMPtr.h" +#include "nsPrintfCString.h" +#include "nsTArray.h" +#include "nsXULAppAPI.h" + +using namespace mozilla; + +namespace mozilla { + +class SandboxReportWrapper final : public mozISandboxReport { + public: + NS_DECL_ISUPPORTS + NS_DECL_MOZISANDBOXREPORT + + explicit SandboxReportWrapper(const SandboxReport& aReport) + : mReport(aReport) {} + + private: + ~SandboxReportWrapper() = default; + SandboxReport mReport; +}; + +NS_IMPL_ISUPPORTS(SandboxReportWrapper, mozISandboxReport) + +/* readonly attribute uint64_t msecAgo; */ +NS_IMETHODIMP SandboxReportWrapper::GetMsecAgo(uint64_t* aMsec) { + struct timespec then = mReport.mTime, now = {0, 0}; + clock_gettime(CLOCK_MONOTONIC_COARSE, &now); + + const uint64_t now_msec = uint64_t(now.tv_sec) * 1000 + now.tv_nsec / 1000000; + const uint64_t then_msec = + uint64_t(then.tv_sec) * 1000 + then.tv_nsec / 1000000; + MOZ_DIAGNOSTIC_ASSERT(now_msec >= then_msec); + if (now_msec >= then_msec) { + *aMsec = now_msec - then_msec; + } else { + *aMsec = 0; + } + return NS_OK; +} + +/* readonly attribute int32_t pid; */ +NS_IMETHODIMP SandboxReportWrapper::GetPid(int32_t* aPid) { + *aPid = mReport.mPid; + return NS_OK; +} + +/* readonly attribute int32_t tid; */ +NS_IMETHODIMP SandboxReportWrapper::GetTid(int32_t* aTid) { + *aTid = mReport.mTid; + return NS_OK; +} + +/* readonly attribute ACString procType; */ +NS_IMETHODIMP SandboxReportWrapper::GetProcType(nsACString& aProcType) { + switch (mReport.mProcType) { + case SandboxReport::ProcType::CONTENT: + aProcType.AssignLiteral("content"); + return NS_OK; + case SandboxReport::ProcType::FILE: + aProcType.AssignLiteral("file"); + return NS_OK; + case SandboxReport::ProcType::MEDIA_PLUGIN: + aProcType.AssignLiteral("mediaPlugin"); + return NS_OK; + case SandboxReport::ProcType::RDD: + aProcType.AssignLiteral("dataDecoder"); + return NS_OK; + case SandboxReport::ProcType::SOCKET_PROCESS: + aProcType.AssignLiteral("socketProcess"); + return NS_OK; + case SandboxReport::ProcType::UTILITY: + aProcType.AssignLiteral("utility"); + return NS_OK; + default: + MOZ_ASSERT(false); + return NS_ERROR_UNEXPECTED; + } +} + +/* readonly attribute uint32_t syscall; */ +NS_IMETHODIMP SandboxReportWrapper::GetSyscall(uint32_t* aSyscall) { + *aSyscall = static_cast(mReport.mSyscall); + MOZ_ASSERT(static_cast(*aSyscall) == mReport.mSyscall); + return NS_OK; +} + +/* readonly attribute uint32_t numArgs; */ +NS_IMETHODIMP SandboxReportWrapper::GetNumArgs(uint32_t* aNumArgs) { + *aNumArgs = static_cast(kSandboxSyscallArguments); + return NS_OK; +} + +/* ACString getArg (in uint32_t aIndex); */ +NS_IMETHODIMP SandboxReportWrapper::GetArg(uint32_t aIndex, + nsACString& aRetval) { + if (aIndex >= kSandboxSyscallArguments) { + return NS_ERROR_INVALID_ARG; + } + const auto arg = mReport.mArgs[aIndex]; + nsAutoCString str; + // Use decimal for smaller numbers (more likely ints) and hex for + // larger (more likely pointers). This cutoff is arbitrary. + if (arg >= 1000000) { + str.AppendLiteral("0x"); + str.AppendInt(arg, 16); + } else { + str.AppendInt(arg, 10); + } + aRetval = str; + return NS_OK; +} + +class SandboxReportArray final : public mozISandboxReportArray { + public: + NS_DECL_ISUPPORTS + NS_DECL_MOZISANDBOXREPORTARRAY + + explicit SandboxReportArray(SandboxReporter::Snapshot&& aSnap) + : mOffset(aSnap.mOffset), mArray(std::move(aSnap.mReports)) {} + + private: + ~SandboxReportArray() = default; + uint64_t mOffset; + nsTArray mArray; +}; + +NS_IMPL_ISUPPORTS(SandboxReportArray, mozISandboxReportArray) + +/* readonly attribute uint64_t begin; */ +NS_IMETHODIMP SandboxReportArray::GetBegin(uint64_t* aBegin) { + *aBegin = mOffset; + return NS_OK; +} + +/* readonly attribute uint64_t end; */ +NS_IMETHODIMP SandboxReportArray::GetEnd(uint64_t* aEnd) { + *aEnd = mOffset + mArray.Length(); + return NS_OK; +} + +/* mozISandboxReport getElement (in uint64_t aIndex); */ +NS_IMETHODIMP SandboxReportArray::GetElement(uint64_t aIndex, + mozISandboxReport** aRetval) { + uint64_t relIndex = aIndex - mOffset; + if (relIndex >= mArray.Length()) { + return NS_ERROR_INVALID_ARG; + } + + nsCOMPtr wrapper = + new SandboxReportWrapper(mArray[relIndex]); + wrapper.forget(aRetval); + return NS_OK; +} + +class SandboxReporterWrapper final : public mozISandboxReporter { + public: + NS_DECL_ISUPPORTS + NS_DECL_MOZISANDBOXREPORTER + + SandboxReporterWrapper() = default; + + private: + ~SandboxReporterWrapper() = default; +}; + +NS_IMPL_ISUPPORTS(SandboxReporterWrapper, mozISandboxReporter) + +/* mozISandboxReportArray snapshot(); */ +NS_IMETHODIMP SandboxReporterWrapper::Snapshot( + mozISandboxReportArray** aRetval) { + if (!XRE_IsParentProcess()) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr wrapper = + new SandboxReportArray(SandboxReporter::Singleton()->GetSnapshot()); + wrapper.forget(aRetval); + return NS_OK; +} + +} // namespace mozilla + +NS_IMPL_COMPONENT_FACTORY(mozISandboxReporter) { + return MakeAndAddRef().downcast(); +} diff --git a/security/sandbox/linux/reporter/components.conf b/security/sandbox/linux/reporter/components.conf new file mode 100644 index 0000000000..7bd278f349 --- /dev/null +++ b/security/sandbox/linux/reporter/components.conf @@ -0,0 +1,13 @@ +# -*- Mode: python; 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/. + +Classes = [ + { + 'cid': '{5118a6f9-2493-4f97-9552-620663e03cb3}', + 'contract_ids': ['@mozilla.org/sandbox/syscall-reporter;1'], + 'type': 'mozISandboxReporter', + }, +] diff --git a/security/sandbox/linux/reporter/moz.build b/security/sandbox/linux/reporter/moz.build new file mode 100644 index 0000000000..d5b037fd09 --- /dev/null +++ b/security/sandbox/linux/reporter/moz.build @@ -0,0 +1,34 @@ +# -*- 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 += [ + "SandboxReporter.h", + "SandboxReporterCommon.h", +] + +UNIFIED_SOURCES += [ + "SandboxReporter.cpp", + "SandboxReporterWrappers.cpp", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +LOCAL_INCLUDES += [ + "/security/sandbox/linux", # SandboxLogging.h +] + +# Need this for base::PlatformThread +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", +] + +FINAL_LIBRARY = "xul" -- cgit v1.2.3