Merge pull request #459 from smcv/multiple-seccomp

Allow loading more than one seccomp program
This commit is contained in:
Simon McVittie
2022-01-31 17:15:23 +00:00
committed by GitHub
5 changed files with 943 additions and 42 deletions

View File

@@ -32,11 +32,13 @@ test_programs = \
$(NULL) $(NULL)
test_scripts = \ test_scripts = \
tests/test-run.sh \ tests/test-run.sh \
tests/test-seccomp.py \
tests/test-specifying-userns.sh \ tests/test-specifying-userns.sh \
tests/test-specifying-pidns.sh \ tests/test-specifying-pidns.sh \
$(NULL) $(NULL)
test_extra_programs = \ test_extra_programs = \
test-bwrap \ test-bwrap \
tests/try-syscall \
$(NULL) $(NULL)
test-bwrap: bwrap test-bwrap: bwrap

View File

@@ -161,11 +161,6 @@ struct _LockFile
LockFile *next; LockFile *next;
}; };
static SetupOp *ops = NULL;
static SetupOp *last_op = NULL;
static LockFile *lock_files = NULL;
static LockFile *last_lock_file = NULL;
enum { enum {
PRIV_SEP_OP_DONE, PRIV_SEP_OP_DONE,
PRIV_SEP_OP_BIND_MOUNT, PRIV_SEP_OP_BIND_MOUNT,
@@ -186,38 +181,104 @@ typedef struct
uint32_t arg2_offset; uint32_t arg2_offset;
} PrivSepOp; } 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 * static SetupOp *
setup_op_new (SetupOpType type) setup_op_new (SetupOpType type)
{ {
SetupOp *op = xcalloc (sizeof (SetupOp)); SetupOp *op = _op_append_new ();
op->type = type; op->type = type;
op->fd = -1; op->fd = -1;
op->flags = 0; op->flags = 0;
if (last_op != NULL)
last_op->next = op;
else
ops = op;
last_op = op;
return op; return op;
} }
DEFINE_LINKED_LIST (LockFile, lock_file)
static LockFile * static LockFile *
lock_file_new (const char *path) lock_file_new (const char *path)
{ {
LockFile *lock = xcalloc (sizeof (LockFile)); LockFile *lock = _lock_file_append_new ();
lock->path = path; lock->path = path;
if (last_lock_file != NULL)
last_lock_file->next = lock;
else
lock_files = lock;
last_lock_file = lock;
return 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 static void
usage (int ecode, FILE *out) 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" " --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" " --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" " --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" " --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" " --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" " --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 * 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. */ * ECHILD, and we then exit pid 1 to clean up the sandbox. */
static int 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; int initial_exit_status = 1;
LockFile *lock; 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 */ /* Optionally bind our lifecycle to that of the caller */
handle_die_with_parent (); handle_die_with_parent ();
if (seccomp_prog != NULL && seccomp_programs_apply ();
prctl (PR_SET_SECCOMP, SECCOMP_MODE_FILTER, seccomp_prog) != 0)
die_with_error ("prctl(PR_SET_SECCOMP)");
while (TRUE) while (TRUE)
{ {
@@ -2074,6 +2134,9 @@ parse_args_recurse (int *argcp,
if (argc < 2) if (argc < 2)
die ("--seccomp takes an argument"); die ("--seccomp takes an argument");
if (seccomp_programs != NULL)
die ("--seccomp cannot be combined with --add-seccomp-fd");
if (opt_seccomp_fd != -1) if (opt_seccomp_fd != -1)
warn_only_last_option ("--seccomp"); warn_only_last_option ("--seccomp");
@@ -2083,6 +2146,27 @@ parse_args_recurse (int *argcp,
opt_seccomp_fd = the_fd; 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; argv += 1;
argc -= 1; argc -= 1;
} }
@@ -2468,9 +2552,6 @@ main (int argc,
struct stat sbuf; struct stat sbuf;
uint64_t val; uint64_t val;
int res UNUSED; int res UNUSED;
cleanup_free char *seccomp_data = NULL;
size_t seccomp_len;
struct sock_fprog seccomp_prog;
cleanup_free char *args_data = NULL; cleanup_free char *args_data = NULL;
int intermediate_pids_sockets[2] = {-1, -1}; int intermediate_pids_sockets[2] = {-1, -1};
@@ -3034,17 +3115,9 @@ main (int argc,
if (opt_seccomp_fd != -1) if (opt_seccomp_fd != -1)
{ {
seccomp_data = load_file_data (opt_seccomp_fd, &seccomp_len); assert (seccomp_programs == NULL);
if (seccomp_data == NULL) /* takes ownership of fd */
die_with_error ("Can't read seccomp data"); seccomp_program_new (&opt_seccomp_fd);
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);
} }
umask (old_umask); umask (old_umask);
@@ -3113,7 +3186,7 @@ main (int argc,
fdwalk (proc_fd, close_extra_fds, dont_close); 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 /* Should be the last thing before execve() so that filters don't
* need to handle anything above */ * need to handle anything above */
if (seccomp_data != NULL && seccomp_programs_apply ();
prctl (PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &seccomp_prog) != 0)
die_with_error ("prctl(PR_SET_SECCOMP)");
if (setup_finished_pipe[1] != -1) if (setup_finished_pipe[1] != -1)
{ {

View File

@@ -328,6 +328,23 @@
Load and use seccomp rules from <arg choice="plain">FD</arg>. Load and use seccomp rules from <arg choice="plain">FD</arg>.
The rules need to be in the form of a compiled cBPF program, The rules need to be in the form of a compiled cBPF program,
as generated by seccomp_export_bpf. as generated by seccomp_export_bpf.
If this option is given more than once, only the last one is used.
Use <option>--add-seccomp-fd</option> if multiple seccomp programs
are needed.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--add-seccomp-fd <arg choice="plain">FD</arg></option></term>
<listitem><para>
Load and use seccomp rules from <arg choice="plain">FD</arg>.
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 <option>--seccomp</option>.
</para></listitem> </para></listitem>
</varlistentry> </varlistentry>
<varlistentry> <varlistentry>

635
tests/test-seccomp.py Executable file
View File

@@ -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()

176
tests/try-syscall.c Normal file
View File

@@ -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 <errno.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/stat.h>
#include <sys/types.h>
#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;
}