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; +}