feat(linux-sandbox): vendor bubblewrap and wire it with FFI (#10413)

## Summary

Vendor Bubblewrap into the repo and add minimal build plumbing in
`codex-linux-sandbox` to compile/link it.

## Why

We want to move Linux sandboxing toward Bubblewrap, but in a safe
two-step rollout:
1) vendoring/build setup (this PR),  
2) runtime integration (follow-up PR).

## Included

- Add `codex-rs/vendor/bubblewrap` sources.
- Add build-time FFI path in `codex-rs/linux-sandbox`.
- Update `build.rs` rerun tracking for vendored files.
- Small vendored compile warning fix (`sockaddr_nl` full init).

follow up in https://github.com/openai/codex/pull/9938
This commit is contained in:
viyatb-oai
2026-02-02 23:33:46 -08:00
committed by GitHub
parent 53d8474061
commit f956cc2a02
57 changed files with 11261 additions and 6 deletions

View File

@@ -0,0 +1,190 @@
# Core source library for shell script tests; the
# canonical version lives in:
#
# https://github.com/ostreedev/ostree
#
# Known copies are in the following repos:
#
# - https://github.com/containers/bubblewrap
# - https://github.com/coreos/rpm-ostree
#
# Copyright (C) 2017 Colin Walters <walters@verbum.org>
#
# SPDX-License-Identifier: LGPL-2.0-or-later
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.
fatal() {
echo $@ 1>&2; exit 1
}
# fatal() is shorter to type, but retain this alias
assert_not_reached () {
fatal "$@"
}
# Some tests look for specific English strings. Use a UTF-8 version
# of the C (POSIX) locale if we have one, or fall back to en_US.UTF-8
# (https://sourceware.org/glibc/wiki/Proposals/C.UTF-8)
#
# If we can't find the locale command assume we have support for C.UTF-8
# (e.g. musl based systems)
if type -p locale >/dev/null; then
export LC_ALL=$(locale -a | grep -iEe '^(C|en_US)\.(UTF-8|utf8)$' | head -n1 || true)
if [ -z "${LC_ALL}" ]; then fatal "Can't find suitable UTF-8 locale"; fi
else
export LC_ALL=C.UTF-8
fi
# A GNU extension, used whenever LC_ALL is not C
unset LANGUAGE
# This should really be the default IMO
export G_DEBUG=fatal-warnings
assert_streq () {
test "$1" = "$2" || fatal "$1 != $2"
}
assert_str_match () {
if ! echo "$1" | grep -E -q "$2"; then
fatal "$1 does not match regexp $2"
fi
}
assert_not_streq () {
(! test "$1" = "$2") || fatal "$1 == $2"
}
assert_has_file () {
test -f "$1" || fatal "Couldn't find '$1'"
}
assert_has_dir () {
test -d "$1" || fatal "Couldn't find '$1'"
}
# Dump ls -al + file contents to stderr, then fatal()
_fatal_print_file() {
file="$1"
shift
ls -al "$file" >&2
sed -e 's/^/# /' < "$file" >&2
fatal "$@"
}
_fatal_print_files() {
file1="$1"
shift
file2="$1"
shift
ls -al "$file1" >&2
sed -e 's/^/# /' < "$file1" >&2
ls -al "$file2" >&2
sed -e 's/^/# /' < "$file2" >&2
fatal "$@"
}
assert_not_has_file () {
if test -f "$1"; then
_fatal_print_file "$1" "File '$1' exists"
fi
}
assert_not_file_has_content () {
fpath=$1
shift
for re in "$@"; do
if grep -q -e "$re" "$fpath"; then
_fatal_print_file "$fpath" "File '$fpath' matches regexp '$re'"
fi
done
}
assert_not_has_dir () {
if test -d "$1"; then
fatal "Directory '$1' exists"
fi
}
assert_file_has_content () {
fpath=$1
shift
for re in "$@"; do
if ! grep -q -e "$re" "$fpath"; then
_fatal_print_file "$fpath" "File '$fpath' doesn't match regexp '$re'"
fi
done
}
assert_file_has_content_once () {
fpath=$1
shift
for re in "$@"; do
if ! test $(grep -e "$re" "$fpath" | wc -l) = "1"; then
_fatal_print_file "$fpath" "File '$fpath' doesn't match regexp '$re' exactly once"
fi
done
}
assert_file_has_content_literal () {
fpath=$1; shift
for s in "$@"; do
if ! grep -q -F -e "$s" "$fpath"; then
_fatal_print_file "$fpath" "File '$fpath' doesn't match fixed string list '$s'"
fi
done
}
assert_file_has_mode () {
mode=$(stat -c '%a' $1)
if [ "$mode" != "$2" ]; then
fatal "File '$1' has wrong mode: expected $2, but got $mode"
fi
}
assert_symlink_has_content () {
if ! test -L "$1"; then
fatal "File '$1' is not a symbolic link"
fi
if ! readlink "$1" | grep -q -e "$2"; then
_fatal_print_file "$1" "Symbolic link '$1' doesn't match regexp '$2'"
fi
}
assert_file_empty() {
if test -s "$1"; then
_fatal_print_file "$1" "File '$1' is not empty"
fi
}
assert_files_equal() {
if ! cmp "$1" "$2"; then
_fatal_print_files "$1" "$2" "File '$1' and '$2' is not equal"
fi
}
# Use to skip all of these tests
skip() {
echo "1..0 # SKIP" "$@"
exit 0
}
report_err () {
local exit_status="$?"
{ { local BASH_XTRACEFD=3; } 2> /dev/null
echo "Unexpected nonzero exit status $exit_status while running: $BASH_COMMAND" >&2
} 3> /dev/null
}
trap report_err ERR

View File

@@ -0,0 +1,115 @@
# shellcheck shell=bash
# Source library for shell script tests.
# Add non-bubblewrap-specific code to libtest-core.sh instead.
#
# Copyright (C) 2017 Colin Walters <walters@verbum.org>
# SPDX-License-Identifier: LGPL-2.0-or-later
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.
set -e
if [ -n "${G_TEST_SRCDIR:-}" ]; then
test_srcdir="${G_TEST_SRCDIR}/tests"
else
test_srcdir=$(dirname "$0")
fi
if [ -n "${G_TEST_BUILDDIR:-}" ]; then
test_builddir="${G_TEST_BUILDDIR}/tests"
else
test_builddir=$(dirname "$0")
fi
. "${test_srcdir}/libtest-core.sh"
# Make sure /sbin/getpcaps etc. are in our PATH even if non-root
PATH="$PATH:/usr/sbin:/sbin"
tempdir=$(mktemp -d /var/tmp/tap-test.XXXXXX)
touch "${tempdir}/.testtmp"
cleanup() {
if test -n "${TEST_SKIP_CLEANUP:-}"; then
echo "Skipping cleanup of ${tempdir}"
elif test -f "${tempdir}/.testtmp"; then
rm -rf "${tempdir}"
fi
}
trap cleanup EXIT
cd "${tempdir}"
: "${BWRAP:=bwrap}"
if test -u "$(type -p ${BWRAP})"; then
bwrap_is_suid=true
fi
FUSE_DIR=
for mp in $(grep " fuse[. ]" /proc/self/mounts | grep "user_id=$(id -u)" | awk '{print $2}'); do
if test -d "$mp"; then
echo "# Using $mp as test fuse mount"
FUSE_DIR="$mp"
break
fi
done
if test "$(id -u)" = "0"; then
is_uidzero=true
else
is_uidzero=false
fi
# This is supposed to be an otherwise readable file in an unreadable (by the user) dir
UNREADABLE=/root/.bashrc
if "${is_uidzero}" || test -x "$(dirname "$UNREADABLE")"; then
UNREADABLE=
fi
# https://github.com/projectatomic/bubblewrap/issues/217
# are we on a merged-/usr system?
if [ /lib -ef /usr/lib ]; then
BWRAP_RO_HOST_ARGS="--ro-bind /usr /usr
--ro-bind /etc /etc
--dir /var/tmp
--symlink usr/lib /lib
--symlink usr/lib64 /lib64
--symlink usr/bin /bin
--symlink usr/sbin /sbin
--proc /proc
--dev /dev"
else
BWRAP_RO_HOST_ARGS="--ro-bind /usr /usr
--ro-bind /etc /etc
--ro-bind /bin /bin
--ro-bind-try /lib /lib
--ro-bind-try /lib64 /lib64
--ro-bind-try /sbin /sbin
--ro-bind-try /nix/store /nix/store
--dir /var/tmp
--proc /proc
--dev /dev"
fi
# Default arg, bind whole host fs to /, tmpfs on /tmp
RUN="${BWRAP} --bind / / --tmpfs /tmp"
if [ -z "${BWRAP_MUST_WORK-}" ] && ! $RUN true; then
skip Seems like bwrap is not working at all. Maybe setuid is not working
fi
extract_child_pid() {
grep child-pid "$1" | sed "s/^.*: \([0-9]*\).*/\1/"
}

View File

@@ -0,0 +1,72 @@
test_programs = [
['test-utils', executable(
'test-utils',
'test-utils.c',
'../utils.c',
'../utils.h',
dependencies : [selinux_dep],
include_directories : common_include_directories,
)],
]
executable(
'try-syscall',
'try-syscall.c',
override_options: ['b_sanitize=none'],
)
test_scripts = [
'test-run.sh',
'test-seccomp.py',
'test-specifying-pidns.sh',
'test-specifying-userns.sh',
]
test_env = environment()
test_env.set('BWRAP', bwrap.full_path())
test_env.set('G_TEST_BUILDDIR', meson.current_build_dir() / '..')
test_env.set('G_TEST_SRCDIR', meson.current_source_dir() / '..')
foreach pair : test_programs
name = pair[0]
test_program = pair[1]
if meson.version().version_compare('>=0.50.0')
test(
name,
test_program,
env : test_env,
protocol : 'tap',
)
else
test(
name,
test_program,
env : test_env,
)
endif
endforeach
foreach test_script : test_scripts
if test_script.endswith('.py')
interpreter = python
else
interpreter = bash
endif
if meson.version().version_compare('>=0.50.0')
test(
test_script,
interpreter,
args : [files(test_script)],
env : test_env,
protocol : 'tap',
)
else
test(
test_script,
interpreter,
args : [files(test_script)],
env : test_env,
)
endif
endforeach

692
codex-rs/vendor/bubblewrap/tests/test-run.sh vendored Executable file
View File

@@ -0,0 +1,692 @@
#!/usr/bin/env bash
set -xeuo pipefail
srcd=$(cd $(dirname "$0") && pwd)
. ${srcd}/libtest.sh
bn=$(basename "$0")
test_count=0
ok () {
test_count=$((test_count + 1))
echo ok $test_count "$@"
}
ok_skip () {
ok "# SKIP" "$@"
}
done_testing () {
echo "1..$test_count"
}
# Test help
${BWRAP} --help > help.txt
assert_file_has_content help.txt "usage: ${BWRAP}"
ok "Help works"
for ALT in "" "--unshare-user-try" "--unshare-pid" "--unshare-user-try --unshare-pid"; do
# Test fuse fs as bind source
if [ "x$FUSE_DIR" != "x" ]; then
$RUN $ALT --proc /proc --dev /dev --bind $FUSE_DIR /tmp/foo true
ok "can bind-mount a FUSE directory with $ALT"
else
ok_skip "no FUSE support"
fi
# no --dev => no devpts => no map_root workaround
$RUN $ALT --proc /proc true
ok "can mount /proc with $ALT"
# No network
$RUN $ALT --unshare-net --proc /proc --dev /dev true
ok "can unshare network, create new /dev with $ALT"
# Unreadable file
echo -n "expect EPERM: " >&2
# Test caps when bwrap is not setuid
if test -n "${bwrap_is_suid:-}"; then
CAP="--cap-add ALL"
else
CAP=""
fi
if ! cat /etc/shadow >/dev/null &&
$RUN $CAP $ALT --unshare-net --proc /proc --bind /etc/shadow /tmp/foo cat /tmp/foo; then
assert_not_reached Could read /etc/shadow via /tmp/foo bind-mount
fi
if ! cat /etc/shadow >/dev/null &&
$RUN $CAP $ALT --unshare-net --proc /proc --bind /etc/shadow /tmp/foo cat /etc/shadow; then
assert_not_reached Could read /etc/shadow
fi
ok "cannot read /etc/shadow with $ALT"
# Unreadable dir
if [ "x$UNREADABLE" != "x" ]; then
echo -n "expect EPERM: " >&2
if $RUN $ALT --unshare-net --proc /proc --dev /dev --bind $UNREADABLE /tmp/foo cat /tmp/foo; then
assert_not_reached Could read $UNREADABLE
fi
ok "cannot read $UNREADABLE with $ALT"
else
ok_skip "not sure what unreadable file to use"
fi
# bind dest in symlink (https://github.com/projectatomic/bubblewrap/pull/119)
$RUN $ALT --dir /tmp/dir --symlink dir /tmp/link --bind /etc /tmp/link true
ok "can bind a destination over a symlink"
done
# Test symlink behaviour
rm -f ./symlink
$RUN --ro-bind / / --bind "$(pwd)" "$(pwd)" --symlink /dev/null "$(pwd)/symlink" true >&2
readlink ./symlink > target.txt
assert_file_has_content target.txt /dev/null
ok "--symlink works"
$RUN --ro-bind / / --bind "$(pwd)" "$(pwd)" --symlink /dev/null "$(pwd)/symlink" true >&2
ok "--symlink is idempotent"
if $RUN --ro-bind / / --bind "$(pwd)" "$(pwd)" --symlink /dev/full "$(pwd)/symlink" true 2>err.txt; then
fatal "creating a conflicting symlink should have failed"
else
assert_file_has_content err.txt "Can't make symlink .*: existing destination is /dev/null"
fi
ok "--symlink doesn't overwrite a conflicting symlink"
# Test devices
$RUN --unshare-pid --dev /dev ls -al /dev/{stdin,stdout,stderr,null,random,urandom,fd,core} >/dev/null
ok "all expected devices were created"
# Test --as-pid-1
$RUN --unshare-pid --as-pid-1 --bind / / bash -c 'echo $$' > as_pid_1.txt
assert_file_has_content as_pid_1.txt "1"
ok "can run as pid 1"
# Test --info-fd and --json-status-fd
if $RUN --unshare-all --info-fd 42 --json-status-fd 43 -- bash -c 'exit 42' 42>info.json 43>json-status.json 2>err.txt; then
fatal "should have been exit 42"
fi
assert_file_has_content info.json '"child-pid": [0-9]'
assert_file_has_content json-status.json '"child-pid": [0-9]'
assert_file_has_content_literal json-status.json '"exit-code": 42'
ok "info and json-status fd"
DATA=$($RUN --proc /proc --unshare-all --info-fd 42 --json-status-fd 43 -- bash -c 'stat -L -c "%n %i" /proc/self/ns/*' 42>info.json 43>json-status.json 2>err.txt)
for NS in "ipc" "mnt" "net" "pid" "uts"; do
want=$(echo "$DATA" | grep "/proc/self/ns/$NS" | awk '{print $2}')
assert_file_has_content info.json "$want"
assert_file_has_content json-status.json "$want"
done
ok "namespace id info in info and json-status fd"
if ! command -v strace >/dev/null || ! strace -h | grep -v -e default | grep -e fault >/dev/null; then
ok_skip "no strace fault injection"
else
! strace -o /dev/null -f -e trace=prctl -e fault=prctl:when=39 $RUN --die-with-parent --json-status-fd 42 true 42>json-status.json
assert_not_file_has_content json-status.json '"exit-code": [0-9]'
ok "pre-exec failure doesn't include exit-code in json-status"
fi
notanexecutable=/
$RUN --json-status-fd 42 $notanexecutable 42>json-status.json || true
assert_not_file_has_content json-status.json '"exit-code": [0-9]'
ok "exec failure doesn't include exit-code in json-status"
# These tests require --unshare-user
if test -n "${bwrap_is_suid:-}"; then
ok_skip "no --cap-add support"
ok_skip "no --cap-add support"
ok_skip "no --disable-userns"
else
BWRAP_RECURSE="$BWRAP --unshare-user --uid 0 --gid 0 --cap-add ALL --bind / / --bind /proc /proc"
# $BWRAP May be inaccessible due to the user namespace so use /proc/self/exe
$BWRAP_RECURSE -- /proc/self/exe --unshare-all --bind / / --bind /proc /proc echo hello > recursive_proc.txt
assert_file_has_content recursive_proc.txt "hello"
ok "can mount /proc recursively"
$BWRAP_RECURSE -- /proc/self/exe --unshare-all ${BWRAP_RO_HOST_ARGS} findmnt > recursive-newroot.txt
assert_file_has_content recursive-newroot.txt "/usr"
ok "can pivot to new rootfs recursively"
$BWRAP --dev-bind / / -- true
! $BWRAP --assert-userns-disabled --dev-bind / / -- true
$BWRAP --unshare-user --disable-userns --dev-bind / / -- true
! $BWRAP --unshare-user --disable-userns --dev-bind / / -- $BWRAP --dev-bind / / -- true
$BWRAP --unshare-user --disable-userns --dev-bind / / -- sh -c "echo 2 > /proc/sys/user/max_user_namespaces || true; ! $BWRAP --unshare-user --dev-bind / / -- true"
$BWRAP --unshare-user --disable-userns --dev-bind / / -- sh -c "echo 100 > /proc/sys/user/max_user_namespaces || true; ! $BWRAP --unshare-user --dev-bind / / -- true"
$BWRAP --unshare-user --disable-userns --dev-bind / / -- sh -c "! $BWRAP --unshare-user --dev-bind / / --assert-userns-disabled -- true"
$BWRAP_RECURSE --dev-bind / / -- true
! $BWRAP_RECURSE --assert-userns-disabled --dev-bind / / -- true
$BWRAP_RECURSE --unshare-user --disable-userns --dev-bind / / -- true
! $BWRAP_RECURSE --unshare-user --disable-userns --dev-bind / / -- /proc/self/exe --dev-bind / / -- true
$BWRAP_RECURSE --unshare-user --disable-userns --dev-bind / / -- sh -c "echo 2 > /proc/sys/user/max_user_namespaces || true; ! $BWRAP --unshare-user --dev-bind / / -- true"
$BWRAP_RECURSE --unshare-user --disable-userns --dev-bind / / -- sh -c "echo 100 > /proc/sys/user/max_user_namespaces || true; ! $BWRAP --unshare-user --dev-bind / / -- true"
$BWRAP_RECURSE --unshare-user --disable-userns --dev-bind / / -- sh -c "! $BWRAP --unshare-user --dev-bind / / --assert-userns-disabled -- true"
ok "can disable nested userns"
fi
# Test error prefixing
if $RUN --unshare-pid --bind /source-enoent /dest true 2>err.txt; then
assert_not_reached "bound nonexistent source"
fi
assert_file_has_content err.txt "^bwrap: Can't find source path.*source-enoent"
ok "error prefixing"
if ! ${is_uidzero}; then
# When invoked as non-root, check that by default we have no caps left
for OPT in "" "--unshare-user-try --as-pid-1" "--unshare-user-try" "--as-pid-1"; do
e=0
$RUN $OPT --unshare-pid getpcaps 1 >&2 2> caps.test || e=$?
sed -e 's/^/# /' < caps.test >&2
test "$e" = 0
assert_not_file_has_content caps.test ': =.*cap'
done
ok "we have no caps as uid != 0"
else
capsh --print | sed -e 's/no-new-privs=0/no-new-privs=1/' > caps.expected
for OPT in "" "--as-pid-1"; do
$RUN $OPT --unshare-pid capsh --print >caps.test
diff -u caps.expected caps.test
done
# And test that we can drop all, as well as specific caps
$RUN $OPT --cap-drop ALL --unshare-pid capsh --print >caps.test
assert_file_has_content caps.test 'Current: =$'
# Check for dropping kill/fowner (we assume all uid 0 callers have this)
# But we should still have net_bind_service for example
$RUN $OPT --cap-drop CAP_KILL --cap-drop CAP_FOWNER --unshare-pid capsh --print >caps.test
# capsh's output format changed from v2.29 -> drops are now indicated with -eip
if grep 'Current: =.*+eip$' caps.test; then
assert_not_file_has_content caps.test '^Current: =.*cap_kill.*+eip$'
assert_not_file_has_content caps.test '^Current: =.*cap_fowner.*+eip$'
assert_file_has_content caps.test '^Current: =.*cap_net_bind_service.*+eip$'
else
assert_file_has_content caps.test '^Current: =eip.*cap_kill.*-eip$'
assert_file_has_content caps.test '^Current: =eip.*cap_fowner.*-eip$'
assert_not_file_has_content caps.test '^Current: =.*cap_net_bind_service.*-eip$'
fi
ok "we have the expected caps as uid 0"
fi
# Test --die-with-parent
cat >lockf-n.py <<EOF
#!/usr/bin/env python3
import struct,fcntl,sys
path = sys.argv[1]
if sys.argv[2] == 'wait':
locktype = fcntl.F_SETLKW
else:
locktype = fcntl.F_SETLK
lockdata = struct.pack("hhqqhh", fcntl.F_WRLCK, 0, 0, 0, 0, 0)
fd=open(sys.argv[1], 'a')
try:
fcntl.fcntl(fd.fileno(), locktype, lockdata)
except IOError as e:
sys.exit(1)
sys.exit(0)
EOF
chmod a+x lockf-n.py
touch lock
for die_with_parent_argv in "--die-with-parent" "--die-with-parent --unshare-pid"; do
# We have to loop here, because bwrap doesn't wait for the lock if
# another process is holding it. If we're unlucky, lockf-n.py will
# be holding it.
bash -c "while true; do $RUN ${die_with_parent_argv} --lock-file $(pwd)/lock sleep 1h; done" &
childshellpid=$!
# Wait for lock to be taken (yes hacky)
for x in $(seq 10); do
if ./lockf-n.py ./lock nowait; then
sleep 1
else
break
fi
done
if ./lockf-n.py ./lock nowait; then
assert_not_reached "timed out waiting for lock"
fi
# Kill the shell, which should kill bwrap (and the sleep)
kill -9 ${childshellpid}
# Lock file should be unlocked
./lockf-n.py ./lock wait
ok "die with parent ${die_with_parent_argv}"
done
printf '%s--dir\0/tmp/hello/world\0' '' > test.args
printf '%s--dir\0/tmp/hello/world2\0' '' > test.args2
printf '%s--dir\0/tmp/hello/world3\0' '' > test.args3
$RUN --args 3 --args 4 --args 5 /bin/sh -c 'test -d /tmp/hello/world && test -d /tmp/hello/world2 && test -d /tmp/hello/world3' 3<test.args 4<test.args2 5<test.args3
ok "we can parse arguments from a fd"
mkdir bin
echo "#!/bin/sh" > bin/--inadvisable-executable-name--
echo "echo hello" >> bin/--inadvisable-executable-name--
chmod +x bin/--inadvisable-executable-name--
PATH="${srcd}:$PATH" $RUN -- sh -c "echo hello" > stdout
assert_file_has_content stdout hello
ok "we can run with --"
PATH="$(pwd)/bin:$PATH" $RUN -- --inadvisable-executable-name-- > stdout
assert_file_has_content stdout hello
ok "we can run an inadvisable executable name with --"
if $RUN -- --dev-bind /dev /dev sh -c 'echo should not have run'; then
assert_not_reached "'--dev-bind' should have been interpreted as a (silly) executable name"
fi
ok "options like --dev-bind are defanged by --"
if command -v mktemp > /dev/null; then
tempfile="$(mktemp /tmp/bwrap-test-XXXXXXXX)"
echo "hello" > "$tempfile"
$BWRAP --bind / / cat "$tempfile" > stdout
assert_file_has_content stdout hello
ok "bind-mount of / exposes real /tmp"
$BWRAP --bind / / --bind /tmp /tmp cat "$tempfile" > stdout
assert_file_has_content stdout hello
ok "bind-mount of /tmp exposes real /tmp"
if [ -d /mnt ] && [ ! -L /mnt ]; then
$BWRAP --bind / / --bind /tmp /mnt cat "/mnt/${tempfile#/tmp/}" > stdout
assert_file_has_content stdout hello
ok "bind-mount of /tmp onto /mnt exposes real /tmp"
else
ok_skip "/mnt does not exist or is a symlink"
fi
else
ok_skip "mktemp not found"
ok_skip "mktemp not found"
ok_skip "mktemp not found"
fi
if $RUN test -d /tmp/oldroot; then
assert_not_reached "/tmp/oldroot should not be visible"
fi
if $RUN test -d /tmp/newroot; then
assert_not_reached "/tmp/newroot should not be visible"
fi
echo "hello" > input.$$
$BWRAP --bind / / --bind "$(pwd)" /tmp cat /tmp/input.$$ > stdout
assert_file_has_content stdout hello
if $BWRAP --bind / / --bind "$(pwd)" /tmp test -d /tmp/oldroot; then
assert_not_reached "/tmp/oldroot should not be visible"
fi
if $BWRAP --bind / / --bind "$(pwd)" /tmp test -d /tmp/newroot; then
assert_not_reached "/tmp/newroot should not be visible"
fi
ok "we can mount another directory onto /tmp"
echo "hello" > input.$$
$RUN --bind "$(pwd)" /tmp/here cat /tmp/here/input.$$ > stdout
assert_file_has_content stdout hello
if $RUN --bind "$(pwd)" /tmp/here test -d /tmp/oldroot; then
assert_not_reached "/tmp/oldroot should not be visible"
fi
if $RUN --bind "$(pwd)" /tmp/here test -d /tmp/newroot; then
assert_not_reached "/tmp/newroot should not be visible"
fi
ok "we can mount another directory inside /tmp"
touch some-file
mkdir -p some-dir
rm -fr new-dir-mountpoint
rm -fr new-file-mountpoint
$RUN \
--bind "$(pwd -P)/some-dir" "$(pwd -P)/new-dir-mountpoint" \
--bind "$(pwd -P)/some-file" "$(pwd -P)/new-file-mountpoint" \
true
command stat -c '%a' new-dir-mountpoint > new-dir-permissions
assert_file_has_content new-dir-permissions 755
command stat -c '%a' new-file-mountpoint > new-file-permissions
assert_file_has_content new-file-permissions 444
ok "Files and directories created as mount points have expected permissions"
if [ -S /dev/log ]; then
$RUN --bind / / --bind "$(realpath /dev/log)" "$(realpath /dev/log)" true
ok "Can bind-mount a socket (/dev/log) onto a socket"
else
ok_skip "- /dev/log is not a socket, cannot test bubblewrap#409"
fi
mkdir -p dir-already-existed
chmod 0710 dir-already-existed
mkdir -p dir-already-existed2
chmod 0754 dir-already-existed2
rm -fr new-dir-default-perms
rm -fr new-dir-set-perms
$RUN \
--perms 1741 --dir "$(pwd -P)/new-dir-set-perms" \
--dir "$(pwd -P)/dir-already-existed" \
--perms 0741 --dir "$(pwd -P)/dir-already-existed2" \
--dir "$(pwd -P)/dir-chmod" \
--chmod 1755 "$(pwd -P)/dir-chmod" \
--dir "$(pwd -P)/new-dir-default-perms" \
true
command stat -c '%a' new-dir-default-perms > new-dir-permissions
assert_file_has_content new-dir-permissions '^755$'
command stat -c '%a' new-dir-set-perms > new-dir-permissions
assert_file_has_content new-dir-permissions '^1741$'
command stat -c '%a' dir-already-existed > dir-permissions
assert_file_has_content dir-permissions '^710$'
command stat -c '%a' dir-already-existed2 > dir-permissions
assert_file_has_content dir-permissions '^754$'
command stat -c '%a' dir-chmod > dir-permissions
assert_file_has_content dir-permissions '^1755$'
ok "Directories created explicitly have expected permissions"
rm -fr parent
rm -fr parent-of-1777
rm -fr parent-of-0755
rm -fr parent-of-0644
rm -fr parent-of-0750
rm -fr parent-of-0710
rm -fr parent-of-0720
rm -fr parent-of-0640
rm -fr parent-of-0700
rm -fr parent-of-0600
rm -fr parent-of-0705
rm -fr parent-of-0604
rm -fr parent-of-0000
$RUN \
--dir "$(pwd -P)"/parent/dir \
--perms 1777 --dir "$(pwd -P)"/parent-of-1777/dir \
--perms 0755 --dir "$(pwd -P)"/parent-of-0755/dir \
--perms 0644 --dir "$(pwd -P)"/parent-of-0644/dir \
--perms 0750 --dir "$(pwd -P)"/parent-of-0750/dir \
--perms 0710 --dir "$(pwd -P)"/parent-of-0710/dir \
--perms 0720 --dir "$(pwd -P)"/parent-of-0720/dir \
--perms 0640 --dir "$(pwd -P)"/parent-of-0640/dir \
--perms 0700 --dir "$(pwd -P)"/parent-of-0700/dir \
--perms 0600 --dir "$(pwd -P)"/parent-of-0600/dir \
--perms 0705 --dir "$(pwd -P)"/parent-of-0705/dir \
--perms 0604 --dir "$(pwd -P)"/parent-of-0604/dir \
--perms 0000 --dir "$(pwd -P)"/parent-of-0000/dir \
true
command stat -c '%a' parent > dir-permissions
assert_file_has_content dir-permissions '^755$'
command stat -c '%a' parent-of-1777 > dir-permissions
assert_file_has_content dir-permissions '^755$'
command stat -c '%a' parent-of-0755 > dir-permissions
assert_file_has_content dir-permissions '^755$'
command stat -c '%a' parent-of-0644 > dir-permissions
assert_file_has_content dir-permissions '^755$'
command stat -c '%a' parent-of-0750 > dir-permissions
assert_file_has_content dir-permissions '^750$'
command stat -c '%a' parent-of-0710 > dir-permissions
assert_file_has_content dir-permissions '^750$'
command stat -c '%a' parent-of-0720 > dir-permissions
assert_file_has_content dir-permissions '^750$'
command stat -c '%a' parent-of-0640 > dir-permissions
assert_file_has_content dir-permissions '^750$'
command stat -c '%a' parent-of-0700 > dir-permissions
assert_file_has_content dir-permissions '^700$'
command stat -c '%a' parent-of-0600 > dir-permissions
assert_file_has_content dir-permissions '^700$'
command stat -c '%a' parent-of-0705 > dir-permissions
assert_file_has_content dir-permissions '^705$'
command stat -c '%a' parent-of-0604 > dir-permissions
assert_file_has_content dir-permissions '^705$'
command stat -c '%a' parent-of-0000 > dir-permissions
assert_file_has_content dir-permissions '^700$'
chmod -R 0700 parent*
rm -fr parent*
ok "Directories created as parents have expected permissions"
$RUN \
--perms 01777 --tmpfs "$(pwd -P)" \
cat /proc/self/mountinfo >&2
$RUN \
--perms 01777 --tmpfs "$(pwd -P)" \
stat -c '%a' "$(pwd -P)" > dir-permissions
assert_file_has_content dir-permissions '^1777$'
$RUN \
--tmpfs "$(pwd -P)" \
stat -c '%a' "$(pwd -P)" > dir-permissions
assert_file_has_content dir-permissions '^755$'
ok "tmpfs has expected permissions"
# 1048576 = 1 MiB
if test -n "${bwrap_is_suid:-}"; then
if $RUN --size 1048576 --tmpfs "$(pwd -P)" true; then
assert_not_reached "Should not allow --size --tmpfs when setuid"
fi
ok "--size --tmpfs is not allowed when setuid"
elif df --output=size --block-size=1K "$(pwd -P)" >/dev/null 2>/dev/null; then
$RUN \
--size 1048576 --tmpfs "$(pwd -P)" \
df --output=size --block-size=1K "$(pwd -P)" > dir-size
assert_file_has_content dir-size '^ *1024$'
$RUN \
--size 1048576 --perms 01777 --tmpfs "$(pwd -P)" \
stat -c '%a' "$(pwd -P)" > dir-permissions
assert_file_has_content dir-permissions '^1777$'
$RUN \
--size 1048576 --perms 01777 --tmpfs "$(pwd -P)" \
df --output=size --block-size=1K "$(pwd -P)" > dir-size
assert_file_has_content dir-size '^ *1024$'
$RUN \
--perms 01777 --size 1048576 --tmpfs "$(pwd -P)" \
stat -c '%a' "$(pwd -P)" > dir-permissions
assert_file_has_content dir-permissions '^1777$'
$RUN \
--perms 01777 --size 1048576 --tmpfs "$(pwd -P)" \
df --output=size --block-size=1K "$(pwd -P)" > dir-size
assert_file_has_content dir-size '^ *1024$'
ok "tmpfs has expected size"
else
$RUN --size 1048576 --tmpfs "$(pwd -P)" true
$RUN --perms 01777 --size 1048576 --tmpfs "$(pwd -P)" true
$RUN --size 1048576 --perms 01777 --tmpfs "$(pwd -P)" true
ok_skip "df is too old, cannot test --size --tmpfs fully"
fi
$RUN \
--file 0 /tmp/file \
stat -c '%a' /tmp/file < /dev/null > file-permissions
assert_file_has_content file-permissions '^666$'
$RUN \
--perms 0640 --file 0 /tmp/file \
stat -c '%a' /tmp/file < /dev/null > file-permissions
assert_file_has_content file-permissions '^640$'
$RUN \
--bind-data 0 /tmp/file \
stat -c '%a' /tmp/file < /dev/null > file-permissions
assert_file_has_content file-permissions '^600$'
$RUN \
--perms 0640 --bind-data 0 /tmp/file \
stat -c '%a' /tmp/file < /dev/null > file-permissions
assert_file_has_content file-permissions '^640$'
$RUN \
--ro-bind-data 0 /tmp/file \
stat -c '%a' /tmp/file < /dev/null > file-permissions
assert_file_has_content file-permissions '^600$'
$RUN \
--perms 0640 --ro-bind-data 0 /tmp/file \
stat -c '%a' /tmp/file < /dev/null > file-permissions
assert_file_has_content file-permissions '^640$'
ok "files have expected permissions"
if $RUN --size 0 --tmpfs /tmp/a true; then
assert_not_reached Zero tmpfs size allowed
fi
if $RUN --size 123bogus --tmpfs /tmp/a true; then
assert_not_reached Bogus tmpfs size allowed
fi
if $RUN --size '' --tmpfs /tmp/a true; then
assert_not_reached Empty tmpfs size allowed
fi
if $RUN --size -12345678 --tmpfs /tmp/a true; then
assert_not_reached Negative tmpfs size allowed
fi
if $RUN --size ' -12345678' --tmpfs /tmp/a true; then
assert_not_reached Negative tmpfs size with space allowed
fi
# This is 2^64
if $RUN --size 18446744073709551616 --tmpfs /tmp/a true; then
assert_not_reached Overflowing tmpfs size allowed
fi
# This is 2^63 + 1; note that the current max size is SIZE_MAX/2
if $RUN --size 9223372036854775809 --tmpfs /tmp/a true; then
assert_not_reached Too-large tmpfs size allowed
fi
ok "bogus tmpfs size not allowed"
if $RUN --perms 0640 --perms 0640 --tmpfs /tmp/a true; then
assert_not_reached Multiple perms options allowed
fi
if $RUN --size 1048576 --size 1048576 --tmpfs /tmp/a true; then
assert_not_reached Multiple perms options allowed
fi
ok "--perms and --size only allowed once"
FOO= BAR=baz $RUN --setenv FOO bar sh -c 'echo "$FOO$BAR"' > stdout
assert_file_has_content stdout barbaz
FOO=wrong BAR=baz $RUN --setenv FOO bar sh -c 'echo "$FOO$BAR"' > stdout
assert_file_has_content stdout barbaz
FOO=wrong BAR=baz $RUN --unsetenv FOO sh -c 'printf "%s%s" "$FOO" "$BAR"' > stdout
printf baz > reference
assert_files_equal stdout reference
FOO=wrong BAR=wrong $RUN --clearenv /usr/bin/env > stdout
echo "PWD=$(pwd -P)" > reference
assert_files_equal stdout reference
ok "environment manipulation"
$RUN sh -c 'echo $0' > stdout
assert_file_has_content stdout sh
$RUN --argv0 sh sh -c 'echo $0' > stdout
assert_file_has_content stdout sh
$RUN --argv0 right sh -c 'echo $0' > stdout
assert_file_has_content stdout right
ok "argv0 manipulation"
echo "foobar" > file-data
$RUN --proc /proc --dev /dev --bind / / --bind-fd 100 /tmp cat /tmp/file-data 100< . > stdout
assert_file_has_content stdout foobar
ok "bind-fd"
$RUN --chdir / --chdir / true > stdout 2>&1
assert_file_has_content stdout '^bwrap: Only the last --chdir option will take effect$'
ok "warning logged for redundant --chdir"
$RUN --level-prefix --chdir / --chdir / true > stdout 2>&1
assert_file_has_content stdout '^<4>bwrap: Only the last --chdir option will take effect$'
ok "--level-prefix"
if test -n "${bwrap_is_suid:-}"; then
ok_skip "no --overlay support"
ok_skip "no --overlay support"
ok_skip "no --tmp-overlay support"
ok_skip "no --ro-overlay support"
ok_skip "no --overlay-src support"
else
mkdir lower1 lower2 upper work
printf 1 > lower1/a
printf 2 > lower1/b
printf 3 > lower2/b
printf 4 > upper/a
# Check if unprivileged overlayfs is available
if ! unshare -rm mount -t overlay -o lowerdir=lower1,upperdir=upper,workdir=work,userxattr overlay lower2; then
ok_skip "no kernel support for unprivileged overlayfs"
ok_skip "no kernel support for unprivileged overlayfs"
ok_skip "no kernel support for unprivileged overlayfs"
ok_skip "no kernel support for unprivileged overlayfs"
ok_skip "no kernel support for unprivileged overlayfs"
else
# Test --overlay
if $RUN --overlay upper work /tmp true 2>err.txt; then
assert_not_reached At least one --overlay-src not required
fi
assert_file_has_content err.txt "^bwrap: --overlay requires at least one --overlay-src"
$RUN --overlay-src lower1 --overlay upper work /tmp/x/y/z cat /tmp/x/y/z/a > stdout
assert_file_has_content stdout '^4$'
$RUN --overlay-src lower1 --overlay upper work /tmp/x/y/z cat /tmp/x/y/z/b > stdout
assert_file_has_content stdout '^2$'
$RUN --overlay-src lower1 --overlay-src lower2 --overlay upper work /tmp/x/y/z cat /tmp/x/y/z/a > stdout
assert_file_has_content stdout '^4$'
$RUN --overlay-src lower1 --overlay-src lower2 --overlay upper work /tmp/x/y/z cat /tmp/x/y/z/b > stdout
assert_file_has_content stdout '^3$'
$RUN --overlay-src lower1 --overlay-src lower2 --overlay upper work /tmp/x/y/z sh -c 'printf 5 > /tmp/x/y/z/b; cat /tmp/x/y/z/b' > stdout
assert_file_has_content stdout '^5$'
assert_file_has_content upper/b '^5$'
ok "--overlay"
# Test --overlay path escaping
# Coincidentally, ":,\ is the face I make contemplating anyone who might
# need this functionality, not that that's going to stop me from supporting
# it.
mkdir 'lower ":,\' 'upper ":,\' 'work ":,\'
printf 1 > 'lower ":,\'/a
$RUN --overlay-src 'lower ":,\' --overlay 'upper ":,\' 'work ":,\' /tmp/x sh -c 'cat /tmp/x/a; printf 2 > /tmp/x/a; cat /tmp/x/a' > stdout
assert_file_has_content stdout '^12$'
assert_file_has_content 'lower ":,\'/a '^1$'
assert_file_has_content 'upper ":,\'/a '^2$'
ok "--overlay path escaping"
# Test --tmp-overlay
printf 1 > lower1/a
printf 2 > lower1/b
printf 3 > lower2/b
if $RUN --tmp-overlay /tmp true 2>err.txt; then
assert_not_reached At least one --overlay-src not required
fi
assert_file_has_content err.txt "^bwrap: --tmp-overlay requires at least one --overlay-src"
$RUN --overlay-src lower1 --tmp-overlay /tmp/x/y/z cat /tmp/x/y/z/a > stdout
assert_file_has_content stdout '^1$'
$RUN --overlay-src lower1 --tmp-overlay /tmp/x/y/z cat /tmp/x/y/z/b > stdout
assert_file_has_content stdout '^2$'
$RUN --overlay-src lower1 --overlay-src lower2 --tmp-overlay /tmp/x/y/z cat /tmp/x/y/z/a > stdout
assert_file_has_content stdout '^1$'
$RUN --overlay-src lower1 --overlay-src lower2 --tmp-overlay /tmp/x/y/z cat /tmp/x/y/z/b > stdout
assert_file_has_content stdout '^3$'
$RUN --overlay-src lower1 --overlay-src lower2 --tmp-overlay /tmp/x/y/z sh -c 'printf 4 > /tmp/x/y/z/b; cat /tmp/x/y/z/b' > stdout
assert_file_has_content stdout '^4$'
$RUN --overlay-src lower1 --tmp-overlay /tmp/x --overlay-src lower2 --tmp-overlay /tmp/y sh -c 'cat /tmp/x/b; printf 4 > /tmp/x/b; cat /tmp/x/b; cat /tmp/y/b' > stdout
assert_file_has_content stdout '^243$'
assert_file_has_content lower1/b '^2$'
assert_file_has_content lower2/b '^3$'
ok "--tmp-overlay"
# Test --ro-overlay
printf 1 > lower1/a
printf 2 > lower1/b
printf 3 > lower2/b
if $RUN --ro-overlay /tmp true 2>err.txt; then
assert_not_reached At least two --overlay-src not required
fi
assert_file_has_content err.txt "^bwrap: --ro-overlay requires at least two --overlay-src"
if $RUN --overlay-src lower1 --ro-overlay /tmp true 2>err.txt; then
assert_not_reached At least two --overlay-src not required
fi
assert_file_has_content err.txt "^bwrap: --ro-overlay requires at least two --overlay-src"
$RUN --overlay-src lower1 --overlay-src lower2 --ro-overlay /tmp/x/y/z cat /tmp/x/y/z/a > stdout
assert_file_has_content stdout '^1$'
$RUN --overlay-src lower1 --overlay-src lower2 --ro-overlay /tmp/x/y/z cat /tmp/x/y/z/b > stdout
assert_file_has_content stdout '^3$'
$RUN --overlay-src lower1 --overlay-src lower2 --ro-overlay /tmp/x/y/z sh -c 'printf 4 > /tmp/x/y/z/b; cat /tmp/x/y/z/b' > stdout
assert_file_has_content stdout '^3$'
ok "--ro-overlay"
# Test --overlay-src restrictions
if $RUN --overlay-src /tmp true 2>err.txt; then
assert_not_reached Trailing --overlay-src allowed
fi
assert_file_has_content err.txt "^bwrap: --overlay-src must be followed by another --overlay-src or one of --overlay, --tmp-overlay, or --ro-overlay"
if $RUN --overlay-src /tmp --chdir / true 2>err.txt; then
assert_not_reached --overlay-src allowed to precede non-overlay options
fi
assert_file_has_content err.txt "^bwrap: --overlay-src must be followed by another --overlay-src or one of --overlay, --tmp-overlay, or --ro-overlay"
ok "--overlay-src restrictions"
fi
fi
done_testing

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

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -xeuo pipefail
srcd=$(cd $(dirname "$0") && pwd)
. "${srcd}/libtest.sh"
echo "1..1"
# This test needs user namespaces
if test -n "${bwrap_is_suid:-}"; then
echo "ok - # SKIP no setuid support for --unshare-user"
else
mkfifo donepipe
$RUN --info-fd 42 --unshare-user --unshare-pid sh -c 'readlink /proc/self/ns/pid > sandbox-pidns; cat < donepipe' >/dev/null 42>info.json &
while ! test -f sandbox-pidns; do sleep 1; done
SANDBOX1PID=$(extract_child_pid info.json)
ASAN_OPTIONS=detect_leaks=0 LSAN_OPTIONS=detect_leaks=0 \
$RUN --userns 11 --pidns 12 readlink /proc/self/ns/pid > sandbox2-pidns 11< /proc/$SANDBOX1PID/ns/user 12< /proc/$SANDBOX1PID/ns/pid
echo foo > donepipe
assert_files_equal sandbox-pidns sandbox2-pidns
rm donepipe info.json sandbox-pidns
echo "ok - Test --pidns"
fi

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -xeuo pipefail
srcd=$(cd $(dirname "$0") && pwd)
. "${srcd}/libtest.sh"
echo "1..1"
# This test needs user namespaces
if test -n "${bwrap_is_suid:-}"; then
echo "ok - # SKIP no setuid support for --unshare-user"
else
mkfifo donepipe
$RUN --info-fd 42 --unshare-user sh -c 'readlink /proc/self/ns/user > sandbox-userns; cat < donepipe' >/dev/null 42>info.json &
while ! test -f sandbox-userns; do sleep 1; done
SANDBOX1PID=$(extract_child_pid info.json)
$RUN --userns 11 readlink /proc/self/ns/user > sandbox2-userns 11< /proc/$SANDBOX1PID/ns/user
echo foo > donepipe
assert_files_equal sandbox-userns sandbox2-userns
rm donepipe info.json sandbox-userns
echo "ok - Test --userns"
fi

View File

@@ -0,0 +1,247 @@
/*
* Copyright © 2019-2021 Collabora Ltd.
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include "utils.h"
/* A small implementation of TAP */
static unsigned int test_number = 0;
__attribute__((format(printf, 1, 2)))
static void
ok (const char *format, ...)
{
va_list ap;
printf ("ok %u - ", ++test_number);
va_start (ap, format);
vprintf (format, ap);
va_end (ap);
printf ("\n");
}
/* for simplicity we always die immediately on failure */
#define not_ok(fmt, ...) die (fmt, ## __VA_ARGS__)
/* approximately GLib-compatible helper macros */
#define g_test_message(fmt, ...) printf ("# " fmt "\n", ## __VA_ARGS__)
#define g_assert_cmpstr(left_expr, op, right_expr) \
do { \
const char *left = (left_expr); \
const char *right = (right_expr); \
if (strcmp0 (left, right) op 0) \
ok ("%s (\"%s\") %s %s (\"%s\")", #left_expr, left, #op, #right_expr, right); \
else \
not_ok ("expected %s (\"%s\") %s %s (\"%s\")", \
#left_expr, left, #op, #right_expr, right); \
} while (0)
#define g_assert_cmpint(left_expr, op, right_expr) \
do { \
intmax_t left = (left_expr); \
intmax_t right = (right_expr); \
if (left op right) \
ok ("%s (%ji) %s %s (%ji)", #left_expr, left, #op, #right_expr, right); \
else \
not_ok ("expected %s (%ji) %s %s (%ji)", \
#left_expr, left, #op, #right_expr, right); \
} while (0)
#define g_assert_cmpuint(left_expr, op, right_expr) \
do { \
uintmax_t left = (left_expr); \
uintmax_t right = (right_expr); \
if (left op right) \
ok ("%s (%ju) %s %s (%ju)", #left_expr, left, #op, #right_expr, right); \
else \
not_ok ("expected %s (%ju) %s %s (%ju)", \
#left_expr, left, #op, #right_expr, right); \
} while (0)
#define g_assert_true(expr) \
do { \
if ((expr)) \
ok ("%s", #expr); \
else \
not_ok ("expected %s to be true", #expr); \
} while (0)
#define g_assert_false(expr) \
do { \
if (!(expr)) \
ok ("!(%s)", #expr); \
else \
not_ok ("expected %s to be false", #expr); \
} while (0)
#define g_assert_null(expr) \
do { \
if ((expr) == NULL) \
ok ("%s was null", #expr); \
else \
not_ok ("expected %s to be null", #expr); \
} while (0)
#define g_assert_nonnull(expr) \
do { \
if ((expr) != NULL) \
ok ("%s wasn't null", #expr); \
else \
not_ok ("expected %s to be non-null", #expr); \
} while (0)
static int
strcmp0 (const char *left,
const char *right)
{
if (left == right)
return 0;
if (left == NULL)
return -1;
if (right == NULL)
return 1;
return strcmp (left, right);
}
static void
test_n_elements (void)
{
int three[] = { 1, 2, 3 };
g_assert_cmpuint (N_ELEMENTS (three), ==, 3);
}
static void
test_strconcat (void)
{
const char *a = "aaa";
const char *b = "bbb";
char *ab = strconcat (a, b);
g_assert_cmpstr (ab, ==, "aaabbb");
free (ab);
}
static void
test_strconcat3 (void)
{
const char *a = "aaa";
const char *b = "bbb";
const char *c = "ccc";
char *abc = strconcat3 (a, b, c);
g_assert_cmpstr (abc, ==, "aaabbbccc");
free (abc);
}
static void
test_has_prefix (void)
{
g_assert_true (has_prefix ("foo", "foo"));
g_assert_true (has_prefix ("foobar", "foo"));
g_assert_false (has_prefix ("foobar", "fool"));
g_assert_false (has_prefix ("foo", "fool"));
g_assert_true (has_prefix ("foo", ""));
g_assert_true (has_prefix ("", ""));
g_assert_false (has_prefix ("", "no"));
g_assert_false (has_prefix ("yes", "no"));
}
static void
test_has_path_prefix (void)
{
static const struct
{
const char *str;
const char *prefix;
bool expected;
} tests[] =
{
{ "/run/host/usr", "/run/host", true },
{ "/run/host/usr", "/run/host/", true },
{ "/run/host", "/run/host", true },
{ "////run///host////usr", "//run//host", true },
{ "////run///host////usr", "//run//host////", true },
{ "/run/hostage", "/run/host", false },
/* Any number of leading slashes is ignored, even zero */
{ "foo/bar", "/foo", true },
{ "/foo/bar", "foo", true },
};
size_t i;
for (i = 0; i < N_ELEMENTS (tests); i++)
{
const char *str = tests[i].str;
const char *prefix = tests[i].prefix;
bool expected = tests[i].expected;
if (expected)
g_test_message ("%s should have path prefix %s", str, prefix);
else
g_test_message ("%s should not have path prefix %s", str, prefix);
if (expected)
g_assert_true (has_path_prefix (str, prefix));
else
g_assert_false (has_path_prefix (str, prefix));
}
}
static void
test_string_builder (void)
{
StringBuilder sb = {0};
strappend (&sb, "aaa");
g_assert_cmpstr (sb.str, ==, "aaa");
strappend (&sb, "bbb");
g_assert_cmpstr (sb.str, ==, "aaabbb");
strappendf (&sb, "c%dc%s", 9, "x");
g_assert_cmpstr (sb.str, ==, "aaabbbc9cx");
strappend_escape_for_mount_options (&sb, "/path :,\\");
g_assert_cmpstr (sb.str, ==, "aaabbbc9cx/path \\:\\,\\\\");
strappend (&sb, "zzz");
g_assert_cmpstr (sb.str, ==, "aaabbbc9cx/path \\:\\,\\\\zzz");
free (sb.str);
sb = (StringBuilder){0};
strappend_escape_for_mount_options (&sb, "aaa");
g_assert_cmpstr (sb.str, ==, "aaa");
free (sb.str);
sb = (StringBuilder){0};
strappend_escape_for_mount_options (&sb, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
g_assert_cmpstr (sb.str, ==, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
free (sb.str);
}
int
main (int argc UNUSED,
char **argv UNUSED)
{
setvbuf (stdout, NULL, _IONBF, 0);
test_n_elements ();
test_strconcat ();
test_strconcat3 ();
test_has_prefix ();
test_has_path_prefix ();
test_string_builder ();
printf ("1..%u\n", test_number);
return 0;
}

View File

@@ -0,0 +1,180 @@
/*
* 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 == _ABIO32
# define MISSING_SYSCALL_BASE 4000
# elif _MIPS_SIM == _ABI64
# define MISSING_SYSCALL_BASE 5000
# elif _MIPS_SIM == _ABIN32
# 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)
#ifndef PR_GET_CHILD_SUBREAPER
#define PR_GET_CHILD_SUBREAPER 37
#endif
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;
}

View File

@@ -0,0 +1,2 @@
/_build/
/subprojects/

View File

@@ -0,0 +1,3 @@
This is a simple example of a project that uses bubblewrap as a
subproject. The intention is that if this project can successfully build
bubblewrap as a subproject, then so could Flatpak.

View File

@@ -0,0 +1,26 @@
#!/usr/bin/python3
# Copyright 2022 Collabora Ltd.
# SPDX-License-Identifier: LGPL-2.0-or-later
import subprocess
import sys
if __name__ == '__main__':
completed = subprocess.run(
['objdump', '-T', '-x', sys.argv[1]],
stdout=subprocess.PIPE,
)
stdout = completed.stdout
assert stdout is not None
seen_rpath = False
for line in stdout.splitlines():
words = line.strip().split()
if words and words[0] in (b'RPATH', b'RUNPATH'):
print(line.decode(errors='backslashreplace'))
assert len(words) == 2, words
assert words[1] == b'${ORIGIN}/../lib', words
seen_rpath = True
assert seen_rpath

View File

@@ -0,0 +1 @@
#error Should not use superproject config.h to compile bubblewrap

View File

@@ -0,0 +1 @@
#error Should not use superproject generated config.h to compile bubblewrap

View File

@@ -0,0 +1,20 @@
project(
'use-bubblewrap-as-subproject',
'c',
version : '0',
meson_version : '>=0.49.0',
)
configure_file(
output : 'config.h',
input : 'dummy-config.h.in',
configuration : configuration_data(),
)
subproject(
'bubblewrap',
default_options : [
'install_rpath=${ORIGIN}/../lib',
'program_prefix=not-flatpak-',
],
)