diff --git a/Makefile.am b/Makefile.am
index 59051e5f..9c8145e0 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -32,11 +32,13 @@ test_programs = \
$(NULL)
test_scripts = \
tests/test-run.sh \
+ tests/test-seccomp.py \
tests/test-specifying-userns.sh \
tests/test-specifying-pidns.sh \
$(NULL)
test_extra_programs = \
test-bwrap \
+ tests/try-syscall \
$(NULL)
test-bwrap: bwrap
diff --git a/bubblewrap.c b/bubblewrap.c
index f1b7e4bc..c0431589 100644
--- a/bubblewrap.c
+++ b/bubblewrap.c
@@ -161,11 +161,6 @@ struct _LockFile
LockFile *next;
};
-static SetupOp *ops = NULL;
-static SetupOp *last_op = NULL;
-static LockFile *lock_files = NULL;
-static LockFile *last_lock_file = NULL;
-
enum {
PRIV_SEP_OP_DONE,
PRIV_SEP_OP_BIND_MOUNT,
@@ -186,38 +181,104 @@ typedef struct
uint32_t arg2_offset;
} PrivSepOp;
+/*
+ * DEFINE_LINKED_LIST:
+ * @Type: A struct with a `Type *next` member
+ * @name: Used to form the names of variables and functions
+ *
+ * Define a global linked list of @Type structures, with pointers
+ * `NAMEs` to the head of the list and `last_NAME` to the tail of the
+ * list.
+ *
+ * A new zero-filled item can be allocated and appended to the list
+ * by calling `_NAME_append_new()`, which returns the new item.
+ */
+#define DEFINE_LINKED_LIST(Type, name) \
+static Type *name ## s = NULL; \
+static Type *last_ ## name = NULL; \
+\
+static inline Type * \
+_ ## name ## _append_new (void) \
+{ \
+ Type *self = xcalloc (sizeof (Type)); \
+\
+ if (last_ ## name != NULL) \
+ last_ ## name ->next = self; \
+ else \
+ name ## s = self; \
+\
+ last_ ## name = self; \
+ return self; \
+}
+
+DEFINE_LINKED_LIST (SetupOp, op)
+
static SetupOp *
setup_op_new (SetupOpType type)
{
- SetupOp *op = xcalloc (sizeof (SetupOp));
+ SetupOp *op = _op_append_new ();
op->type = type;
op->fd = -1;
op->flags = 0;
- if (last_op != NULL)
- last_op->next = op;
- else
- ops = op;
-
- last_op = op;
return op;
}
+DEFINE_LINKED_LIST (LockFile, lock_file)
+
static LockFile *
lock_file_new (const char *path)
{
- LockFile *lock = xcalloc (sizeof (LockFile));
+ LockFile *lock = _lock_file_append_new ();
lock->path = path;
- if (last_lock_file != NULL)
- last_lock_file->next = lock;
- else
- lock_files = lock;
-
- last_lock_file = lock;
return lock;
}
+typedef struct _SeccompProgram SeccompProgram;
+
+struct _SeccompProgram
+{
+ struct sock_fprog program;
+ SeccompProgram *next;
+};
+
+DEFINE_LINKED_LIST (SeccompProgram, seccomp_program)
+
+static SeccompProgram *
+seccomp_program_new (int *fd)
+{
+ SeccompProgram *self = _seccomp_program_append_new ();
+ cleanup_free char *data = NULL;
+ size_t len;
+
+ data = load_file_data (*fd, &len);
+
+ if (data == NULL)
+ die_with_error ("Can't read seccomp data");
+
+ close (*fd);
+ *fd = -1;
+
+ if (len % 8 != 0)
+ die ("Invalid seccomp data, must be multiple of 8");
+
+ self->program.len = len / 8;
+ self->program.filter = (struct sock_filter *) steal_pointer (&data);
+ return self;
+}
+
+static void
+seccomp_programs_apply (void)
+{
+ SeccompProgram *program;
+
+ for (program = seccomp_programs; program != NULL; program = program->next)
+ {
+ if (prctl (PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &program->program) != 0)
+ die_with_error ("prctl(PR_SET_SECCOMP)");
+ }
+}
static void
usage (int ecode, FILE *out)
@@ -268,7 +329,8 @@ usage (int ecode, FILE *out)
" --bind-data FD DEST Copy from FD to file which is bind-mounted on DEST\n"
" --ro-bind-data FD DEST Copy from FD to file which is readonly bind-mounted on DEST\n"
" --symlink SRC DEST Create symlink at DEST with target SRC\n"
- " --seccomp FD Load and use seccomp rules from FD\n"
+ " --seccomp FD Load and use seccomp rules from FD (not repeatable)\n"
+ " --add-seccomp FD Load and use seccomp rules from FD (repeatable)\n"
" --block-fd FD Block on FD until some data to read is available\n"
" --userns-block-fd FD Block on FD until the user namespace is ready\n"
" --info-fd FD Write information about the running container to FD\n"
@@ -502,7 +564,7 @@ monitor_child (int event_fd, pid_t child_pid, int setup_finished_fd)
* When there are no other processes in the sandbox the wait will return
* ECHILD, and we then exit pid 1 to clean up the sandbox. */
static int
-do_init (int event_fd, pid_t initial_pid, struct sock_fprog *seccomp_prog)
+do_init (int event_fd, pid_t initial_pid)
{
int initial_exit_status = 1;
LockFile *lock;
@@ -530,9 +592,7 @@ do_init (int event_fd, pid_t initial_pid, struct sock_fprog *seccomp_prog)
/* Optionally bind our lifecycle to that of the caller */
handle_die_with_parent ();
- if (seccomp_prog != NULL &&
- prctl (PR_SET_SECCOMP, SECCOMP_MODE_FILTER, seccomp_prog) != 0)
- die_with_error ("prctl(PR_SET_SECCOMP)");
+ seccomp_programs_apply ();
while (TRUE)
{
@@ -2074,6 +2134,9 @@ parse_args_recurse (int *argcp,
if (argc < 2)
die ("--seccomp takes an argument");
+ if (seccomp_programs != NULL)
+ die ("--seccomp cannot be combined with --add-seccomp-fd");
+
if (opt_seccomp_fd != -1)
warn_only_last_option ("--seccomp");
@@ -2083,6 +2146,27 @@ parse_args_recurse (int *argcp,
opt_seccomp_fd = the_fd;
+ argv += 1;
+ argc -= 1;
+ }
+ else if (strcmp (arg, "--add-seccomp-fd") == 0)
+ {
+ int the_fd;
+ char *endptr;
+
+ if (argc < 2)
+ die ("--add-seccomp-fd takes an argument");
+
+ if (opt_seccomp_fd != -1)
+ die ("--add-seccomp-fd cannot be combined with --seccomp");
+
+ the_fd = strtol (argv[1], &endptr, 10);
+ if (argv[1][0] == 0 || endptr[0] != 0 || the_fd < 0)
+ die ("Invalid fd: %s", argv[1]);
+
+ /* takes ownership of fd */
+ seccomp_program_new (&the_fd);
+
argv += 1;
argc -= 1;
}
@@ -2468,9 +2552,6 @@ main (int argc,
struct stat sbuf;
uint64_t val;
int res UNUSED;
- cleanup_free char *seccomp_data = NULL;
- size_t seccomp_len;
- struct sock_fprog seccomp_prog;
cleanup_free char *args_data = NULL;
int intermediate_pids_sockets[2] = {-1, -1};
@@ -3034,17 +3115,9 @@ main (int argc,
if (opt_seccomp_fd != -1)
{
- seccomp_data = load_file_data (opt_seccomp_fd, &seccomp_len);
- if (seccomp_data == NULL)
- die_with_error ("Can't read seccomp data");
-
- if (seccomp_len % 8 != 0)
- die ("Invalid seccomp data, must be multiple of 8");
-
- seccomp_prog.len = seccomp_len / 8;
- seccomp_prog.filter = (struct sock_filter *) seccomp_data;
-
- close (opt_seccomp_fd);
+ assert (seccomp_programs == NULL);
+ /* takes ownership of fd */
+ seccomp_program_new (&opt_seccomp_fd);
}
umask (old_umask);
@@ -3113,7 +3186,7 @@ main (int argc,
fdwalk (proc_fd, close_extra_fds, dont_close);
}
- return do_init (event_fd, pid, seccomp_data != NULL ? &seccomp_prog : NULL);
+ return do_init (event_fd, pid);
}
}
@@ -3141,9 +3214,7 @@ main (int argc,
/* Should be the last thing before execve() so that filters don't
* need to handle anything above */
- if (seccomp_data != NULL &&
- prctl (PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &seccomp_prog) != 0)
- die_with_error ("prctl(PR_SET_SECCOMP)");
+ seccomp_programs_apply ();
if (setup_finished_pipe[1] != -1)
{
diff --git a/bwrap.xml b/bwrap.xml
index 94107ad3..47681759 100644
--- a/bwrap.xml
+++ b/bwrap.xml
@@ -328,6 +328,23 @@
Load and use seccomp rules from FD.
The rules need to be in the form of a compiled cBPF program,
as generated by seccomp_export_bpf.
+ If this option is given more than once, only the last one is used.
+ Use if multiple seccomp programs
+ are needed.
+
+
+
+
+
+ Load and use seccomp rules from FD.
+ The rules need to be in the form of a compiled cBPF program,
+ as generated by seccomp_export_bpf.
+ This option can be repeated, in which case all the seccomp
+ programs will be loaded in the order given (note that the kernel
+ will evaluate them in reverse order, so the last program on the
+ bwrap command-line is evaluated first). All of them, except
+ possibly the last, must allow use of the PR_SET_SECCOMP prctl.
+ This option cannot be combined with .
diff --git a/tests/test-seccomp.py b/tests/test-seccomp.py
new file mode 100755
index 00000000..3ce02ef8
--- /dev/null
+++ b/tests/test-seccomp.py
@@ -0,0 +1,635 @@
+#!/usr/bin/env python3
+# Copyright 2021 Simon McVittie
+# SPDX-License-Identifier: LGPL-2.0-or-later
+
+import errno
+import logging
+import os
+import subprocess
+import sys
+import tempfile
+import termios
+import unittest
+
+try:
+ import seccomp
+except ImportError:
+ print('1..0 # SKIP cannot import seccomp Python module')
+ sys.exit(0)
+
+
+# This is the @default set from systemd as of 2021-10-11
+DEFAULT_SET = set('''
+brk
+cacheflush
+clock_getres
+clock_getres_time64
+clock_gettime
+clock_gettime64
+clock_nanosleep
+clock_nanosleep_time64
+execve
+exit
+exit_group
+futex
+futex_time64
+get_robust_list
+get_thread_area
+getegid
+getegid32
+geteuid
+geteuid32
+getgid
+getgid32
+getgroups
+getgroups32
+getpgid
+getpgrp
+getpid
+getppid
+getrandom
+getresgid
+getresgid32
+getresuid
+getresuid32
+getrlimit
+getsid
+gettid
+gettimeofday
+getuid
+getuid32
+membarrier
+mmap
+mmap2
+munmap
+nanosleep
+pause
+prlimit64
+restart_syscall
+rseq
+rt_sigreturn
+sched_getaffinity
+sched_yield
+set_robust_list
+set_thread_area
+set_tid_address
+set_tls
+sigreturn
+time
+ugetrlimit
+'''.split())
+
+# This is the @basic-io set from systemd
+BASIC_IO_SET = set('''
+_llseek
+close
+close_range
+dup
+dup2
+dup3
+lseek
+pread64
+preadv
+preadv2
+pwrite64
+pwritev
+pwritev2
+read
+readv
+write
+writev
+'''.split())
+
+# This is the @filesystem-io set from systemd
+FILESYSTEM_SET = set('''
+access
+chdir
+chmod
+close
+creat
+faccessat
+faccessat2
+fallocate
+fchdir
+fchmod
+fchmodat
+fcntl
+fcntl64
+fgetxattr
+flistxattr
+fremovexattr
+fsetxattr
+fstat
+fstat64
+fstatat64
+fstatfs
+fstatfs64
+ftruncate
+ftruncate64
+futimesat
+getcwd
+getdents
+getdents64
+getxattr
+inotify_add_watch
+inotify_init
+inotify_init1
+inotify_rm_watch
+lgetxattr
+link
+linkat
+listxattr
+llistxattr
+lremovexattr
+lsetxattr
+lstat
+lstat64
+mkdir
+mkdirat
+mknod
+mknodat
+newfstatat
+oldfstat
+oldlstat
+oldstat
+open
+openat
+openat2
+readlink
+readlinkat
+removexattr
+rename
+renameat
+renameat2
+rmdir
+setxattr
+stat
+stat64
+statfs
+statfs64
+statx
+symlink
+symlinkat
+truncate
+truncate64
+unlink
+unlinkat
+utime
+utimensat
+utimensat_time64
+utimes
+'''.split())
+
+# Miscellaneous syscalls used during process startup, at least on x86_64
+ALLOWED = DEFAULT_SET | BASIC_IO_SET | FILESYSTEM_SET | set('''
+arch_prctl
+ioctl
+madvise
+mprotect
+mremap
+prctl
+readdir
+umask
+'''.split())
+
+# Syscalls we will try to use, expecting them to be either allowed or
+# blocked by our allow and/or deny lists
+TRY_SYSCALLS = [
+ 'chmod',
+ 'chroot',
+ 'clone3',
+ 'ioctl TIOCNOTTY',
+ 'ioctl TIOCSTI CVE-2019-10063',
+ 'ioctl TIOCSTI',
+ 'listen',
+ 'prctl',
+]
+
+
+class Test(unittest.TestCase):
+ def setUp(self) -> None:
+ here = os.path.dirname(os.path.abspath(__file__))
+
+ if 'G_TEST_SRCDIR' in os.environ:
+ self.test_srcdir = os.getenv('G_TEST_SRCDIR') + '/tests'
+ else:
+ self.test_srcdir = here
+
+ if 'G_TEST_BUILDDIR' in os.environ:
+ self.test_builddir = os.getenv('G_TEST_BUILDDIR') + '/tests'
+ else:
+ self.test_builddir = here
+
+ self.bwrap = os.getenv('BWRAP', 'bwrap')
+ self.try_syscall = os.path.join(self.test_builddir, 'try-syscall')
+
+ completed = subprocess.run(
+ [
+ self.bwrap,
+ '--ro-bind', '/', '/',
+ 'true',
+ ],
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL,
+ stderr=2,
+ )
+
+ if completed.returncode != 0:
+ raise unittest.SkipTest(
+ 'cannot run bwrap (does it need to be setuid?)'
+ )
+
+ def tearDown(self) -> None:
+ pass
+
+ def test_no_seccomp(self) -> None:
+ for syscall in TRY_SYSCALLS:
+ print('# {} without seccomp'.format(syscall))
+ completed = subprocess.run(
+ [
+ self.bwrap,
+ '--ro-bind', '/', '/',
+ self.try_syscall, syscall,
+ ],
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL,
+ stderr=2,
+ )
+
+ if (
+ syscall == 'ioctl TIOCSTI CVE-2019-10063'
+ and completed.returncode == errno.ENOENT
+ ):
+ print('# Cannot test 64-bit syscall parameter on 32-bit')
+ continue
+
+ if syscall == 'clone3':
+ # If the kernel supports it, we didn't block it so
+ # it fails with EFAULT. If the kernel doesn't support it,
+ # it'll fail with ENOSYS instead.
+ self.assertIn(
+ completed.returncode,
+ (errno.ENOSYS, errno.EFAULT),
+ )
+ elif syscall.startswith('ioctl') or syscall == 'listen':
+ self.assertEqual(completed.returncode, errno.EBADF)
+ else:
+ self.assertEqual(completed.returncode, errno.EFAULT)
+
+ def test_seccomp_allowlist(self) -> None:
+ with tempfile.TemporaryFile() as allowlist_temp:
+ allowlist = seccomp.SyscallFilter(seccomp.ERRNO(errno.ENOSYS))
+
+ if os.uname().machine == 'x86_64':
+ # Allow Python and try-syscall to be different word sizes
+ allowlist.add_arch(seccomp.Arch.X86)
+
+ for syscall in ALLOWED:
+ try:
+ allowlist.add_rule(seccomp.ALLOW, syscall)
+ except Exception as e:
+ print('# Cannot add {} to allowlist: {!r}'.format(syscall, e))
+
+ allowlist.export_bpf(allowlist_temp)
+
+ for syscall in TRY_SYSCALLS:
+ print('# allowlist vs. {}'.format(syscall))
+ allowlist_temp.seek(0, os.SEEK_SET)
+
+ completed = subprocess.run(
+ [
+ self.bwrap,
+ '--ro-bind', '/', '/',
+ '--seccomp', str(allowlist_temp.fileno()),
+ self.try_syscall, syscall,
+ ],
+ pass_fds=(allowlist_temp.fileno(),),
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL,
+ stderr=2,
+ )
+
+ if (
+ syscall == 'ioctl TIOCSTI CVE-2019-10063'
+ and completed.returncode == errno.ENOENT
+ ):
+ print('# Cannot test 64-bit syscall parameter on 32-bit')
+ continue
+
+ if syscall.startswith('ioctl'):
+ # We allow this, so it is executed (and in this simple
+ # example, immediately fails)
+ self.assertEqual(completed.returncode, errno.EBADF)
+ elif syscall in ('chroot', 'listen', 'clone3'):
+ # We don't allow these, so they fail with ENOSYS.
+ # clone3 might also be failing with ENOSYS because
+ # the kernel genuinely doesn't support it.
+ self.assertEqual(completed.returncode, errno.ENOSYS)
+ else:
+ # We allow this, so it is executed (and in this simple
+ # example, immediately fails)
+ self.assertEqual(completed.returncode, errno.EFAULT)
+
+ def test_seccomp_denylist(self) -> None:
+ with tempfile.TemporaryFile() as denylist_temp:
+ denylist = seccomp.SyscallFilter(seccomp.ALLOW)
+
+ if os.uname().machine == 'x86_64':
+ # Allow Python and try-syscall to be different word sizes
+ denylist.add_arch(seccomp.Arch.X86)
+
+ # Using ECONNREFUSED here because it's unlikely that any of
+ # these syscalls will legitimately fail with that code, so
+ # if they fail like this, it will be as a result of seccomp.
+ denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'chmod')
+ denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'chroot')
+ denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'prctl')
+ denylist.add_rule(
+ seccomp.ERRNO(errno.ECONNREFUSED), 'ioctl',
+ seccomp.Arg(1, seccomp.MASKED_EQ, 0xffffffff, termios.TIOCSTI),
+ )
+
+ denylist.export_bpf(denylist_temp)
+
+ for syscall in TRY_SYSCALLS:
+ print('# denylist vs. {}'.format(syscall))
+ denylist_temp.seek(0, os.SEEK_SET)
+
+ completed = subprocess.run(
+ [
+ self.bwrap,
+ '--ro-bind', '/', '/',
+ '--seccomp', str(denylist_temp.fileno()),
+ self.try_syscall, syscall,
+ ],
+ pass_fds=(denylist_temp.fileno(),),
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL,
+ stderr=2,
+ )
+
+ if (
+ syscall == 'ioctl TIOCSTI CVE-2019-10063'
+ and completed.returncode == errno.ENOENT
+ ):
+ print('# Cannot test 64-bit syscall parameter on 32-bit')
+ continue
+
+ if syscall == 'clone3':
+ # If the kernel supports it, we didn't block it so
+ # it fails with EFAULT. If the kernel doesn't support it,
+ # it'll fail with ENOSYS instead.
+ self.assertIn(
+ completed.returncode,
+ (errno.ENOSYS, errno.EFAULT),
+ )
+ elif syscall in ('ioctl TIOCNOTTY', 'listen'):
+ # Not on the denylist
+ self.assertEqual(completed.returncode, errno.EBADF)
+ else:
+ # We blocked all of these
+ self.assertEqual(completed.returncode, errno.ECONNREFUSED)
+
+ def test_seccomp_stacked(self, allowlist_first=False) -> None:
+ with tempfile.TemporaryFile(
+ ) as allowlist_temp, tempfile.TemporaryFile(
+ ) as denylist_temp:
+ # This filter is a simplified version of what Flatpak wants
+
+ allowlist = seccomp.SyscallFilter(seccomp.ERRNO(errno.ENOSYS))
+ denylist = seccomp.SyscallFilter(seccomp.ALLOW)
+
+ if os.uname().machine == 'x86_64':
+ # Allow Python and try-syscall to be different word sizes
+ allowlist.add_arch(seccomp.Arch.X86)
+ denylist.add_arch(seccomp.Arch.X86)
+
+ for syscall in ALLOWED:
+ try:
+ allowlist.add_rule(seccomp.ALLOW, syscall)
+ except Exception as e:
+ print('# Cannot add {} to allowlist: {!r}'.format(syscall, e))
+
+ denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'chmod')
+ denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'chroot')
+ denylist.add_rule(
+ seccomp.ERRNO(errno.ECONNREFUSED), 'ioctl',
+ seccomp.Arg(1, seccomp.MASKED_EQ, 0xffffffff, termios.TIOCSTI),
+ )
+
+ # All seccomp programs except the last must allow prctl(),
+ # because otherwise we wouldn't be able to add the remaining
+ # seccomp programs. We document that the last program can
+ # block prctl, so test that.
+ if allowlist_first:
+ denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'prctl')
+
+ allowlist.export_bpf(allowlist_temp)
+ denylist.export_bpf(denylist_temp)
+
+ for syscall in TRY_SYSCALLS:
+ print('# stacked vs. {}'.format(syscall))
+ allowlist_temp.seek(0, os.SEEK_SET)
+ denylist_temp.seek(0, os.SEEK_SET)
+
+ if allowlist_first:
+ fds = [allowlist_temp.fileno(), denylist_temp.fileno()]
+ else:
+ fds = [denylist_temp.fileno(), allowlist_temp.fileno()]
+
+ completed = subprocess.run(
+ [
+ self.bwrap,
+ '--ro-bind', '/', '/',
+ '--add-seccomp-fd', str(fds[0]),
+ '--add-seccomp-fd', str(fds[1]),
+ self.try_syscall, syscall,
+ ],
+ pass_fds=fds,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL,
+ stderr=2,
+ )
+
+ if (
+ syscall == 'ioctl TIOCSTI CVE-2019-10063'
+ and completed.returncode == errno.ENOENT
+ ):
+ print('# Cannot test 64-bit syscall parameter on 32-bit')
+ continue
+
+ if syscall == 'ioctl TIOCNOTTY':
+ # Not denied by the denylist, and allowed by the allowlist
+ self.assertEqual(completed.returncode, errno.EBADF)
+ elif syscall in ('clone3', 'listen'):
+ # We didn't deny these, so the denylist has no effect
+ # and we fall back to the allowlist, which doesn't
+ # include them either.
+ # clone3 might also be failing with ENOSYS because
+ # the kernel genuinely doesn't support it.
+ self.assertEqual(completed.returncode, errno.ENOSYS)
+ elif syscall == 'chroot':
+ # This is denied by the denylist *and* not allowed by
+ # the allowlist. The result depends which one we added
+ # first: the most-recently-added filter "wins".
+ if allowlist_first:
+ self.assertEqual(
+ completed.returncode,
+ errno.ECONNREFUSED,
+ )
+ else:
+ self.assertEqual(completed.returncode, errno.ENOSYS)
+ elif syscall == 'prctl':
+ # We can only put this on the denylist if the denylist
+ # is the last to be added.
+ if allowlist_first:
+ self.assertEqual(
+ completed.returncode,
+ errno.ECONNREFUSED,
+ )
+ else:
+ self.assertEqual(completed.returncode, errno.EFAULT)
+ else:
+ # chmod is allowed by the allowlist but blocked by the
+ # denylist. Denying takes precedence over allowing,
+ # regardless of order.
+ self.assertEqual(completed.returncode, errno.ECONNREFUSED)
+
+ def test_seccomp_stacked_allowlist_first(self) -> None:
+ self.test_seccomp_stacked(allowlist_first=True)
+
+ def test_seccomp_invalid(self) -> None:
+ with tempfile.TemporaryFile(
+ ) as allowlist_temp, tempfile.TemporaryFile(
+ ) as denylist_temp:
+ completed = subprocess.run(
+ [
+ self.bwrap,
+ '--ro-bind', '/', '/',
+ '--add-seccomp-fd', '-1',
+ 'true',
+ ],
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.PIPE,
+ )
+ self.assertIn(b'bwrap: Invalid fd: -1\n', completed.stderr)
+ self.assertEqual(completed.returncode, 1)
+
+ completed = subprocess.run(
+ [
+ self.bwrap,
+ '--ro-bind', '/', '/',
+ '--seccomp', '0a',
+ 'true',
+ ],
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.PIPE,
+ )
+ self.assertIn(b'bwrap: Invalid fd: 0a\n', completed.stderr)
+ self.assertEqual(completed.returncode, 1)
+
+ completed = subprocess.run(
+ [
+ self.bwrap,
+ '--ro-bind', '/', '/',
+ '--add-seccomp-fd', str(denylist_temp.fileno()),
+ '--seccomp', str(allowlist_temp.fileno()),
+ 'true',
+ ],
+ pass_fds=(allowlist_temp.fileno(), denylist_temp.fileno()),
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.PIPE,
+ )
+ self.assertIn(
+ b'bwrap: --seccomp cannot be combined with --add-seccomp-fd\n',
+ completed.stderr,
+ )
+ self.assertEqual(completed.returncode, 1)
+
+ completed = subprocess.run(
+ [
+ self.bwrap,
+ '--ro-bind', '/', '/',
+ '--seccomp', str(allowlist_temp.fileno()),
+ '--add-seccomp-fd', str(denylist_temp.fileno()),
+ 'true',
+ ],
+ pass_fds=(allowlist_temp.fileno(), denylist_temp.fileno()),
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.PIPE,
+ )
+ self.assertIn(
+ b'--add-seccomp-fd cannot be combined with --seccomp',
+ completed.stderr,
+ )
+ self.assertEqual(completed.returncode, 1)
+
+ completed = subprocess.run(
+ [
+ self.bwrap,
+ '--ro-bind', '/', '/',
+ '--add-seccomp-fd', str(allowlist_temp.fileno()),
+ '--add-seccomp-fd', str(allowlist_temp.fileno()),
+ 'true',
+ ],
+ pass_fds=(allowlist_temp.fileno(), allowlist_temp.fileno()),
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.PIPE,
+ )
+ self.assertIn(
+ b"bwrap: Can't read seccomp data: ",
+ completed.stderr,
+ )
+ self.assertEqual(completed.returncode, 1)
+
+ allowlist_temp.write(b'\x01')
+ allowlist_temp.seek(0, os.SEEK_SET)
+ completed = subprocess.run(
+ [
+ self.bwrap,
+ '--ro-bind', '/', '/',
+ '--add-seccomp-fd', str(denylist_temp.fileno()),
+ '--add-seccomp-fd', str(allowlist_temp.fileno()),
+ 'true',
+ ],
+ pass_fds=(allowlist_temp.fileno(), denylist_temp.fileno()),
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.PIPE,
+ )
+ self.assertIn(
+ b'bwrap: Invalid seccomp data, must be multiple of 8\n',
+ completed.stderr,
+ )
+ self.assertEqual(completed.returncode, 1)
+
+
+def main():
+ logging.basicConfig(level=logging.DEBUG)
+
+ try:
+ from tap.runner import TAPTestRunner
+ except ImportError:
+ TAPTestRunner = None # type: ignore
+
+ if TAPTestRunner is not None:
+ runner = TAPTestRunner()
+ runner.set_stream(True)
+ unittest.main(testRunner=runner)
+ else:
+ print('# tap.runner not available, using simple TAP output')
+ print('1..1')
+ program = unittest.main(exit=False)
+ if program.result.wasSuccessful():
+ print('ok 1 - %r' % program.result)
+ else:
+ print('not ok 1 - %r' % program.result)
+ sys.exit(1)
+
+if __name__ == '__main__':
+ main()
diff --git a/tests/try-syscall.c b/tests/try-syscall.c
new file mode 100644
index 00000000..df350542
--- /dev/null
+++ b/tests/try-syscall.c
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2021 Simon McVittie
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ *
+ * Try one or more system calls that might have been blocked by a
+ * seccomp filter. Return the last value of errno seen.
+ *
+ * In general, we pass a bad fd or pointer to each syscall that will
+ * accept one, so that it will fail with EBADF or EFAULT without side-effects.
+ *
+ * This helper is used for regression tests in both bubblewrap and flatpak.
+ * Please keep both copies in sync.
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#if defined(_MIPS_SIM)
+# if _MIPS_SIM == _MIPS_SIM_ABI32
+# define MISSING_SYSCALL_BASE 4000
+# elif _MIPS_SIM == _MIPS_SIM_ABI64
+# define MISSING_SYSCALL_BASE 5000
+# elif _MIPS_SIM == _MIPS_SIM_NABI32
+# define MISSING_SYSCALL_BASE 6000
+# else
+# error "Unknown MIPS ABI"
+# endif
+#endif
+
+#if defined(__ia64__)
+# define MISSING_SYSCALL_BASE 1024
+#endif
+
+#if defined(__alpha__)
+# define MISSING_SYSCALL_BASE 110
+#endif
+
+#if defined(__x86_64__) && defined(__ILP32__)
+# define MISSING_SYSCALL_BASE 0x40000000
+#endif
+
+/*
+ * MISSING_SYSCALL_BASE:
+ *
+ * Number to add to the syscall numbers of recently-added syscalls
+ * to get the appropriate syscall for the current ABI.
+ */
+#ifndef MISSING_SYSCALL_BASE
+# define MISSING_SYSCALL_BASE 0
+#endif
+
+#ifndef __NR_clone3
+# define __NR_clone3 (MISSING_SYSCALL_BASE + 435)
+#endif
+
+/*
+ * The size of clone3's parameter (as of 2021)
+ */
+#define SIZEOF_STRUCT_CLONE_ARGS ((size_t) 88)
+
+/*
+ * An invalid pointer that will cause syscalls to fail with EFAULT
+ */
+#define WRONG_POINTER ((char *) 1)
+
+int
+main (int argc, char **argv)
+{
+ int errsv = 0;
+ int i;
+
+ for (i = 1; i < argc; i++)
+ {
+ const char *arg = argv[i];
+
+ if (strcmp (arg, "print-errno-values") == 0)
+ {
+ printf ("EBADF=%d\n", EBADF);
+ printf ("EFAULT=%d\n", EFAULT);
+ printf ("ENOENT=%d\n", ENOENT);
+ printf ("ENOSYS=%d\n", ENOSYS);
+ printf ("EPERM=%d\n", EPERM);
+ }
+ else if (strcmp (arg, "chmod") == 0)
+ {
+ /* If not blocked by seccomp, this will fail with EFAULT */
+ if (chmod (WRONG_POINTER, 0700) != 0)
+ {
+ errsv = errno;
+ perror (arg);
+ }
+ }
+ else if (strcmp (arg, "chroot") == 0)
+ {
+ /* If not blocked by seccomp, this will fail with EFAULT */
+ if (chroot (WRONG_POINTER) != 0)
+ {
+ errsv = errno;
+ perror (arg);
+ }
+ }
+ else if (strcmp (arg, "clone3") == 0)
+ {
+ /* If not blocked by seccomp, this will fail with EFAULT */
+ if (syscall (__NR_clone3, WRONG_POINTER, SIZEOF_STRUCT_CLONE_ARGS) != 0)
+ {
+ errsv = errno;
+ perror (arg);
+ }
+ }
+ else if (strcmp (arg, "ioctl TIOCNOTTY") == 0)
+ {
+ /* If not blocked by seccomp, this will fail with EBADF */
+ if (ioctl (-1, TIOCNOTTY) != 0)
+ {
+ errsv = errno;
+ perror (arg);
+ }
+ }
+ else if (strcmp (arg, "ioctl TIOCSTI") == 0)
+ {
+ /* If not blocked by seccomp, this will fail with EBADF */
+ if (ioctl (-1, TIOCSTI, WRONG_POINTER) != 0)
+ {
+ errsv = errno;
+ perror (arg);
+ }
+ }
+#ifdef __LP64__
+ else if (strcmp (arg, "ioctl TIOCSTI CVE-2019-10063") == 0)
+ {
+ unsigned long not_TIOCSTI = (0x123UL << 32) | (unsigned long) TIOCSTI;
+
+ /* If not blocked by seccomp, this will fail with EBADF */
+ if (syscall (__NR_ioctl, -1, not_TIOCSTI, WRONG_POINTER) != 0)
+ {
+ errsv = errno;
+ perror (arg);
+ }
+ }
+#endif
+ else if (strcmp (arg, "listen") == 0)
+ {
+ /* If not blocked by seccomp, this will fail with EBADF */
+ if (listen (-1, 42) != 0)
+ {
+ errsv = errno;
+ perror (arg);
+ }
+ }
+ else if (strcmp (arg, "prctl") == 0)
+ {
+ /* If not blocked by seccomp, this will fail with EFAULT */
+ if (prctl (PR_GET_CHILD_SUBREAPER, WRONG_POINTER, 0, 0, 0) != 0)
+ {
+ errsv = errno;
+ perror (arg);
+ }
+ }
+ else
+ {
+ fprintf (stderr, "Unsupported syscall \"%s\"\n", arg);
+ errsv = ENOENT;
+ }
+ }
+
+ return errsv;
+}