commit 1a10948260d915d4982415e37dd6495bca9ef545
Author: Simon South <simon(a)simonsouth.net>
Date: Thu Sep 2 11:04:23 2021 -0400
test: Add sandbox unit tests
---
changes/issue16803 | 2 +
src/test/include.am | 1 +
src/test/test.c | 4 +
src/test/test.h | 1 +
src/test/test_sandbox.c | 348 ++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 356 insertions(+)
diff --git a/changes/issue16803 b/changes/issue16803
new file mode 100644
index 0000000000..7d0dd833e2
--- /dev/null
+++ b/changes/issue16803
@@ -0,0 +1,2 @@
+ o Testing:
+ - Add unit tests for the Linux seccomp sandbox. Resolves issue 16803.
diff --git a/src/test/include.am b/src/test/include.am
index 0dc1630044..fb2c03ceb9 100644
--- a/src/test/include.am
+++ b/src/test/include.am
@@ -231,6 +231,7 @@ src_test_test_SOURCES += \
src/test/test_routerkeys.c \
src/test/test_routerlist.c \
src/test/test_routerset.c \
+ src/test/test_sandbox.c \
src/test/test_scheduler.c \
src/test/test_sendme.c \
src/test/test_shared_random.c \
diff --git a/src/test/test.c b/src/test/test.c
index 0aa1353ec2..c38d78da30 100644
--- a/src/test/test.c
+++ b/src/test/test.c
@@ -52,6 +52,7 @@
#include "core/crypto/onion_fast.h"
#include "core/crypto/onion_tap.h"
#include "core/or/policies.h"
+#include "lib/sandbox/sandbox.h"
#include "app/config/statefile.h"
#include "lib/crypt_ops/crypto_curve25519.h"
#include "feature/nodelist/networkstatus.h"
@@ -732,6 +733,9 @@ struct testgroup_t testgroups[] = {
{ "routerkeys/", routerkeys_tests },
{ "routerlist/", routerlist_tests },
{ "routerset/" , routerset_tests },
+#ifdef USE_LIBSECCOMP
+ { "sandbox/" , sandbox_tests },
+#endif
{ "scheduler/", scheduler_tests },
{ "sendme/", sendme_tests },
{ "shared-random/", sr_tests },
diff --git a/src/test/test.h b/src/test/test.h
index 0a22043acc..e17bce427c 100644
--- a/src/test/test.h
+++ b/src/test/test.h
@@ -184,6 +184,7 @@ extern struct testcase_t router_tests[];
extern struct testcase_t routerkeys_tests[];
extern struct testcase_t routerlist_tests[];
extern struct testcase_t routerset_tests[];
+extern struct testcase_t sandbox_tests[];
extern struct testcase_t scheduler_tests[];
extern struct testcase_t sendme_tests[];
extern struct testcase_t socks_tests[];
diff --git a/src/test/test_sandbox.c b/src/test/test_sandbox.c
new file mode 100644
index 0000000000..e5064c58ec
--- /dev/null
+++ b/src/test/test_sandbox.c
@@ -0,0 +1,348 @@
+/* Copyright (c) 2021, The Tor Project, Inc. */
+/* See LICENSE for licensing information */
+
+#ifndef _LARGEFILE64_SOURCE
+/**
+ * Temporarily required for O_LARGEFILE flag. Needs to be removed
+ * with the libevent fix.
+ */
+#define _LARGEFILE64_SOURCE
+#endif /* !defined(_LARGEFILE64_SOURCE) */
+
+#include "orconfig.h"
+
+#include "lib/sandbox/sandbox.h"
+
+#ifdef USE_LIBSECCOMP
+
+#include <dirent.h>
+#ifdef HAVE_FCNTL_H
+#include <fcntl.h>
+#endif
+#ifdef HAVE_SYS_STAT_H
+#include <sys/stat.h>
+#endif
+#ifdef HAVE_UNISTD_H
+#include <unistd.h>
+#endif
+
+#include "core/or/or.h"
+
+#include "test/test.h"
+#include "test/log_test_helpers.h"
+
+typedef struct {
+ sandbox_cfg_t *cfg;
+
+ char *file_ops_allowed;
+ char *file_ops_blocked;
+
+ char *file_rename_target_allowed;
+
+ char *dir_ops_allowed;
+ char *dir_ops_blocked;
+} sandbox_data_t;
+
+/* All tests are skipped when coverage support is enabled (see further below)
+ * as the sandbox interferes with the use of gcov. Prevent a compiler warning
+ * by omitting these definitions in that case. */
+#ifndef ENABLE_COVERAGE
+static void *
+setup_sandbox(const struct testcase_t *testcase)
+{
+ sandbox_data_t *data = tor_malloc_zero(sizeof(*data));
+
+ (void)testcase;
+
+ /* Establish common file and directory names within the test suite's
+ * temporary directory. */
+ data->file_ops_allowed = tor_strdup(get_fname("file_ops_allowed"));
+ data->file_ops_blocked = tor_strdup(get_fname("file_ops_blocked"));
+
+ data->file_rename_target_allowed =
+ tor_strdup(get_fname("file_rename_target_allowed"));
+
+ data->dir_ops_allowed = tor_strdup(get_fname("dir_ops_allowed"));
+ data->dir_ops_blocked = tor_strdup(get_fname("dir_ops_blocked"));
+
+ /* Create the corresponding filesystem objects. */
+ creat(data->file_ops_allowed, S_IRWXU);
+ creat(data->file_ops_blocked, S_IRWXU);
+ mkdir(data->dir_ops_allowed, S_IRWXU);
+ mkdir(data->dir_ops_blocked, S_IRWXU);
+
+ /* Create the sandbox configuration. */
+ data->cfg = sandbox_cfg_new();
+
+ sandbox_cfg_allow_open_filename(&data->cfg,
+ tor_strdup(data->file_ops_allowed));
+ sandbox_cfg_allow_open_filename(&data->cfg,
+ tor_strdup(data->dir_ops_allowed));
+
+ sandbox_cfg_allow_chmod_filename(&data->cfg,
+ tor_strdup(data->file_ops_allowed));
+ sandbox_cfg_allow_chmod_filename(&data->cfg,
+ tor_strdup(data->dir_ops_allowed));
+ sandbox_cfg_allow_chown_filename(&data->cfg,
+ tor_strdup(data->file_ops_allowed));
+ sandbox_cfg_allow_chown_filename(&data->cfg,
+ tor_strdup(data->dir_ops_allowed));
+
+ sandbox_cfg_allow_rename(&data->cfg, tor_strdup(data->file_ops_allowed),
+ tor_strdup(data->file_rename_target_allowed));
+
+ sandbox_cfg_allow_openat_filename(&data->cfg,
+ tor_strdup(data->dir_ops_allowed));
+
+ sandbox_cfg_allow_opendir_dirname(&data->cfg,
+ tor_strdup(data->dir_ops_allowed));
+
+ sandbox_cfg_allow_stat_filename(&data->cfg,
+ tor_strdup(data->file_ops_allowed));
+ sandbox_cfg_allow_stat_filename(&data->cfg,
+ tor_strdup(data->dir_ops_allowed));
+
+ /* Activate the sandbox, which will remain in effect until the process
+ * terminates. */
+ sandbox_init(data->cfg);
+
+ return data;
+}
+
+static int
+cleanup_sandbox(const struct testcase_t *testcase, void *data_)
+{
+ sandbox_data_t *data = data_;
+
+ (void)testcase;
+
+ tor_free(data->dir_ops_blocked);
+ tor_free(data->dir_ops_allowed);
+ tor_free(data->file_rename_target_allowed);
+ tor_free(data->file_ops_blocked);
+ tor_free(data->file_ops_allowed);
+
+ tor_free(data);
+
+ return 1;
+}
+
+static const struct testcase_setup_t sandboxed_testcase_setup = {
+ .setup_fn = setup_sandbox,
+ .cleanup_fn = cleanup_sandbox
+};
+#endif /* !defined(ENABLE_COVERAGE) */
+
+static void
+test_sandbox_is_active(void *ignored)
+{
+ (void)ignored;
+
+ tt_assert(!sandbox_is_active());
+
+ sandbox_init(sandbox_cfg_new());
+ tt_assert(sandbox_is_active());
+
+ done:
+ (void)0;
+}
+
+static void
+test_sandbox_open_filename(void *arg)
+{
+ sandbox_data_t *data = arg;
+ int fd, errsv;
+
+ fd = open(sandbox_intern_string(data->file_ops_allowed), O_RDONLY);
+ if (fd == -1)
+ tt_abort_perror("open");
+ close(fd);
+
+ /* It might be nice to use sandbox_intern_string() in the line below as well
+ * (and likewise in the test cases that follow) but this would require
+ * capturing the warning message it logs, and the mechanism for doing so
+ * relies on system calls that are normally blocked by the sandbox and may
+ * vary across architectures. */
+ fd = open(data->file_ops_blocked, O_RDONLY);
+ errsv = errno;
+ tt_int_op(fd, OP_EQ, -1);
+ tt_int_op(errsv, OP_EQ, EPERM);
+
+ done:
+ if (fd >= 0)
+ close(fd);
+}
+
+static void
+test_sandbox_chmod_filename(void *arg)
+{
+ sandbox_data_t *data = arg;
+ int rc, errsv;
+
+ if (chmod(sandbox_intern_string(data->file_ops_allowed),
+ S_IRUSR | S_IWUSR) != 0)
+ tt_abort_perror("chmod");
+
+ rc = chmod(data->file_ops_blocked, S_IRUSR | S_IWUSR);
+ errsv = errno;
+ tt_int_op(rc, OP_EQ, -1);
+ tt_int_op(errsv, OP_EQ, EPERM);
+
+ done:
+ (void)0;
+}
+
+static void
+test_sandbox_chown_filename(void *arg)
+{
+ sandbox_data_t *data = arg;
+ int rc, errsv;
+
+ if (chown(sandbox_intern_string(data->file_ops_allowed), -1, -1) != 0)
+ tt_abort_perror("chown");
+
+ rc = chown(data->file_ops_blocked, -1, -1);
+ errsv = errno;
+ tt_int_op(rc, OP_EQ, -1);
+ tt_int_op(errsv, OP_EQ, EPERM);
+
+ done:
+ (void)0;
+}
+
+static void
+test_sandbox_rename_filename(void *arg)
+{
+ sandbox_data_t *data = arg;
+ const char *fname_old = sandbox_intern_string(data->file_ops_allowed),
+ *fname_new = sandbox_intern_string(data->file_rename_target_allowed);
+ int rc, errsv;
+
+ if (rename(fname_old, fname_new) != 0)
+ tt_abort_perror("rename");
+
+ rc = rename(fname_new, fname_old);
+ errsv = errno;
+ tt_int_op(rc, OP_EQ, -1);
+ tt_int_op(errsv, OP_EQ, EPERM);
+
+ done:
+ (void)0;
+}
+
+static void
+test_sandbox_openat_filename(void *arg)
+{
+ sandbox_data_t *data = arg;
+ int flags = O_RDONLY | O_NONBLOCK | O_LARGEFILE | O_DIRECTORY | O_CLOEXEC;
+ int fd, errsv;
+
+ fd = openat(AT_FDCWD, sandbox_intern_string(data->dir_ops_allowed), flags);
+ if (fd < 0)
+ tt_abort_perror("openat");
+ close(fd);
+
+ fd = openat(AT_FDCWD, data->dir_ops_blocked, flags);
+ errsv = errno;
+ tt_int_op(fd, OP_EQ, -1);
+ tt_int_op(errsv, OP_EQ, EPERM);
+
+ done:
+ if (fd >= 0)
+ close(fd);
+}
+
+static void
+test_sandbox_opendir_dirname(void *arg)
+{
+ sandbox_data_t *data = arg;
+ DIR *dir;
+ int errsv;
+
+ dir = opendir(sandbox_intern_string(data->dir_ops_allowed));
+ if (dir == NULL)
+ tt_abort_perror("opendir");
+ closedir(dir);
+
+ dir = opendir(data->dir_ops_blocked);
+ errsv = errno;
+ tt_ptr_op(dir, OP_EQ, NULL);
+ tt_int_op(errsv, OP_EQ, EPERM);
+
+ done:
+ if (dir)
+ closedir(dir);
+}
+
+static void
+test_sandbox_stat_filename(void *arg)
+{
+ sandbox_data_t *data = arg;
+ struct stat st;
+
+ if (stat(sandbox_intern_string(data->file_ops_allowed), &st) != 0)
+ tt_abort_perror("stat");
+
+ int rc = stat(data->file_ops_blocked, &st);
+ int errsv = errno;
+ tt_int_op(rc, OP_EQ, -1);
+ tt_int_op(errsv, OP_EQ, EPERM);
+
+ done:
+ (void)0;
+}
+
+#define SANDBOX_TEST_SKIPPED(name) \
+ { #name, test_sandbox_ ## name, TT_SKIP, NULL, NULL }
+
+/* Skip all tests when coverage support is enabled, as the sandbox interferes
+ * with gcov and prevents it from producing any results. */
+#ifdef ENABLE_COVERAGE
+#define SANDBOX_TEST(name, flags) SANDBOX_TEST_SKIPPED(name)
+#define SANDBOX_TEST_IN_SANDBOX(name) SANDBOX_TEST_SKIPPED(name)
+#else
+#define SANDBOX_TEST(name, flags) \
+ { #name, test_sandbox_ ## name, flags, NULL, NULL }
+#define SANDBOX_TEST_IN_SANDBOX(name) \
+ { #name, test_sandbox_ ## name, TT_FORK, &sandboxed_testcase_setup, NULL }
+#endif /* defined(ENABLE_COVERAGE) */
+
+struct testcase_t sandbox_tests[] = {
+ SANDBOX_TEST(is_active, TT_FORK),
+
+/* When Tor is built with fragile compiler-hardening the sandbox is unable to
+ * filter requests to open files or directories (on systems where glibc uses
+ * the "open" system call to provide this functionality), as doing so would
+ * interfere with the address sanitizer as it retrieves information about the
+ * running process via the filesystem. Skip these tests in that case as the
+ * corresponding functions are likely to have no effect and this will cause the
+ * tests to fail. */
+#ifdef ENABLE_FRAGILE_HARDENING
+ SANDBOX_TEST_SKIPPED(open_filename),
+ SANDBOX_TEST_SKIPPED(opendir_dirname),
+#else
+ SANDBOX_TEST_IN_SANDBOX(open_filename),
+ SANDBOX_TEST_IN_SANDBOX(opendir_dirname),
+#endif /* defined(ENABLE_FRAGILE_HARDENING) */
+
+ SANDBOX_TEST_IN_SANDBOX(openat_filename),
+ SANDBOX_TEST_IN_SANDBOX(chmod_filename),
+ SANDBOX_TEST_IN_SANDBOX(chown_filename),
+ SANDBOX_TEST_IN_SANDBOX(rename_filename),
+
+/* Currently the sandbox is unable to filter stat() calls on systems where
+ * glibc implements this function using the legacy "stat" system call, or where
+ * glibc version 2.33 or later is in use and the newer "newfstatat" syscall is
+ * available.
+ *
+ * Skip testing sandbox_cfg_allow_stat_filename() if it seems the likely the
+ * function will have no effect and the test will therefore not succeed. */
+#if !defined(__NR_newfstatat) && (!defined(__NR_stat) || defined(__NR_stat64))
+ SANDBOX_TEST_IN_SANDBOX(stat_filename),
+#else
+ SANDBOX_TEST_SKIPPED(stat_filename),
+#endif
+ END_OF_TESTCASES
+};
+
+#endif /* defined(USE_SECCOMP) */