diff options
author | markus@chromium.org <markus@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-08-27 22:09:36 +0000 |
---|---|---|
committer | markus@chromium.org <markus@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-08-27 22:09:36 +0000 |
commit | b379c73977b06d5b0faefb8f660d09c8d823842b (patch) | |
tree | e74276d5831657a614095ad2c38b00bc619c77cb /sandbox | |
parent | e33bbc2105ad5a6fc86e74a438201cd67b7285ed (diff) | |
download | chromium_src-b379c73977b06d5b0faefb8f660d09c8d823842b.zip chromium_src-b379c73977b06d5b0faefb8f660d09c8d823842b.tar.gz chromium_src-b379c73977b06d5b0faefb8f660d09c8d823842b.tar.bz2 |
Simplified unit testing of sandboxing code. We now have helper methods that run all tests
inside their own processes. And we have another set of helpers that ensure we actually set
a sandboxing policy and don't forget to start the sandbox prior to running the tests.
Also simplified the handling of unexpected failure and termination of the sandbox'd process.
TODO: we still don't have a good story for testing fatal errors. We will eventually need
some form of exit tests.
BUG=n/a
TEST=sandbox_linux_unittests
Review URL: https://chromiumcodereview.appspot.com/10878033
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@153555 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'sandbox')
-rw-r--r-- | sandbox/linux/sandbox_linux.gypi | 6 | ||||
-rw-r--r-- | sandbox/linux/seccomp-bpf/Makefile | 2 | ||||
-rw-r--r-- | sandbox/linux/seccomp-bpf/bpf_tests.cc | 35 | ||||
-rw-r--r-- | sandbox/linux/seccomp-bpf/bpf_tests.h | 56 | ||||
-rw-r--r-- | sandbox/linux/seccomp-bpf/die.cc | 66 | ||||
-rw-r--r-- | sandbox/linux/seccomp-bpf/die.h | 47 | ||||
-rw-r--r-- | sandbox/linux/seccomp-bpf/sandbox_bpf.cc | 91 | ||||
-rw-r--r-- | sandbox/linux/seccomp-bpf/sandbox_bpf.h | 52 | ||||
-rw-r--r-- | sandbox/linux/seccomp-bpf/sandbox_bpf_unittest.cc | 172 | ||||
-rw-r--r-- | sandbox/linux/seccomp-bpf/util.cc | 4 | ||||
-rw-r--r-- | sandbox/linux/tests/main.cc | 16 | ||||
-rw-r--r-- | sandbox/linux/tests/unit_tests.cc | 81 | ||||
-rw-r--r-- | sandbox/linux/tests/unit_tests.h | 54 |
13 files changed, 453 insertions, 229 deletions
diff --git a/sandbox/linux/sandbox_linux.gypi b/sandbox/linux/sandbox_linux.gypi index fb68c73c..6d59816 100644 --- a/sandbox/linux/sandbox_linux.gypi +++ b/sandbox/linux/sandbox_linux.gypi @@ -41,7 +41,9 @@ '../testing/gtest.gyp:gtest', ], 'sources': [ + 'tests/main.cc', 'tests/unit_tests.cc', + 'tests/unit_tests.h', 'suid/client/setuid_sandbox_client_unittest.cc', ], 'include_dirs': [ @@ -51,6 +53,8 @@ [ 'OS=="linux" and (target_arch=="ia32" or target_arch=="x64" ' 'or target_arch=="arm")', { 'sources': [ + 'seccomp-bpf/bpf_tests.cc', + 'seccomp-bpf/bpf_tests.h', 'seccomp-bpf/sandbox_bpf_unittest.cc', ], }], @@ -60,6 +64,8 @@ 'target_name': 'seccomp_bpf', 'type': 'static_library', 'sources': [ + 'seccomp-bpf/die.cc', + 'seccomp-bpf/die.h', 'seccomp-bpf/sandbox_bpf.cc', 'seccomp-bpf/sandbox_bpf.h', 'seccomp-bpf/verifier.cc', diff --git a/sandbox/linux/seccomp-bpf/Makefile b/sandbox/linux/seccomp-bpf/Makefile index eb3cde6..5753124 100644 --- a/sandbox/linux/seccomp-bpf/Makefile +++ b/sandbox/linux/seccomp-bpf/Makefile @@ -2,7 +2,7 @@ CFLAGS = -g -O3 -Wall -Werror -Wextra -Wno-missing-field-initializers -fPIC -I. CPPFLAGS = -D_GNU_SOURCE -DSECCOMP_BPF_STANDALONE -iquote ../../.. LDFLAGS = -g -lpthread DEPFLAGS = -MMD -MF .$@.d -MODS := demo sandbox_bpf util verifier +MODS := demo sandbox_bpf die util verifier OBJS64 := $(shell echo ${MODS} | xargs -n 1 | sed -e 's/$$/.o64/') OBJS32 := $(shell echo ${MODS} | xargs -n 1 | sed -e 's/$$/.o32/') ALL_OBJS = $(OBJS32) $(OBJS64) diff --git a/sandbox/linux/seccomp-bpf/bpf_tests.cc b/sandbox/linux/seccomp-bpf/bpf_tests.cc new file mode 100644 index 0000000..efe4020 --- /dev/null +++ b/sandbox/linux/seccomp-bpf/bpf_tests.cc @@ -0,0 +1,35 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "sandbox/linux/seccomp-bpf/bpf_tests.h" + +using playground2::Die; +using playground2::Sandbox; + +namespace sandbox { + +void BpfTests::TestWrapper(void *void_arg) { + TestArgs *arg = reinterpret_cast<TestArgs *>(void_arg); + Die::EnableSimpleExit(); + if (Sandbox::supportsSeccompSandbox(-1) == + Sandbox::STATUS_AVAILABLE) { + // Ensure the the sandbox is actually available at this time + int proc_fd; + BPF_ASSERT((proc_fd = open("/proc", O_RDONLY|O_DIRECTORY)) >= 0); + BPF_ASSERT(Sandbox::supportsSeccompSandbox(proc_fd) == + Sandbox::STATUS_AVAILABLE); + + // Initialize and then start the sandbox with our custom policy + Sandbox::setProcFd(proc_fd); + Sandbox::setSandboxPolicy(arg->policy(), NULL); + Sandbox::startSandbox(); + arg->test()(); + } else { + // TODO(markus): (crbug.com/141545) Call the compiler and verify the + // policy. That's the least we can do, if we don't have kernel support. + Sandbox::setSandboxPolicy(arg->policy(), NULL); + } +} + +} // namespace diff --git a/sandbox/linux/seccomp-bpf/bpf_tests.h b/sandbox/linux/seccomp-bpf/bpf_tests.h new file mode 100644 index 0000000..ace0ce3 --- /dev/null +++ b/sandbox/linux/seccomp-bpf/bpf_tests.h @@ -0,0 +1,56 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SANDBOX_LINUX_SECCOMP_BPF_BPF_TESTS_H__ +#define SANDBOX_LINUX_SECCOMP_BPF_BPF_TESTS_H__ + +#include "sandbox/linux/tests/unit_tests.h" +#include "sandbox/linux/seccomp-bpf/sandbox_bpf.h" + + +namespace sandbox { + +// BPF_TEST() is a special version of SANDBOX_TEST(). It turns into a no-op, +// if the host does not have kernel support for running BPF filters. +// Also, it takes advantage of the Die class to avoid calling LOG(FATAL), from +// inside our tests, as we don't need or even want all the error handling that +// LOG(FATAL) would do. +#define BPF_TEST(test_case_name, test_name, policy) \ + void BPF_TEST_##test_name(); \ + TEST(test_case_name, test_name) { \ + sandbox::BpfTests::TestArgs arg(BPF_TEST_##test_name, policy); \ + sandbox::BpfTests::RunTestInProcess(sandbox::BpfTests::TestWrapper, &arg);\ + } \ + void BPF_TEST_##test_name() + +// Assertions are handled exactly the same as with a normal SANDBOX_TEST() +#define BPF_ASSERT SANDBOX_ASSERT + + +class BpfTests : public UnitTests { + public: + class TestArgs { + public: + TestArgs(void (*test)(), playground2::Sandbox::EvaluateSyscall policy) + : test_(test), + policy_(policy) { + } + + void (*test() const)() { return test_; } + playground2::Sandbox::EvaluateSyscall policy() const { return policy_; } + + private: + void (*test_)(); + playground2::Sandbox::EvaluateSyscall policy_; + }; + + static void TestWrapper(void *void_arg); + + private: + DISALLOW_IMPLICIT_CONSTRUCTORS(BpfTests); +}; + +} // namespace + +#endif // SANDBOX_LINUX_SECCOMP_BPF_BPF_TESTS_H__ diff --git a/sandbox/linux/seccomp-bpf/die.cc b/sandbox/linux/seccomp-bpf/die.cc new file mode 100644 index 0000000..b141424 --- /dev/null +++ b/sandbox/linux/seccomp-bpf/die.cc @@ -0,0 +1,66 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <string> + +#include "sandbox/linux/seccomp-bpf/sandbox_bpf.h" + + +namespace playground2 { + +void Die::ExitGroup() { + // exit_group() should exit our program. After all, it is defined as a + // function that doesn't return. But things can theoretically go wrong. + // Especially, since we are dealing with system call filters. Continuing + // execution would be very bad in most cases where ExitGroup() gets called. + // So, we'll try a few other strategies too. + syscall(__NR_exit_group, 1); + + // We have no idea what our run-time environment looks like. So, signal + // handlers might or might not do the right thing. Try to reset settings + // to a defined state; but we have not way to verify whether we actually + // succeeded in doing so. Nonetheless, triggering a fatal signal could help + // us terminate. + signal(SIGSEGV, SIG_DFL); + syscall(__NR_prctl, PR_SET_DUMPABLE, (void *)0, (void *)0, (void *)0); + if (*(volatile char *)0) { } + + // If there is no way for us to ask for the program to exit, the next + // best thing we can do is to loop indefinitely. Maybe, somebody will notice + // and file a bug... + // We in fact retry the system call inside of our loop so that it will + // stand out when somebody tries to diagnose the problem by using "strace". + for (;;) { + syscall(__NR_exit_group, 1); + } +} + +void Die::SandboxDie(const char *msg, const char *file, int line) { + if (simple_exit_) { + LogToStderr(msg, file, line); + } else { + #if defined(SECCOMP_BPF_STANDALONE) + Die::LogToStderr(msg, file, line); + #else + logging::LogMessage(file, line, logging::LOG_FATAL).stream() << msg; + #endif + } + ExitGroup(); +} + +void Die::LogToStderr(const char *msg, const char *file, int line) { + if (msg) { + char buf[40]; + snprintf(buf, sizeof(buf), "%d", line); + std::string s = std::string(file) + ":" + buf + ":" + msg + "\n"; + + // No need to loop. Short write()s are unlikely and if they happen we + // probably prefer them over a loop that blocks. + if (HANDLE_EINTR(write(2, s.c_str(), s.length()))) { } + } +} + +bool Die::simple_exit_ = false; + +} // namespace diff --git a/sandbox/linux/seccomp-bpf/die.h b/sandbox/linux/seccomp-bpf/die.h new file mode 100644 index 0000000..608afde --- /dev/null +++ b/sandbox/linux/seccomp-bpf/die.h @@ -0,0 +1,47 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SANDBOX_LINUX_SECCOMP_BPF_DIE_H__ +#define SANDBOX_LINUX_SECCOMP_BPF_DIE_H__ + +namespace playground2 { + +class Die { + public: + // This is the main API for using this file. Prints a error message and + // exits with a fatal error. + #define SANDBOX_DIE(m) Die::SandboxDie(m, __FILE__, __LINE__) + + // Terminate the program, even if the current sandbox policy prevents some + // of the more commonly used functions used for exiting. + // Most users would want to call SANDBOX_DIE() instead, as it logs extra + // information. But calling ExitGroup() is correct and in some rare cases + // preferable. So, we make it part of the public API. + static void ExitGroup() __attribute__((noreturn)); + + // This method gets called by SANDBOX_DIE(). There is normally no reason + // to call it directly unless you are defining your own exiting macro. + static void SandboxDie(const char *msg, const char *file, int line) + __attribute__((noreturn)); + + // Writes a message to stderr. Used as a fall-back choice, if we don't have + // any other way to report an error. + static void LogToStderr(const char *msg, const char *file, int line); + + // We generally want to run all exit handlers. This means, on SANDBOX_DIE() + // we should be calling LOG(FATAL). But there are some situations where + // we just need to print a message and then terminate. This would typically + // happen in cases where we consume the error message internally (e.g. in + // unit tests or in the supportsSeccompSandbox() method). + static void EnableSimpleExit() { simple_exit_ = true; } + + private: + static bool simple_exit_; + + DISALLOW_IMPLICIT_CONSTRUCTORS(Die); +}; + +} // namespace + +#endif // SANDBOX_LINUX_SECCOMP_BPF_DIE_H__ diff --git a/sandbox/linux/seccomp-bpf/sandbox_bpf.cc b/sandbox/linux/seccomp-bpf/sandbox_bpf.cc index dd6ad94..73efcfa 100644 --- a/sandbox/linux/seccomp-bpf/sandbox_bpf.cc +++ b/sandbox/linux/seccomp-bpf/sandbox_bpf.cc @@ -60,11 +60,11 @@ bool Sandbox::RunFunctionInPolicy(void (*CodeInSandbox)(), sigset_t oldMask, newMask; if (sigfillset(&newMask) || sigprocmask(SIG_BLOCK, &newMask, &oldMask)) { - die("sigprocmask() failed"); + SANDBOX_DIE("sigprocmask() failed"); } int fds[2]; if (pipe2(fds, O_NONBLOCK|O_CLOEXEC)) { - die("pipe() failed"); + SANDBOX_DIE("pipe() failed"); } pid_t pid = fork(); @@ -76,14 +76,14 @@ bool Sandbox::RunFunctionInPolicy(void (*CodeInSandbox)(), // attacker might cause fork() to fail at will and could trick us // into running without a sandbox. sigprocmask(SIG_SETMASK, &oldMask, NULL); // OK, if it fails - die("fork() failed unexpectedly"); + SANDBOX_DIE("fork() failed unexpectedly"); } // In the child process if (!pid) { // Test a very simple sandbox policy to verify that we can // successfully turn on sandboxing. - dryRun_ = true; + Die::EnableSimpleExit(); if (HANDLE_EINTR(close(fds[0])) || dup2(fds[1], 2) != 2 || HANDLE_EINTR(close(fds[1]))) { @@ -93,29 +93,34 @@ bool Sandbox::RunFunctionInPolicy(void (*CodeInSandbox)(), evaluators_.clear(); setSandboxPolicy(syscallEvaluator, NULL); setProcFd(proc_fd); - startSandbox(); + + // By passing "quiet=true" to "startSandboxInternal()" we suppress + // messages for expected and benign failures (e.g. if the current + // kernel lacks support for BPF filters). + startSandboxInternal(true); + // Run our code in the sandbox CodeInSandbox(); } - die(NULL); + SANDBOX_DIE(NULL); } // In the parent process. if (HANDLE_EINTR(close(fds[1]))) { - die("close() failed"); + SANDBOX_DIE("close() failed"); } if (sigprocmask(SIG_SETMASK, &oldMask, NULL)) { - die("sigprocmask() failed"); + SANDBOX_DIE("sigprocmask() failed"); } int status; if (HANDLE_EINTR(waitpid(pid, &status, 0)) != pid) { - die("waitpid() failed unexpectedly"); + SANDBOX_DIE("waitpid() failed unexpectedly"); } bool rc = WIFEXITED(status) && WEXITSTATUS(status) == 100; // If we fail to support sandboxing, there might be an additional // error message. If so, this was an entirely unexpected and fatal - // failure. We should report the failure and somebody most fix + // failure. We should report the failure and somebody must fix // things. This is probably a security-critical bug in the sandboxing // code. if (!rc) { @@ -126,11 +131,11 @@ bool Sandbox::RunFunctionInPolicy(void (*CodeInSandbox)(), --len; } buf[len] = '\000'; - die(buf); + SANDBOX_DIE(buf); } } if (HANDLE_EINTR(close(fds[0]))) { - die("close() failed"); + SANDBOX_DIE("close() failed"); } return rc; @@ -193,12 +198,13 @@ void Sandbox::setProcFd(int proc_fd) { proc_fd_ = proc_fd; } -void Sandbox::startSandbox() { +void Sandbox::startSandboxInternal(bool quiet) { if (status_ == STATUS_UNSUPPORTED || status_ == STATUS_UNAVAILABLE) { - die("Trying to start sandbox, even though it is known to be unavailable"); + SANDBOX_DIE("Trying to start sandbox, even though it is known to be " + "unavailable"); } else if (status_ == STATUS_ENABLED) { - die("Cannot start sandbox recursively. Use multiple calls to " - "setSandboxPolicy() to stack policies instead"); + SANDBOX_DIE("Cannot start sandbox recursively. Use multiple calls to " + "setSandboxPolicy() to stack policies instead"); } if (proc_fd_ < 0) { proc_fd_ = open("/proc", O_RDONLY|O_DIRECTORY); @@ -208,7 +214,7 @@ void Sandbox::startSandbox() { // In the future, we might want to tighten this requirement. } if (!isSingleThreaded(proc_fd_)) { - die("Cannot start sandbox, if process is already multi-threaded"); + SANDBOX_DIE("Cannot start sandbox, if process is already multi-threaded"); } // We no longer need access to any files in /proc. We want to do this @@ -216,13 +222,13 @@ void Sandbox::startSandbox() { // close(). if (proc_fd_ >= 0) { if (HANDLE_EINTR(close(proc_fd_))) { - die("Failed to close file descriptor for /proc"); + SANDBOX_DIE("Failed to close file descriptor for /proc"); } proc_fd_ = -1; } // Install the filters. - installFilter(); + installFilter(quiet); // We are now inside the sandbox. status_ = STATUS_ENABLED; @@ -262,7 +268,7 @@ void Sandbox::policySanityChecks(EvaluateSyscall syscallEvaluator, // We also have similar checks later, when we actually compile the BPF // program. That catches problems with incorrectly stacked evaluators. if (!isDenied(syscallEvaluator(-1))) { - die("Negative system calls should always be disallowed by policy"); + SANDBOX_DIE("Negative system calls should always be disallowed by policy"); } #ifndef NDEBUG #if defined(__i386__) || defined(__x86_64__) @@ -271,7 +277,7 @@ void Sandbox::policySanityChecks(EvaluateSyscall syscallEvaluator, sysnum <= (MAX_SYSCALL & ~0x40000000u); ++sysnum) { if (!isDenied(syscallEvaluator(sysnum))) { - die("In x32 mode, you should not allow any non-x32 system calls"); + SANDBOX_DIE("In x32 mode, you should not allow any non-x32 system calls"); } } #else @@ -279,7 +285,7 @@ void Sandbox::policySanityChecks(EvaluateSyscall syscallEvaluator, sysnum <= (MAX_SYSCALL | 0x40000000u); ++sysnum) { if (!isDenied(syscallEvaluator(sysnum))) { - die("x32 system calls should be explicitly disallowed"); + SANDBOX_DIE("x32 system calls should be explicitly disallowed"); } } #endif @@ -293,8 +299,8 @@ void Sandbox::policySanityChecks(EvaluateSyscall syscallEvaluator, !isDenied(syscallEvaluator(-1)) || !isDenied(syscallEvaluator(static_cast<int>(MIN_SYSCALL) - 1)) || !isDenied(syscallEvaluator(static_cast<int>(MAX_SYSCALL) + 1))) { - die("Even for default-allow policies, you must never allow system calls " - "outside of the standard system call range"); + SANDBOX_DIE("Even for default-allow policies, you must never allow system " + "calls outside of the standard system call range"); } return; } @@ -302,17 +308,17 @@ void Sandbox::policySanityChecks(EvaluateSyscall syscallEvaluator, void Sandbox::setSandboxPolicy(EvaluateSyscall syscallEvaluator, EvaluateArguments argumentEvaluator) { if (status_ == STATUS_ENABLED) { - die("Cannot change policy after sandbox has started"); + SANDBOX_DIE("Cannot change policy after sandbox has started"); } policySanityChecks(syscallEvaluator, argumentEvaluator); evaluators_.push_back(std::make_pair(syscallEvaluator, argumentEvaluator)); } -void Sandbox::installFilter() { +void Sandbox::installFilter(bool quiet) { // Verify that the user pushed a policy. if (evaluators_.empty()) { filter_failed: - die("Failed to configure system call filters"); + SANDBOX_DIE("Failed to configure system call filters"); } // Set new SIGSYS handler @@ -335,13 +341,13 @@ void Sandbox::installFilter() { // We can't handle stacked evaluators, yet. We'll get there eventually // though. Hang tight. if (evaluators_.size() != 1) { - die("Not implemented"); + SANDBOX_DIE("Not implemented"); } // Assemble the BPF filter program. Program *program = new Program(); if (!program) { - die("Out of memory"); + SANDBOX_DIE("Out of memory"); } // If the architecture doesn't match SECCOMP_ARCH, disallow the @@ -396,7 +402,7 @@ void Sandbox::installFilter() { #ifndef NDEBUG const char *err = NULL; if (!Verifier::verifyBPF(*program, evaluators_, &err)) { - die(err); + SANDBOX_DIE(err); } #endif @@ -422,10 +428,12 @@ void Sandbox::installFilter() { // Install BPF filter program if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { - die(dryRun_ ? NULL : "Kernel refuses to enable no-new-privs"); + SANDBOX_DIE(quiet + ? NULL : "Kernel refuses to enable no-new-privs"); } else { if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) { - die(dryRun_ ? NULL : "Kernel refuses to turn on BPF filters"); + SANDBOX_DIE(quiet + ? NULL : "Kernel refuses to turn on BPF filters"); } } @@ -463,7 +471,7 @@ void Sandbox::findRanges(Ranges *ranges) { if (oldErr != evaluateSyscall(std::numeric_limits<int>::max()) || oldErr != evaluateSyscall(std::numeric_limits<int>::min()) || oldErr != evaluateSyscall(-1)) { - die("Invalid seccomp policy"); + SANDBOX_DIE("Invalid seccomp policy"); } ranges->push_back( Range(oldSysnum, std::numeric_limits<unsigned>::max(), oldErr)); @@ -477,7 +485,7 @@ void Sandbox::emitJumpStatements(Program *program, RetInsns *rets, // As a sanity check, we need to have at least two distinct ranges for us // to be able to build a jump table. if (stop - start <= 1) { - die("Invalid set of system call ranges"); + SANDBOX_DIE("Invalid set of system call ranges"); } // Pick the range object that is located at the mid point of our list. @@ -488,7 +496,7 @@ void Sandbox::emitJumpStatements(Program *program, RetInsns *rets, Program::size_type jmp = program->size(); if (jmp >= SECCOMP_MAX_PROGRAM_SIZE) { compiler_err: - die("Internal compiler error; failed to compile jump table"); + SANDBOX_DIE("Internal compiler error; failed to compile jump table"); } program->push_back((struct sock_filter) BPF_JUMP(BPF_JMP+BPF_JGE+BPF_K, mid->from, @@ -542,7 +550,7 @@ void Sandbox::emitReturnStatements(Program *program, const RetInsns& rets) { ++ret_iter) { Program::size_type ip = program->size(); if (ip >= SECCOMP_MAX_PROGRAM_SIZE) { - die("Internal compiler error; failed to compile jump table"); + SANDBOX_DIE("Internal compiler error; failed to compile jump table"); } program->push_back((struct sock_filter) BPF_STMT(BPF_RET+BPF_K, ret_iter->first)); @@ -555,7 +563,7 @@ void Sandbox::emitReturnStatements(Program *program, const RetInsns& rets) { // Jumps are always relative and they are always forward. int distance = ip - insn_iter->addr - 1; if (distance < 0 || distance > 255) { - die("Internal compiler error; failed to compile jump table"); + SANDBOX_DIE("Internal compiler error; failed to compile jump table"); } // Decide whether we need to patch up the "true" or the "false" jump @@ -576,7 +584,7 @@ void Sandbox::sigSys(int nr, siginfo_t *info, void *void_context) { if (nr != SIGSYS || info->si_code != SYS_SECCOMP || !void_context || info->si_errno <= 0 || static_cast<size_t>(info->si_errno) > trapArraySize_) { - // die() can call LOG(FATAL). This is not normally async-signal safe + // SANDBOX_DIE() can call LOG(FATAL). This is not normally async-signal safe // and can lead to bugs. We should eventually implement a different // logging and reporting mechanism that is safe to be called from // the sigSys() handler. @@ -584,7 +592,7 @@ void Sandbox::sigSys(int nr, siginfo_t *info, void *void_context) { // could actually make an argument that spurious SIGSYS should // just get silently ignored. TBD sigsys_err: - die("Unexpected SIGSYS received"); + SANDBOX_DIE("Unexpected SIGSYS received"); } // Signal handlers should always preserve "errno". Otherwise, we could @@ -640,7 +648,7 @@ void Sandbox::sigSys(int nr, siginfo_t *info, void *void_context) { } intptr_t Sandbox::bpfFailure(const struct arch_seccomp_data&, void *aux) { - die(static_cast<char *>(aux)); + SANDBOX_DIE(static_cast<char *>(aux)); } int Sandbox::getTrapId(Sandbox::TrapFnc fnc, const void *aux) { @@ -665,7 +673,7 @@ int Sandbox::getTrapId(Sandbox::TrapFnc fnc, const void *aux) { if (id > SECCOMP_RET_DATA) { // In practice, this is pretty much impossible to trigger, as there // are other kernel limitations that restrict overall BPF program sizes. - die("Too many SECCOMP_RET_TRAP callback instances"); + SANDBOX_DIE("Too many SECCOMP_RET_TRAP callback instances"); } traps_->push_back(ErrorCode(fnc, aux, id)); @@ -683,7 +691,6 @@ int Sandbox::getTrapId(Sandbox::TrapFnc fnc, const void *aux) { } } -bool Sandbox::dryRun_ = false; Sandbox::SandboxStatus Sandbox::status_ = STATUS_UNKNOWN; int Sandbox::proc_fd_ = -1; Sandbox::Evaluators Sandbox::evaluators_; diff --git a/sandbox/linux/seccomp-bpf/sandbox_bpf.h b/sandbox/linux/seccomp-bpf/sandbox_bpf.h index eb99d99..c173d78 100644 --- a/sandbox/linux/seccomp-bpf/sandbox_bpf.h +++ b/sandbox/linux/seccomp-bpf/sandbox_bpf.h @@ -144,6 +144,9 @@ #endif +#include "sandbox/linux/seccomp-bpf/die.h" + + struct arch_seccomp_data { int nr; uint32_t arch; @@ -223,13 +226,13 @@ class Sandbox { err_ = SECCOMP_RET_ALLOW; break; case SB_INSPECT_ARG_1...SB_INSPECT_ARG_6: - die("Not implemented"); + SANDBOX_DIE("Not implemented"); break; case 1 ... 4095: err_ = SECCOMP_RET_ERRNO + err; break; default: - die("Invalid use of ErrorCode object"); + SANDBOX_DIE("Invalid use of ErrorCode object"); } } @@ -311,48 +314,9 @@ class Sandbox { // This is the main public entry point. It finds all system calls that // need rewriting, sets up the resources needed by the sandbox, and // enters Seccomp mode. - static void startSandbox(); + static void startSandbox() { startSandboxInternal(false); } protected: - // Print an error message and terminate the program. Used for fatal errors. - static void die(const char *msg) __attribute__((noreturn)) { - if (msg) { -#ifndef SECCOMP_BPF_STANDALONE - if (!dryRun_) { - // LOG(FATAL) is not neccessarily async-signal safe. It would be - // better to always use the code for the SECCOMP_BPF_STANDALONE case. - // But that prevents the logging and reporting infrastructure from - // picking up sandbox related crashes. - // For now, in picking between two evils, we decided in favor of - // LOG(FATAL). In the long run, we probably want to rewrite this code - // to be async-signal safe. - LOG(FATAL) << msg; - } else -#endif - { - // If there is no logging infrastructure in place, we just write error - // messages to stderr. - // We also write to stderr, if we are called in a child process from - // supportsSeccompSandbox(). This makes sure we can actually do the - // correct logging from the parent process, which is more likely to - // have access to logging infrastructure. - if (HANDLE_EINTR(write(2, msg, strlen(msg)))) { } - if (HANDLE_EINTR(write(2, "\n", 1))) { } - } - } - for (;;) { - // exit_group() should exit our program. After all, it is defined as a - // function that doesn't return. But things can theoretically go wrong. - // Especially, since we are dealing with system call filters. Continuing - // execution would be very bad in most cases where die() gets called. - // So, if there is no way for us to ask for the program to exit, the next - // best thing we can do is to loop indefinitely. Maybe, somebody will - // notice and file a bug... - syscall(__NR_exit_group, 1); - _exit(1); - } - } - // Get a file descriptor pointing to "/proc", if currently available. static int getProcFd() { return proc_fd_; } @@ -388,11 +352,12 @@ class Sandbox { static bool RunFunctionInPolicy(void (*function)(), EvaluateSyscall syscallEvaluator, int proc_fd); + static void startSandboxInternal(bool quiet); static bool isSingleThreaded(int proc_fd); static bool disableFilesystem(); static void policySanityChecks(EvaluateSyscall syscallEvaluator, EvaluateArguments argumentEvaluator); - static void installFilter(); + static void installFilter(bool quiet); static void findRanges(Ranges *ranges); static void emitJumpStatements(Program *program, RetInsns *rets, Ranges::const_iterator start, @@ -402,7 +367,6 @@ class Sandbox { static intptr_t bpfFailure(const struct arch_seccomp_data& data, void *aux); static int getTrapId(TrapFnc fnc, const void *aux); - static bool dryRun_; static SandboxStatus status_; static int proc_fd_; static Evaluators evaluators_; diff --git a/sandbox/linux/seccomp-bpf/sandbox_bpf_unittest.cc b/sandbox/linux/seccomp-bpf/sandbox_bpf_unittest.cc index 54fe7a7..ac25b2b 100644 --- a/sandbox/linux/seccomp-bpf/sandbox_bpf_unittest.cc +++ b/sandbox/linux/seccomp-bpf/sandbox_bpf_unittest.cc @@ -4,7 +4,7 @@ #include <ostream> -#include "sandbox/linux/seccomp-bpf/sandbox_bpf.h" +#include "sandbox/linux/seccomp-bpf/bpf_tests.h" #include "sandbox/linux/seccomp-bpf/verifier.h" #include "testing/gtest/include/gtest/gtest.h" @@ -17,6 +17,8 @@ const int kExpectedReturnValue = 42; const int kArmPublicSysnoCeiling = __NR_SYSCALL_BASE + 1024; #endif +// This test should execute no matter whether we have kernel support. So, +// we make it a TEST() instead of a BPF_TEST(). TEST(SandboxBpf, CallSupports) { // We check that we don't crash, but it's ok if the kernel doesn't // support it. @@ -31,83 +33,11 @@ TEST(SandboxBpf, CallSupports) { << "\n"; } -TEST(SandboxBpf, CallSupportsTwice) { +SANDBOX_TEST(SandboxBpf, CallSupportsTwice) { Sandbox::supportsSeccompSandbox(-1); Sandbox::supportsSeccompSandbox(-1); } -__attribute__((noreturn)) void DoCrash() { - // Cause a #PF. This only works if we assume that we have the default - // SIGSEGV handler. - *(reinterpret_cast<volatile char*>(NULL)) = '\0'; - for (;;) { - // If we didn't manage to crash there is really nothing we can do reliably - // but spin. - } -} - -__attribute__((noreturn)) void ExitGroup(int status) { - syscall(__NR_exit_group, status); - // If exit_group() failed, there is a high likelihood this - // happened due to a bug in the sandbox. We therefore cannot - // blindly assume that the failure happened because we are - // running on an old kernel that supports exit(), but not - // exit_group(). We cannot even trust "errno" returning - // "ENOSYS". So, calling exit() as the fallback for - // exit_group() would at this point almost certainly be a - // bug. Instead, try some more aggressive methods to make - // the program stop. - DoCrash(); -} - -// Helper function to start a sandbox with a policy specified in -// evaluator -void StartSandboxOrDie(Sandbox::EvaluateSyscall evaluator) { - int proc_fd = open("/proc", O_RDONLY|O_DIRECTORY); - if (proc_fd < 0 || !evaluator) { - ExitGroup(1); - } - if (Sandbox::supportsSeccompSandbox(proc_fd) != - Sandbox::STATUS_AVAILABLE) { - ExitGroup(1); - } - Sandbox::setProcFd(proc_fd); - Sandbox::setSandboxPolicy(evaluator, NULL); - Sandbox::startSandbox(); -} - -void RunInSandbox(Sandbox::EvaluateSyscall evaluator, - void (*SandboxedCode)()) { - // TODO(markus): Implement IsEqual for ErrorCode - // IsEqual(evaluator(__NR_exit_group), Sandbox::SB_ALLOWED) << - // "You need to always allow exit_group() in your test policy"; - StartSandboxOrDie(evaluator); - // TODO(jln): find a way to use the testing framework inside - // SandboxedCode() or at the very least to surface errors - SandboxedCode(); - // SandboxedCode() should have exited, this is a failure - ExitGroup(1); -} - -// evaluator should always allow ExitGroup -// SandboxedCode should ExitGroup(kExpectedReturnValue) if and only if -// things go as expected. -void TryPolicyInProcess(Sandbox::EvaluateSyscall evaluator, - void (*SandboxedCode)()) { - // TODO(jln) figure out a way to surface whether we're actually testing - // something or not. - if (Sandbox::supportsSeccompSandbox(-1) == Sandbox::STATUS_AVAILABLE) { - EXPECT_EXIT(RunInSandbox(evaluator, SandboxedCode), - ::testing::ExitedWithCode(kExpectedReturnValue), - ""); - } else { - // The sandbox is not available. We should still try to exercise what we - // can. - // TODO(markus): (crbug.com/141545) let us call the compiler from here. - Sandbox::setSandboxPolicy(evaluator, NULL); - } -} - // A simple blacklist test Sandbox::ErrorCode BlacklistNanosleepPolicy(int sysno) { @@ -124,17 +54,12 @@ Sandbox::ErrorCode BlacklistNanosleepPolicy(int sysno) { } } -void NanosleepProcess(void) { +BPF_TEST(SandboxBpf, ApplyBasicBlacklistPolicy, BlacklistNanosleepPolicy) { + // nanosleep() should be denied const struct timespec ts = {0, 0}; errno = 0; - if(syscall(__NR_nanosleep, &ts, NULL) != -1 || errno != EACCES) { - ExitGroup(1); - } - ExitGroup(kExpectedReturnValue); -} - -TEST(SandboxBpf, ApplyBasicBlacklistPolicy) { - TryPolicyInProcess(BlacklistNanosleepPolicy, NanosleepProcess); + BPF_ASSERT(syscall(__NR_nanosleep, &ts, NULL) == -1); + BPF_ASSERT(errno == EACCES); } // Now do a simple whitelist test @@ -149,19 +74,15 @@ Sandbox::ErrorCode WhitelistGetpidPolicy(int sysno) { } } -void GetpidProcess(void) { - errno = 0; +BPF_TEST(SandboxBpf, ApplyBasicWhitelistPolicy, WhitelistGetpidPolicy) { // getpid() should be allowed - if (syscall(__NR_getpid) < 0 || errno) - ExitGroup(1); - // getpgid() should be denied - if (getpgid(0) != -1 || errno != ENOMEM) - ExitGroup(1); - ExitGroup(kExpectedReturnValue); -} + errno = 0; + BPF_ASSERT(syscall(__NR_getpid) > 0); + BPF_ASSERT(errno == 0); -TEST(SandboxBpf, ApplyBasicWhitelistPolicy) { - TryPolicyInProcess(WhitelistGetpidPolicy, GetpidProcess); + // getpgid() should be denied + BPF_ASSERT(getpgid(0) == -1); + BPF_ASSERT(errno == ENOMEM); } // A simple blacklist policy, with a SIGSYS handler @@ -173,8 +94,7 @@ static int BlacklistNanosleepPolicySigsysAuxData; intptr_t EnomemHandler(const struct arch_seccomp_data& args, void *aux) { // We also check that the auxiliary data is correct - if (!aux) - ExitGroup(1); + SANDBOX_ASSERT(aux); *(static_cast<int*>(aux)) = kExpectedReturnValue; return -ENOMEM; } @@ -194,26 +114,22 @@ Sandbox::ErrorCode BlacklistNanosleepPolicySigsys(int sysno) { } } -void NanosleepProcessSigsys(void) { - const struct timespec ts = {0, 0}; - errno = 0; +BPF_TEST(SandboxBpf, BasicBlacklistWithSigsys, + BlacklistNanosleepPolicySigsys) { // getpid() should work properly - if (syscall(__NR_getpid) < 0) - ExitGroup(1); + errno = 0; + BPF_ASSERT(syscall(__NR_getpid) > 0); + BPF_ASSERT(errno == 0); + // Our Auxiliary Data, should be reset by the signal handler BlacklistNanosleepPolicySigsysAuxData = -1; - errno = 0; - if (syscall(__NR_nanosleep, &ts, NULL) != -1 || errno != ENOMEM) - ExitGroup(1); - // We expect the signal handler to modify AuxData - if (BlacklistNanosleepPolicySigsysAuxData != kExpectedReturnValue) - ExitGroup(1); - else - ExitGroup(kExpectedReturnValue); -} + const struct timespec ts = {0, 0}; + BPF_ASSERT(syscall(__NR_nanosleep, &ts, NULL) == -1); + BPF_ASSERT(errno == ENOMEM); -TEST(SandboxBpf, BasicBlacklistWithSigsys) { - TryPolicyInProcess(BlacklistNanosleepPolicySigsys, NanosleepProcessSigsys); + // We expect the signal handler to modify AuxData + BPF_ASSERT( + BlacklistNanosleepPolicySigsysAuxData == kExpectedReturnValue); } // A more complex, but synthetic policy. This tests the correctness of the BPF @@ -244,21 +160,24 @@ Sandbox::ErrorCode SyntheticPolicy(int sysno) { return ENOSYS; #endif - if (sysno == __NR_exit_group) { + // TODO(markus): allow calls to write(). This should start working as soon + // as we switch to the new code generator. Currently we are blowing up, + // because our jumptable is getting too big. + if (sysno == __NR_exit_group /* || sysno == __NR_write */) { // exit_group() is special, we really need it to work. + // write() is needed for BPF_ASSERT() to report a useful error message. return Sandbox::SB_ALLOWED; } else { return SysnoToRandomErrno(sysno); } } -void SyntheticProcess(void) { +BPF_TEST(SandboxBpf, SyntheticPolicy, SyntheticPolicy) { // Ensure that that kExpectedReturnValue + syscallnumber + 1 does not int // overflow. - if (std::numeric_limits<int>::max() - kExpectedReturnValue - 1 < - static_cast<int>(MAX_SYSCALL)) { - ExitGroup(1); - } + BPF_ASSERT( + std::numeric_limits<int>::max() - kExpectedReturnValue - 1 >= + static_cast<int>(MAX_SYSCALL)); // TODO(jorgelo): remove this limit once crbug.com/141694 is fixed. #if defined(__arm__) @@ -270,24 +189,15 @@ void SyntheticProcess(void) { for (int syscall_number = static_cast<int>(MIN_SYSCALL); syscall_number <= sysno_ceiling; ++syscall_number) { - if (syscall_number == __NR_exit_group) { + if (syscall_number == __NR_exit_group || + syscall_number == __NR_write) { // exit_group() is special continue; } errno = 0; - if (syscall(syscall_number) != -1 || - errno != SysnoToRandomErrno(syscall_number)) { - // Exit with a return value that is different than kExpectedReturnValue - // to signal an error. Make it easy to see what syscall_number failed in - // the test report. - ExitGroup(kExpectedReturnValue + syscall_number + 1); - } + BPF_ASSERT(syscall(syscall_number) == -1); + BPF_ASSERT(errno == SysnoToRandomErrno(syscall_number)); } - ExitGroup(kExpectedReturnValue); -} - -TEST(SandboxBpf, SyntheticPolicy) { - TryPolicyInProcess(SyntheticPolicy, SyntheticProcess); } } // namespace diff --git a/sandbox/linux/seccomp-bpf/util.cc b/sandbox/linux/seccomp-bpf/util.cc index 3d3233b..ac75146 100644 --- a/sandbox/linux/seccomp-bpf/util.cc +++ b/sandbox/linux/seccomp-bpf/util.cc @@ -120,7 +120,7 @@ void Util::closeAllBut(int fd, ...) { int fdir; if ((proc_fd = Sandbox::getProcFd()) < 0 || (fdir = openat(proc_fd, "self/fd", O_RDONLY|O_DIRECTORY)) < 0) { - Sandbox::die("Cannot access \"/proc/self/fd\""); + SANDBOX_DIE("Cannot access \"/proc/self/fd\""); } int dev_null = open("/dev/null", O_RDWR); DIR *dir = fdopendir(fdir); @@ -140,7 +140,7 @@ void Util::closeAllBut(int fd, ...) { // then we are better off leaving the standard descriptors open. if (dev_null >= 0) { if (HANDLE_EINTR(dup2(dev_null, i))) { - Sandbox::die("Cannot dup2()"); + SANDBOX_DIE("Cannot dup2()"); } } } else { diff --git a/sandbox/linux/tests/main.cc b/sandbox/linux/tests/main.cc new file mode 100644 index 0000000..4412645 --- /dev/null +++ b/sandbox/linux/tests/main.cc @@ -0,0 +1,16 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "testing/gtest/include/gtest/gtest.h" + + +int main(int argc, char *argv[]) { + testing::InitGoogleTest(&argc, argv); + // Always go through re-execution for death tests. + // This makes gtest only marginally slower for us and has the + // additional side effect of getting rid of gtest warnings about fork() + // safety. + ::testing::FLAGS_gtest_death_test_style = "threadsafe"; + return RUN_ALL_TESTS(); +} diff --git a/sandbox/linux/tests/unit_tests.cc b/sandbox/linux/tests/unit_tests.cc index 2e24d8c..105c45b 100644 --- a/sandbox/linux/tests/unit_tests.cc +++ b/sandbox/linux/tests/unit_tests.cc @@ -2,13 +2,76 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "testing/gtest/include/gtest/gtest.h" -int main(int argc, char *argv[]) { - testing::InitGoogleTest(&argc, argv); - // Always go through re-execution for death tests. - // This makes gtest only marginally slower for us and has the - // additional side effect of getting rid of gtest warnings about fork() - // safety. - ::testing::FLAGS_gtest_death_test_style = "threadsafe"; - return RUN_ALL_TESTS(); +#include <stdio.h> +#include <sys/resource.h> +#include <sys/time.h> + +#include "base/file_util.h" +#include "sandbox/linux/tests/unit_tests.h" + +namespace sandbox { + +static const int kExpectedValue = 42; + +void UnitTests::RunTestInProcess(UnitTests::Test test, void *arg) { + // Runs a test in a sub-process. This is necessary for most of the code + // in the BPF sandbox, as it potentially makes global state changes and as + // it also tends to raise fatal errors, if the code has been used in an + // insecure manner. + int fds[2]; + ASSERT_EQ(0, pipe(fds)); + + pid_t pid; + ASSERT_LE(0, (pid = fork())); + if (!pid) { + // In child process + // Redirect stderr to our pipe. This way, we can capture all error + // messages, if we decide we want to do so in our tests. + SANDBOX_ASSERT(dup2(fds[1], 2) == 2); + SANDBOX_ASSERT(!close(fds[0])); + SANDBOX_ASSERT(!close(fds[1])); + + // Disable core files. They are not very useful for our individual test + // cases. + struct rlimit no_core = { 0 }; + setrlimit(RLIMIT_CORE, &no_core); + + test(arg); + _exit(kExpectedValue); + } + + (void)HANDLE_EINTR(close(fds[1])); + std::vector<char> msg; + ssize_t rc; + do { + const unsigned int kCapacity = 256; + size_t len = msg.size(); + msg.resize(len + kCapacity); + rc = HANDLE_EINTR(read(fds[0], &msg[len], kCapacity)); + msg.resize(len + std::max(rc, static_cast<ssize_t>(0))); + } while (rc > 0); + std::string details; + if (!msg.empty()) { + details = "Actual test failure: " + std::string(msg.begin(), msg.end()); + } + (void)HANDLE_EINTR(close(fds[0])); + + int status = 0; + int waitpid_returned = HANDLE_EINTR(waitpid(pid, &status, 0)); + ASSERT_EQ(pid, waitpid_returned) << details; + bool subprocess_terminated_normally = WIFEXITED(status); + ASSERT_TRUE(subprocess_terminated_normally) << details; + int subprocess_exit_status = WEXITSTATUS(status); + ASSERT_EQ(kExpectedValue, subprocess_exit_status) << details; + bool subprocess_exited_but_printed_messages = !msg.empty(); + EXPECT_FALSE(subprocess_exited_but_printed_messages) << details; } + +void UnitTests::AssertionFailure(const char *expr, const char *file, + int line) { + fprintf(stderr, "%s:%d:%s", file, line, expr); + fflush(stderr); + _exit(1); +} + +} // namespace diff --git a/sandbox/linux/tests/unit_tests.h b/sandbox/linux/tests/unit_tests.h new file mode 100644 index 0000000..d6b4761 --- /dev/null +++ b/sandbox/linux/tests/unit_tests.h @@ -0,0 +1,54 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SANDBOX_LINUX_TESTS_UNIT_TESTS_H__ +#define SANDBOX_LINUX_TESTS_UNIT_TESTS_H__ + +#include "base/basictypes.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace sandbox { + +// Define a new test case that runs inside of a death test. This is necessary, +// as most of our tests by definition make global and irreversible changes to +// the system (i.e. they install a sandbox). GTest provides death tests as a +// tool to isolate global changes from the rest of the tests. +#define SANDBOX_TEST(test_case_name, test_name) \ + void TEST_##test_name(void *); \ + TEST(test_case_name, test_name) { \ + sandbox::UnitTests::RunTestInProcess(TEST_##test_name, NULL); \ + } \ + void TEST_##test_name(void *) + +// Simple assertion macro that is compatible with running inside of a death +// test. We unfortunately cannot use any of the GTest macros. +#define SANDBOX_STR(x) #x +#define SANDBOX_ASSERT(expr) \ + ((expr) \ + ? static_cast<void>(0) \ + : sandbox::UnitTests::AssertionFailure(SANDBOX_STR(expr), \ + __FILE__, __LINE__)) + +class UnitTests { + public: + typedef void (*Test)(void *); + + // Runs a test inside a short-lived process. Do not call this function + // directly. It is automatically invoked by SANDBOX_TEST(). Most sandboxing + // functions make global irreversible changes to the execution environment + // and must therefore execute in their own isolated process. + static void RunTestInProcess(Test test, void *arg); + + // Report a useful error message and terminate the current SANDBOX_TEST(). + // Calling this function from outside a SANDBOX_TEST() is unlikely to do + // anything useful. + static void AssertionFailure(const char *expr, const char *file, int line); + + private: + DISALLOW_IMPLICIT_CONSTRUCTORS(UnitTests); +}; + +} // namespace + +#endif // SANDBOX_LINUX_TESTS_UNIT_TESTS_H__ |