├── LICENSE ├── tests ├── use-as-subproject │ ├── .gitignore │ ├── config.h │ ├── dummy-config.h.in │ ├── README │ ├── meson.build │ └── assert-correct-rpath.py ├── test-specifying-userns.sh ├── test-specifying-pidns.sh ├── meson.build ├── libtest.sh ├── try-syscall.c ├── libtest-core.sh ├── test-utils.c ├── test-seccomp.py └── test-run.sh ├── .dir-locals.el ├── bubblewrap.jpg ├── demos ├── flatpak.bpf ├── userns-block-fd.py ├── bubblewrap-shell.sh └── flatpak-run.sh ├── uncrustify.sh ├── .editorconfig ├── completions ├── meson.build ├── zsh │ ├── meson.build │ └── _bwrap └── bash │ ├── meson.build │ └── bwrap ├── CODE-OF-CONDUCT.md ├── Makefile-bwrap.am ├── autogen.sh ├── Makefile-docs.am ├── release-checklist.md ├── network.h ├── packaging └── bubblewrap.spec ├── meson_options.txt ├── bind-mount.h ├── SECURITY.md ├── ci └── builddeps.sh ├── Makefile.am ├── uncrustify.cfg ├── meson.build ├── .github └── workflows │ └── check.yml ├── configure.ac ├── network.c ├── utils.h ├── m4 └── attributes.m4 ├── README.md ├── git.mk ├── bind-mount.c └── utils.c /LICENSE: -------------------------------------------------------------------------------- 1 | COPYING -------------------------------------------------------------------------------- /tests/use-as-subproject/.gitignore: -------------------------------------------------------------------------------- 1 | /_build/ 2 | /subprojects/ 3 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((c-mode . ((indent-tabs-mode . nil) (c-file-style . "gnu")))) 2 | -------------------------------------------------------------------------------- /bubblewrap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aperezdc/bubblewrap/main/bubblewrap.jpg -------------------------------------------------------------------------------- /demos/flatpak.bpf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aperezdc/bubblewrap/main/demos/flatpak.bpf -------------------------------------------------------------------------------- /tests/use-as-subproject/config.h: -------------------------------------------------------------------------------- 1 | #error Should not use superproject config.h to compile bubblewrap 2 | -------------------------------------------------------------------------------- /tests/use-as-subproject/dummy-config.h.in: -------------------------------------------------------------------------------- 1 | #error Should not use superproject generated config.h to compile bubblewrap 2 | -------------------------------------------------------------------------------- /uncrustify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | uncrustify -c uncrustify.cfg --no-backup `git ls-tree --name-only -r HEAD | grep \\\.[ch]$` 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.[ch]] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | indent_brace_style = gnu 6 | -------------------------------------------------------------------------------- /completions/meson.build: -------------------------------------------------------------------------------- 1 | if get_option('bash_completion').enabled() 2 | subdir('bash') 3 | endif 4 | 5 | if get_option('zsh_completion').enabled() 6 | subdir('zsh') 7 | endif 8 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## The bubblewrap Project Community Code of Conduct 2 | 3 | The bubblewrap project follows the [Containers Community Code of Conduct](https://github.com/containers/common/blob/HEAD/CODE-OF-CONDUCT.md). 4 | -------------------------------------------------------------------------------- /tests/use-as-subproject/README: -------------------------------------------------------------------------------- 1 | This is a simple example of a project that uses bubblewrap as a 2 | subproject. The intention is that if this project can successfully build 3 | bubblewrap as a subproject, then so could Flatpak. 4 | -------------------------------------------------------------------------------- /completions/zsh/meson.build: -------------------------------------------------------------------------------- 1 | zsh_completion_dir = get_option('zsh_completion_dir') 2 | 3 | if zsh_completion_dir == '' 4 | zsh_completion_dir = get_option('datadir') / 'zsh' / 'site-functions' 5 | endif 6 | 7 | install_data('_bwrap', install_dir : zsh_completion_dir) 8 | -------------------------------------------------------------------------------- /Makefile-bwrap.am: -------------------------------------------------------------------------------- 1 | bwrap_SOURCES = \ 2 | $(bwrap_srcpath)/bubblewrap.c \ 3 | $(bwrap_srcpath)/bind-mount.h \ 4 | $(bwrap_srcpath)/bind-mount.c \ 5 | $(bwrap_srcpath)/network.h \ 6 | $(bwrap_srcpath)/network.c \ 7 | $(bwrap_srcpath)/utils.h \ 8 | $(bwrap_srcpath)/utils.c \ 9 | $(NULL) 10 | 11 | bwrap_CFLAGS = $(AM_CFLAGS) 12 | bwrap_LDADD = $(SELINUX_LIBS) 13 | -------------------------------------------------------------------------------- /autogen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | test -n "$srcdir" || srcdir=$(dirname "$0") 4 | test -n "$srcdir" || srcdir=. 5 | 6 | olddir=$(pwd) 7 | cd "$srcdir" 8 | 9 | if ! (autoreconf --version >/dev/null 2>&1); then 10 | echo "*** No autoreconf found, please install it ***" 11 | exit 1 12 | fi 13 | 14 | mkdir -p m4 15 | 16 | autoreconf --force --install --verbose 17 | 18 | cd "$olddir" 19 | test -n "$NOCONFIGURE" || "$srcdir/configure" "$@" 20 | -------------------------------------------------------------------------------- /tests/use-as-subproject/meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'use-bubblewrap-as-subproject', 3 | 'c', 4 | version : '0', 5 | meson_version : '>=0.49.0', 6 | ) 7 | 8 | configure_file( 9 | output : 'config.h', 10 | input : 'dummy-config.h.in', 11 | configuration : configuration_data(), 12 | ) 13 | 14 | subproject( 15 | 'bubblewrap', 16 | default_options : [ 17 | 'install_rpath=${ORIGIN}/../lib', 18 | 'program_prefix=not-flatpak-', 19 | ], 20 | ) 21 | -------------------------------------------------------------------------------- /Makefile-docs.am: -------------------------------------------------------------------------------- 1 | XSLTPROC = xsltproc 2 | 3 | XSLTPROC_FLAGS = \ 4 | --nonet \ 5 | --stringparam man.output.quietly 1 \ 6 | --stringparam funcsynopsis.style ansi \ 7 | --stringparam man.th.extra1.suppress 1 \ 8 | --stringparam man.authors.section.enabled 0 \ 9 | --stringparam man.copyright.section.enabled 0 10 | 11 | .xml.1: 12 | $(XSLTPROC) $(XSLTPROC_FLAGS) http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $< 13 | 14 | if ENABLE_MAN 15 | man_MANS = bwrap.1 16 | CLEANFILES += $(man_MANS) 17 | endif 18 | EXTRA_DIST += bwrap.xml 19 | -------------------------------------------------------------------------------- /release-checklist.md: -------------------------------------------------------------------------------- 1 | bubblewrap release checklist 2 | ============================ 3 | 4 | * Collect release notes 5 | * Update version number in `configure.ac` **and** `meson.build` 6 | * Commit the changes 7 | * `meson dist -C ${builddir}` 8 | * Do any final smoke-testing, e.g. update a package, install and test it 9 | * `git evtag sign v$VERSION` 10 | * Include the release notes in the tag message 11 | * `git push --atomic origin main v$VERSION` 12 | * https://github.com/containers/bubblewrap/releases/new 13 | * Fill in the new version's tag in the "Tag version" box 14 | * Title: `$VERSION` 15 | * Copy the release notes into the description 16 | * Upload the tarball that you built with `meson dist` 17 | * Get the `sha256sum` of the tarball and append it to the description 18 | * `Publish release` 19 | -------------------------------------------------------------------------------- /tests/use-as-subproject/assert-correct-rpath.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # Copyright 2022 Collabora Ltd. 3 | # SPDX-License-Identifier: LGPL-2.0-or-later 4 | 5 | import subprocess 6 | import sys 7 | 8 | if __name__ == '__main__': 9 | completed = subprocess.run( 10 | ['objdump', '-T', '-x', sys.argv[1]], 11 | stdout=subprocess.PIPE, 12 | ) 13 | stdout = completed.stdout 14 | assert stdout is not None 15 | seen_rpath = False 16 | 17 | for line in stdout.splitlines(): 18 | words = line.strip().split() 19 | 20 | if words and words[0] in (b'RPATH', b'RUNPATH'): 21 | print(line.decode(errors='backslashreplace')) 22 | assert len(words) == 2, words 23 | assert words[1] == b'${ORIGIN}/../lib', words 24 | seen_rpath = True 25 | 26 | assert seen_rpath 27 | -------------------------------------------------------------------------------- /tests/test-specifying-userns.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xeuo pipefail 4 | 5 | srcd=$(cd $(dirname "$0") && pwd) 6 | . "${srcd}/libtest.sh" 7 | 8 | echo "1..1" 9 | 10 | # This test needs user namespaces 11 | if test -n "${bwrap_is_suid:-}"; then 12 | echo "ok - # SKIP no setuid support for --unshare-user" 13 | else 14 | mkfifo donepipe 15 | 16 | $RUN --info-fd 42 --unshare-user sh -c 'readlink /proc/self/ns/user > sandbox-userns; cat < donepipe' >/dev/null 42>info.json & 17 | while ! test -f sandbox-userns; do sleep 1; done 18 | SANDBOX1PID=$(extract_child_pid info.json) 19 | 20 | $RUN --userns 11 readlink /proc/self/ns/user > sandbox2-userns 11< /proc/$SANDBOX1PID/ns/user 21 | echo foo > donepipe 22 | 23 | assert_files_equal sandbox-userns sandbox2-userns 24 | 25 | rm donepipe info.json sandbox-userns 26 | 27 | echo "ok - Test --userns" 28 | fi 29 | -------------------------------------------------------------------------------- /network.h: -------------------------------------------------------------------------------- 1 | /* bubblewrap 2 | * Copyright (C) 2016 Alexander Larsson 3 | * SPDX-License-Identifier: LGPL-2.0-or-later 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 2 of the License, or (at your option) any later version. 9 | * 10 | * This library is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this library. If not, see . 17 | * 18 | */ 19 | 20 | #pragma once 21 | 22 | void loopback_setup (void); 23 | -------------------------------------------------------------------------------- /tests/test-specifying-pidns.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xeuo pipefail 4 | 5 | srcd=$(cd $(dirname "$0") && pwd) 6 | . "${srcd}/libtest.sh" 7 | 8 | echo "1..1" 9 | 10 | # This test needs user namespaces 11 | if test -n "${bwrap_is_suid:-}"; then 12 | echo "ok - # SKIP no setuid support for --unshare-user" 13 | else 14 | mkfifo donepipe 15 | $RUN --info-fd 42 --unshare-user --unshare-pid sh -c 'readlink /proc/self/ns/pid > sandbox-pidns; cat < donepipe' >/dev/null 42>info.json & 16 | while ! test -f sandbox-pidns; do sleep 1; done 17 | SANDBOX1PID=$(extract_child_pid info.json) 18 | 19 | ASAN_OPTIONS=detect_leaks=0 LSAN_OPTIONS=detect_leaks=0 \ 20 | $RUN --userns 11 --pidns 12 readlink /proc/self/ns/pid > sandbox2-pidns 11< /proc/$SANDBOX1PID/ns/user 12< /proc/$SANDBOX1PID/ns/pid 21 | echo foo > donepipe 22 | 23 | assert_files_equal sandbox-pidns sandbox2-pidns 24 | 25 | rm donepipe info.json sandbox-pidns 26 | 27 | echo "ok - Test --pidns" 28 | fi 29 | -------------------------------------------------------------------------------- /demos/userns-block-fd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os, select, subprocess, sys, json 4 | 5 | pipe_info = os.pipe() 6 | userns_block = os.pipe() 7 | 8 | pid = os.fork() 9 | 10 | if pid != 0: 11 | os.close(pipe_info[1]) 12 | os.close(userns_block[0]) 13 | 14 | select.select([pipe_info[0]], [], []) 15 | 16 | data = json.load(os.fdopen(pipe_info[0])) 17 | child_pid = str(data['child-pid']) 18 | 19 | subprocess.call(["newuidmap", child_pid, "0", str(os.getuid()), "1"]) 20 | subprocess.call(["newgidmap", child_pid, "0", str(os.getgid()), "1"]) 21 | 22 | os.write(userns_block[1], b'1') 23 | else: 24 | os.close(pipe_info[0]) 25 | os.close(userns_block[1]) 26 | 27 | os.set_inheritable(pipe_info[1], True) 28 | os.set_inheritable(userns_block[0], True) 29 | 30 | args = ["bwrap", 31 | "bwrap", 32 | "--unshare-all", 33 | "--unshare-user", 34 | "--userns-block-fd", "%i" % userns_block[0], 35 | "--info-fd", "%i" % pipe_info[1], 36 | "--bind", "/", "/", 37 | "cat", "/proc/self/uid_map"] 38 | 39 | os.execlp(*args) 40 | -------------------------------------------------------------------------------- /completions/bash/meson.build: -------------------------------------------------------------------------------- 1 | bash_completion_dir = get_option('bash_completion_dir') 2 | 3 | if bash_completion_dir == '' 4 | bash_completion = dependency( 5 | 'bash-completion', 6 | version : '>=2.0', 7 | required : false, 8 | ) 9 | 10 | if bash_completion.found() 11 | if meson.version().version_compare('>=0.51.0') 12 | bash_completion_dir = bash_completion.get_variable( 13 | default_value: '', 14 | pkgconfig: 'completionsdir', 15 | pkgconfig_define: [ 16 | 'prefix', get_option('prefix'), 17 | 'datadir', get_option('prefix') / get_option('datadir'), 18 | ], 19 | ) 20 | else 21 | bash_completion_dir = bash_completion.get_pkgconfig_variable( 22 | 'completionsdir', 23 | default: '', 24 | define_variable: [ 25 | 'datadir', get_option('prefix') / get_option('datadir'), 26 | ], 27 | ) 28 | endif 29 | endif 30 | endif 31 | 32 | if bash_completion_dir == '' 33 | bash_completion_dir = get_option('datadir') / 'bash-completion' / 'completions' 34 | endif 35 | 36 | install_data('bwrap', install_dir : bash_completion_dir) 37 | -------------------------------------------------------------------------------- /demos/bubblewrap-shell.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Use bubblewrap to run /bin/sh reusing the host OS binaries (/usr), but with 3 | # separate /tmp, /home, /var, /run, and /etc. For /etc we just inherit the 4 | # host's resolv.conf, and set up "stub" passwd/group files. Not sharing 5 | # /home for example is intentional. If you wanted to, you could design 6 | # a bwrap-using program that shared individual parts of /home, perhaps 7 | # public content. 8 | # 9 | # Another way to build on this example is to remove --share-net to disable 10 | # networking. 11 | set -euo pipefail 12 | (exec bwrap --ro-bind /usr /usr \ 13 | --dir /tmp \ 14 | --dir /var \ 15 | --symlink ../tmp var/tmp \ 16 | --proc /proc \ 17 | --dev /dev \ 18 | --ro-bind /etc/resolv.conf /etc/resolv.conf \ 19 | --symlink usr/lib /lib \ 20 | --symlink usr/lib64 /lib64 \ 21 | --symlink usr/bin /bin \ 22 | --symlink usr/sbin /sbin \ 23 | --chdir / \ 24 | --unshare-all \ 25 | --share-net \ 26 | --die-with-parent \ 27 | --dir /run/user/$(id -u) \ 28 | --setenv XDG_RUNTIME_DIR "/run/user/`id -u`" \ 29 | --setenv PS1 "bwrap-demo$ " \ 30 | --file 11 /etc/passwd \ 31 | --file 12 /etc/group \ 32 | /bin/sh) \ 33 | 11< <(getent passwd $UID 65534) \ 34 | 12< <(getent group $(id -g) 65534) 35 | -------------------------------------------------------------------------------- /completions/bash/bwrap: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | # bash completion file for bubblewrap commands 4 | # 5 | 6 | _bwrap() { 7 | local cur prev words cword 8 | _init_completion || return 9 | 10 | # Please keep sorted in LC_ALL=C order 11 | local boolean_options=" 12 | --as-pid-1 13 | --assert-userns-disabled 14 | --clearenv 15 | --disable-userns 16 | --help 17 | --new-session 18 | --unshare-all 19 | --unshare-cgroup 20 | --unshare-cgroup-try 21 | --unshare-ipc 22 | --unshare-net 23 | --unshare-pid 24 | --unshare-user 25 | --unshare-user-try 26 | --unshare-uts 27 | --version 28 | " 29 | 30 | # Please keep sorted in LC_ALL=C order 31 | local options_with_args=" 32 | $boolean_optons 33 | --add-seccomp-fd 34 | --args 35 | --argv0 36 | --bind 37 | --bind-data 38 | --block-fd 39 | --cap-add 40 | --cap-drop 41 | --chdir 42 | --chmod 43 | --dev 44 | --dev-bind 45 | --die-with-parent 46 | --dir 47 | --exec-label 48 | --file 49 | --file-label 50 | --gid 51 | --hostname 52 | --info-fd 53 | --lock-file 54 | --perms 55 | --proc 56 | --remount-ro 57 | --ro-bind 58 | --seccomp 59 | --setenv 60 | --size 61 | --symlink 62 | --sync-fd 63 | --uid 64 | --unsetenv 65 | --userns-block-fd 66 | " 67 | 68 | if [[ "$cur" == -* ]]; then 69 | COMPREPLY=( $( compgen -W "$boolean_options $options_with_args" -- "$cur" ) ) 70 | fi 71 | 72 | return 0 73 | } 74 | complete -F _bwrap bwrap 75 | 76 | # vim:set ft=bash: 77 | -------------------------------------------------------------------------------- /packaging/bubblewrap.spec: -------------------------------------------------------------------------------- 1 | %global commit0 66d12bb23b04e201c5846e325f0b10930ed802f8 2 | %global shortcommit0 %(c=%{commit0}; echo ${c:0:7}) 3 | 4 | Summary: Core execution tool for unprivileged containers 5 | Name: bubblewrap 6 | Version: 0 7 | Release: 1%{?dist} 8 | #VCS: git:https://github.com/projectatomic/bubblewrap 9 | Source0: https://github.com/projectatomic/%{name}/archive/%{commit0}.tar.gz#/%{name}-%{shortcommit0}.tar.gz 10 | License: LGPLv2+ 11 | URL: https://github.com/projectatomic/bubblewrap 12 | 13 | BuildRequires: git 14 | # We always run autogen.sh 15 | BuildRequires: autoconf automake libtool 16 | BuildRequires: libcap-devel 17 | BuildRequires: pkgconfig(libselinux) 18 | BuildRequires: libxslt 19 | BuildRequires: docbook-style-xsl 20 | 21 | %description 22 | Bubblewrap (/usr/bin/bwrap) is a core execution engine for unprivileged 23 | containers that works as a setuid binary on kernels without 24 | user namespaces. 25 | 26 | %prep 27 | %autosetup -Sgit -n %{name}-%{version} 28 | 29 | %build 30 | env NOCONFIGURE=1 ./autogen.sh 31 | %configure --disable-silent-rules --with-priv-mode=none 32 | 33 | make %{?_smp_mflags} 34 | 35 | %install 36 | make install DESTDIR=$RPM_BUILD_ROOT INSTALL="install -p -c" 37 | find $RPM_BUILD_ROOT -name '*.la' -delete 38 | 39 | %files 40 | %license COPYING 41 | %doc README.md 42 | %{_datadir}/bash-completion/completions/bwrap 43 | %if (0%{?rhel} != 0 && 0%{?rhel} <= 7) 44 | %attr(4755,root,root) %{_bindir}/bwrap 45 | %else 46 | %{_bindir}/bwrap 47 | %endif 48 | %{_mandir}/man1/* 49 | -------------------------------------------------------------------------------- /tests/meson.build: -------------------------------------------------------------------------------- 1 | test_programs = [ 2 | ['test-utils', executable( 3 | 'test-utils', 4 | 'test-utils.c', 5 | '../utils.c', 6 | '../utils.h', 7 | dependencies : [selinux_dep], 8 | include_directories : common_include_directories, 9 | )], 10 | ] 11 | 12 | executable( 13 | 'try-syscall', 14 | 'try-syscall.c', 15 | override_options: ['b_sanitize=none'], 16 | ) 17 | 18 | test_scripts = [ 19 | 'test-run.sh', 20 | 'test-seccomp.py', 21 | 'test-specifying-pidns.sh', 22 | 'test-specifying-userns.sh', 23 | ] 24 | 25 | test_env = environment() 26 | test_env.set('BWRAP', bwrap.full_path()) 27 | test_env.set('G_TEST_BUILDDIR', meson.current_build_dir() / '..') 28 | test_env.set('G_TEST_SRCDIR', meson.current_source_dir() / '..') 29 | 30 | foreach pair : test_programs 31 | name = pair[0] 32 | test_program = pair[1] 33 | if meson.version().version_compare('>=0.50.0') 34 | test( 35 | name, 36 | test_program, 37 | env : test_env, 38 | protocol : 'tap', 39 | ) 40 | else 41 | test( 42 | name, 43 | test_program, 44 | env : test_env, 45 | ) 46 | endif 47 | endforeach 48 | 49 | foreach test_script : test_scripts 50 | if test_script.endswith('.py') 51 | interpreter = python 52 | else 53 | interpreter = bash 54 | endif 55 | 56 | if meson.version().version_compare('>=0.50.0') 57 | test( 58 | test_script, 59 | interpreter, 60 | args : [files(test_script)], 61 | env : test_env, 62 | protocol : 'tap', 63 | ) 64 | else 65 | test( 66 | test_script, 67 | interpreter, 68 | args : [files(test_script)], 69 | env : test_env, 70 | ) 71 | endif 72 | endforeach 73 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option( 2 | 'bash_completion', 3 | type : 'feature', 4 | description : 'install bash completion script', 5 | value : 'enabled', 6 | ) 7 | option( 8 | 'bash_completion_dir', 9 | type : 'string', 10 | description : 'install bash completion script in this directory', 11 | value : '', 12 | ) 13 | option( 14 | 'bwrapdir', 15 | type : 'string', 16 | description : 'install bwrap in this directory [default: bindir, or libexecdir in subprojects]', 17 | ) 18 | option( 19 | 'build_rpath', 20 | type : 'string', 21 | description : 'set a RUNPATH or RPATH on the bwrap executable', 22 | ) 23 | option( 24 | 'install_rpath', 25 | type : 'string', 26 | description : 'set a RUNPATH or RPATH on the bwrap executable', 27 | ) 28 | option( 29 | 'man', 30 | type : 'feature', 31 | description : 'generate man pages', 32 | value : 'auto', 33 | ) 34 | option( 35 | 'program_prefix', 36 | type : 'string', 37 | description : 'Prepend string to bwrap executable name, for use with subprojects', 38 | ) 39 | option( 40 | 'python', 41 | type : 'string', 42 | description : 'Path to Python 3, or empty to use python3', 43 | ) 44 | option( 45 | 'require_userns', 46 | type : 'boolean', 47 | description : 'require user namespaces by default when installed setuid', 48 | value : 'false', 49 | ) 50 | option( 51 | 'selinux', 52 | type : 'feature', 53 | description : 'enable optional SELINUX support', 54 | value : 'auto', 55 | ) 56 | option( 57 | 'tests', 58 | type : 'boolean', 59 | description : 'build tests', 60 | value : 'true', 61 | ) 62 | option( 63 | 'zsh_completion', 64 | type : 'feature', 65 | description : 'install zsh completion script', 66 | value : 'enabled', 67 | ) 68 | option( 69 | 'zsh_completion_dir', 70 | type : 'string', 71 | description : 'install zsh completion script in this directory', 72 | value : '', 73 | ) 74 | -------------------------------------------------------------------------------- /bind-mount.h: -------------------------------------------------------------------------------- 1 | /* bubblewrap 2 | * Copyright (C) 2016 Alexander Larsson 3 | * SPDX-License-Identifier: LGPL-2.0-or-later 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 2 of the License, or (at your option) any later version. 9 | * 10 | * This library is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this library. If not, see . 17 | * 18 | */ 19 | 20 | #pragma once 21 | 22 | #include "utils.h" 23 | 24 | typedef enum { 25 | BIND_READONLY = (1 << 0), 26 | BIND_DEVICES = (1 << 2), 27 | BIND_RECURSIVE = (1 << 3), 28 | } bind_option_t; 29 | 30 | typedef enum 31 | { 32 | BIND_MOUNT_SUCCESS = 0, 33 | BIND_MOUNT_ERROR_MOUNT, 34 | BIND_MOUNT_ERROR_REALPATH_DEST, 35 | BIND_MOUNT_ERROR_REOPEN_DEST, 36 | BIND_MOUNT_ERROR_READLINK_DEST_PROC_FD, 37 | BIND_MOUNT_ERROR_FIND_DEST_MOUNT, 38 | BIND_MOUNT_ERROR_REMOUNT_DEST, 39 | BIND_MOUNT_ERROR_REMOUNT_SUBMOUNT, 40 | } bind_mount_result; 41 | 42 | bind_mount_result bind_mount (int proc_fd, 43 | const char *src, 44 | const char *dest, 45 | bind_option_t options, 46 | char **failing_path); 47 | 48 | void die_with_bind_result (bind_mount_result res, 49 | int saved_errno, 50 | const char *failing_path, 51 | const char *format, 52 | ...) 53 | __attribute__((__noreturn__)) 54 | __attribute__((format (printf, 4, 5))); 55 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security and Disclosure Information Policy for the bubblewrap Project 2 | 3 | The bubblewrap Project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/HEAD/SECURITY.md) for the Containers Projects. 4 | 5 | ### System security 6 | 7 | If bubblewrap is setuid root, then the goal is that it does not allow 8 | a malicious local user to do anything that would not have been possible 9 | on a kernel that allows unprivileged users to create new user namespaces. 10 | For example, [CVE-2020-5291](https://github.com/containers/bubblewrap/security/advisories/GHSA-j2qp-rvxj-43vj) 11 | was treated as a security vulnerability in bubblewrap. 12 | 13 | If bubblewrap is not setuid root, then it is not a security boundary 14 | between the user and the OS, because anything bubblewrap could do, a 15 | malicious user could equally well do by writing their own tool equivalent 16 | to bubblewrap. 17 | 18 | ### Sandbox security 19 | 20 | bubblewrap is a toolkit for constructing sandbox environments. 21 | bubblewrap is not a complete, ready-made sandbox with a specific security 22 | policy. 23 | 24 | Some of bubblewrap's use-cases want a security boundary between the sandbox 25 | and the real system; other use-cases want the ability to change the layout of 26 | the filesystem for processes inside the sandbox, but do not aim to be a 27 | security boundary. 28 | As a result, the level of protection between the sandboxed processes and 29 | the host system is entirely determined by the arguments passed to 30 | bubblewrap. 31 | 32 | Whatever program constructs the command-line arguments for bubblewrap 33 | (often a larger framework like Flatpak, libgnome-desktop, sandwine 34 | or an ad-hoc script) is responsible for defining its own security model, 35 | and choosing appropriate bubblewrap command-line arguments to implement 36 | that security model. 37 | 38 | For example, 39 | [CVE-2017-5226](https://github.com/flatpak/flatpak/security/advisories/GHSA-7gfv-rvfx-h87x) 40 | (in which a Flatpak app could send input to a parent terminal using the 41 | `TIOCSTI` ioctl) is considered to be a Flatpak vulnerability, not a 42 | bubblewrap vulnerability. 43 | -------------------------------------------------------------------------------- /ci/builddeps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2021 Simon McVittie 3 | # SPDX-License-Identifier: LGPL-2.0-or-later 4 | 5 | set -eux 6 | set -o pipefail 7 | 8 | usage() { 9 | if [ "${1-2}" -ne 0 ]; then 10 | exec >&2 11 | fi 12 | cat <&2 46 | usage 2 47 | ;; 48 | esac 49 | done 50 | 51 | # No more arguments please 52 | for arg in "$@"; do 53 | usage 2 54 | done 55 | 56 | if dpkg-vendor --derives-from Debian; then 57 | apt-get -y update 58 | apt-get -q -y install \ 59 | autoconf \ 60 | automake \ 61 | build-essential \ 62 | docbook-xml \ 63 | docbook-xsl \ 64 | libcap-dev \ 65 | libselinux1-dev \ 66 | libtool \ 67 | meson \ 68 | pkg-config \ 69 | python3 \ 70 | xsltproc \ 71 | ${NULL+} 72 | 73 | if [ -n "${opt_clang}" ]; then 74 | apt-get -y install clang 75 | fi 76 | 77 | exit 0 78 | fi 79 | 80 | if command -v yum; then 81 | yum -y install \ 82 | 'pkgconfig(libselinux)' \ 83 | /usr/bin/eu-readelf \ 84 | autoconf \ 85 | automake \ 86 | docbook-style-xsl \ 87 | gcc \ 88 | git \ 89 | libasan \ 90 | libcap-devel \ 91 | libtool \ 92 | libtsan \ 93 | libubsan \ 94 | libxslt \ 95 | make \ 96 | meson \ 97 | redhat-rpm-config \ 98 | rsync \ 99 | ${NULL+} 100 | 101 | if [ -n "${opt_clang}" ]; then 102 | yum -y install clang 103 | fi 104 | 105 | exit 0 106 | fi 107 | 108 | echo "Unknown distribution" >&2 109 | exit 1 110 | 111 | # vim:set sw=4 sts=4 et: 112 | -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | AM_CFLAGS = $(WARN_CFLAGS) 2 | CLEANFILES = 3 | EXTRA_DIST = \ 4 | .dir-locals.el \ 5 | .editorconfig \ 6 | README.md \ 7 | autogen.sh \ 8 | completions/bash/meson.build \ 9 | completions/meson.build \ 10 | completions/zsh/meson.build \ 11 | demos/bubblewrap-shell.sh \ 12 | demos/flatpak-run.sh \ 13 | demos/flatpak.bpf \ 14 | demos/userns-block-fd.py \ 15 | meson.build \ 16 | meson_options.txt \ 17 | packaging/bubblewrap.spec \ 18 | tests/meson.build \ 19 | tests/use-as-subproject/README \ 20 | tests/use-as-subproject/config.h \ 21 | tests/use-as-subproject/dummy-config.h.in \ 22 | tests/use-as-subproject/meson.build \ 23 | uncrustify.cfg \ 24 | uncrustify.sh \ 25 | $(NULL) 26 | 27 | GITIGNOREFILES = build-aux/ gtk-doc.make config.h.in aclocal.m4 28 | 29 | bin_PROGRAMS = bwrap 30 | 31 | bwrap_srcpath := $(srcdir) 32 | include Makefile-bwrap.am 33 | 34 | install-exec-hook: 35 | if PRIV_MODE_SETUID 36 | $(SUDO_BIN) chown root $(DESTDIR)$(bindir)/bwrap 37 | $(SUDO_BIN) chmod u+s $(DESTDIR)$(bindir)/bwrap 38 | endif 39 | 40 | test_programs = \ 41 | tests/test-utils \ 42 | $(NULL) 43 | test_scripts = \ 44 | tests/test-run.sh \ 45 | tests/test-seccomp.py \ 46 | tests/test-specifying-userns.sh \ 47 | tests/test-specifying-pidns.sh \ 48 | $(NULL) 49 | test_extra_programs = \ 50 | test-bwrap \ 51 | tests/try-syscall \ 52 | $(NULL) 53 | 54 | test-bwrap$(EXEEXT): bwrap 55 | rm -rf test-bwrap 56 | cp bwrap test-bwrap 57 | chmod 0755 test-bwrap 58 | if PRIV_MODE_SETUID 59 | $(SUDO_BIN) chown root test-bwrap 60 | $(SUDO_BIN) chmod u+s test-bwrap 61 | endif 62 | 63 | tests_test_utils_SOURCES = \ 64 | tests/test-utils.c \ 65 | utils.h \ 66 | utils.c \ 67 | $(NULL) 68 | tests_test_utils_LDADD = $(SELINUX_LIBS) 69 | 70 | test_bwrap_SOURCES= 71 | 72 | include Makefile-docs.am 73 | 74 | LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) $(top_srcdir)/build-aux/tap-driver.sh 75 | LOG_COMPILER = 76 | TESTS_ENVIRONMENT = \ 77 | BWRAP=$(abs_top_builddir)/test-bwrap \ 78 | G_TEST_BUILDDIR=$(abs_top_builddir) \ 79 | G_TEST_SRCDIR=$(abs_top_srcdir) \ 80 | $(NULL) 81 | check_PROGRAMS = $(test_programs) $(test_extra_programs) 82 | TESTS = $(test_programs) $(test_scripts) 83 | 84 | EXTRA_DIST += $(test_scripts) 85 | EXTRA_DIST += tests/libtest-core.sh 86 | EXTRA_DIST += tests/libtest.sh 87 | 88 | if ENABLE_BASH_COMPLETION 89 | bashcompletiondir = $(BASH_COMPLETION_DIR) 90 | dist_bashcompletion_DATA = completions/bash/bwrap 91 | endif 92 | 93 | if ENABLE_ZSH_COMPLETION 94 | zshcompletiondir = $(ZSH_COMPLETION_DIR) 95 | dist_zshcompletion_DATA = completions/zsh/_bwrap 96 | endif 97 | 98 | -include $(top_srcdir)/git.mk 99 | 100 | AM_DISTCHECK_CONFIGURE_FLAGS = \ 101 | --with-bash-completion-dir="\$(datadir)"/bash-completion/ \ 102 | $(NULL) 103 | -------------------------------------------------------------------------------- /demos/flatpak-run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # For this to work you first have to run these commands: 3 | # curl -O http://sdk.gnome.org/nightly/keys/nightly.gpg 4 | # flatpak --user remote-add --gpg-key=nightly.gpg gnome-nightly http://sdk.gnome.org/nightly/repo/ 5 | # flatpak --user install gnome-nightly org.gnome.Platform 6 | # flatpak --user install gnome-nightly org.gnome.Weather 7 | 8 | mkdir -p ~/.var/app/org.gnome.Weather/cache ~/.var/app/org.gnome.Weather/config ~/.var/app/org.gnome.Weather/data 9 | 10 | ( 11 | exec bwrap \ 12 | --ro-bind ~/.local/share/flatpak/runtime/org.gnome.Platform/x86_64/master/active/files /usr \ 13 | --lock-file /usr/.ref \ 14 | --ro-bind ~/.local/share/flatpak/app/org.gnome.Weather/x86_64/master/active/files/ /app \ 15 | --lock-file /app/.ref \ 16 | --dev /dev \ 17 | --proc /proc \ 18 | --dir /tmp \ 19 | --symlink /tmp /var/tmp \ 20 | --symlink /run /var/run \ 21 | --symlink usr/lib /lib \ 22 | --symlink usr/lib64 /lib64 \ 23 | --symlink usr/bin /bin \ 24 | --symlink usr/sbin /sbin \ 25 | --symlink usr/etc /etc \ 26 | --dir /run/user/`id -u` \ 27 | --ro-bind /etc/machine-id /usr/etc/machine-id \ 28 | --ro-bind /etc/resolv.conf /run/host/monitor/resolv.conf \ 29 | --ro-bind /sys/block /sys/block \ 30 | --ro-bind /sys/bus /sys/bus \ 31 | --ro-bind /sys/class /sys/class \ 32 | --ro-bind /sys/dev /sys/dev \ 33 | --ro-bind /sys/devices /sys/devices \ 34 | --dev-bind /dev/dri /dev/dri \ 35 | --bind /tmp/.X11-unix/X0 /tmp/.X11-unix/X99 \ 36 | --bind ~/.var/app/org.gnome.Weather ~/.var/app/org.gnome.Weather \ 37 | --bind ~/.config/dconf ~/.config/dconf \ 38 | --bind /run/user/`id -u`/dconf /run/user/`id -u`/dconf \ 39 | --unshare-pid \ 40 | --setenv XDG_RUNTIME_DIR "/run/user/`id -u`" \ 41 | --setenv DISPLAY :99 \ 42 | --setenv GI_TYPELIB_PATH /app/lib/girepository-1.0 \ 43 | --setenv GST_PLUGIN_PATH /app/lib/gstreamer-1.0 \ 44 | --setenv LD_LIBRARY_PATH /app/lib:/usr/lib/GL \ 45 | --setenv DCONF_USER_CONFIG_DIR .config/dconf \ 46 | --setenv PATH /app/bin:/usr/bin \ 47 | --setenv XDG_CONFIG_DIRS /app/etc/xdg:/etc/xdg \ 48 | --setenv XDG_DATA_DIRS /app/share:/usr/share \ 49 | --setenv SHELL /bin/sh \ 50 | --setenv XDG_CACHE_HOME ~/.var/app/org.gnome.Weather/cache \ 51 | --setenv XDG_CONFIG_HOME ~/.var/app/org.gnome.Weather/config \ 52 | --setenv XDG_DATA_HOME ~/.var/app/org.gnome.Weather/data \ 53 | --file 10 /run/user/`id -u`/flatpak-info \ 54 | --bind-data 11 /usr/etc/passwd \ 55 | --bind-data 12 /usr/etc/group \ 56 | --seccomp 13 \ 57 | /bin/sh) \ 58 | 11< <(getent passwd $UID 65534 ) \ 59 | 12< <(getent group $(id -g) 65534) \ 60 | 13< `dirname $0`/flatpak.bpf \ 61 | 10< 7 | # SPDX-License-Identifier: LGPL-2.0-or-later 8 | # 9 | # This library is free software; you can redistribute it and/or 10 | # modify it under the terms of the GNU Lesser General Public 11 | # License as published by the Free Software Foundation; either 12 | # version 2 of the License, or (at your option) any later version. 13 | # 14 | # This library is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 | # Lesser General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Lesser General Public 20 | # License along with this library; if not, write to the 21 | # Free Software Foundation, Inc., 59 Temple Place - Suite 330, 22 | # Boston, MA 02111-1307, USA. 23 | 24 | set -e 25 | 26 | if [ -n "${G_TEST_SRCDIR:-}" ]; then 27 | test_srcdir="${G_TEST_SRCDIR}/tests" 28 | else 29 | test_srcdir=$(dirname "$0") 30 | fi 31 | 32 | if [ -n "${G_TEST_BUILDDIR:-}" ]; then 33 | test_builddir="${G_TEST_BUILDDIR}/tests" 34 | else 35 | test_builddir=$(dirname "$0") 36 | fi 37 | 38 | . "${test_srcdir}/libtest-core.sh" 39 | 40 | # Make sure /sbin/getpcaps etc. are in our PATH even if non-root 41 | PATH="$PATH:/usr/sbin:/sbin" 42 | 43 | tempdir=$(mktemp -d /var/tmp/tap-test.XXXXXX) 44 | touch "${tempdir}/.testtmp" 45 | cleanup() { 46 | if test -n "${TEST_SKIP_CLEANUP:-}"; then 47 | echo "Skipping cleanup of ${tempdir}" 48 | elif test -f "${tempdir}/.testtmp"; then 49 | rm -rf "${tempdir}" 50 | fi 51 | } 52 | trap cleanup EXIT 53 | cd "${tempdir}" 54 | 55 | : "${BWRAP:=bwrap}" 56 | if test -u "$(type -p ${BWRAP})"; then 57 | bwrap_is_suid=true 58 | fi 59 | 60 | FUSE_DIR= 61 | for mp in $(grep " fuse[. ]" /proc/self/mounts | grep "user_id=$(id -u)" | awk '{print $2}'); do 62 | if test -d "$mp"; then 63 | echo "# Using $mp as test fuse mount" 64 | FUSE_DIR="$mp" 65 | break 66 | fi 67 | done 68 | 69 | if test "$(id -u)" = "0"; then 70 | is_uidzero=true 71 | else 72 | is_uidzero=false 73 | fi 74 | 75 | # This is supposed to be an otherwise readable file in an unreadable (by the user) dir 76 | UNREADABLE=/root/.bashrc 77 | if "${is_uidzero}" || test -x "$(dirname "$UNREADABLE")"; then 78 | UNREADABLE= 79 | fi 80 | 81 | # https://github.com/projectatomic/bubblewrap/issues/217 82 | # are we on a merged-/usr system? 83 | if [ /lib -ef /usr/lib ]; then 84 | BWRAP_RO_HOST_ARGS="--ro-bind /usr /usr 85 | --ro-bind /etc /etc 86 | --dir /var/tmp 87 | --symlink usr/lib /lib 88 | --symlink usr/lib64 /lib64 89 | --symlink usr/bin /bin 90 | --symlink usr/sbin /sbin 91 | --proc /proc 92 | --dev /dev" 93 | else 94 | BWRAP_RO_HOST_ARGS="--ro-bind /usr /usr 95 | --ro-bind /etc /etc 96 | --ro-bind /bin /bin 97 | --ro-bind-try /lib /lib 98 | --ro-bind-try /lib64 /lib64 99 | --ro-bind-try /sbin /sbin 100 | --ro-bind-try /nix/store /nix/store 101 | --dir /var/tmp 102 | --proc /proc 103 | --dev /dev" 104 | fi 105 | 106 | # Default arg, bind whole host fs to /, tmpfs on /tmp 107 | RUN="${BWRAP} --bind / / --tmpfs /tmp" 108 | 109 | if [ -z "${BWRAP_MUST_WORK-}" ] && ! $RUN true; then 110 | skip Seems like bwrap is not working at all. Maybe setuid is not working 111 | fi 112 | 113 | extract_child_pid() { 114 | grep child-pid "$1" | sed "s/^.*: \([0-9]*\).*/\1/" 115 | } 116 | -------------------------------------------------------------------------------- /uncrustify.cfg: -------------------------------------------------------------------------------- 1 | newlines lf 2 | 3 | input_tab_size 8 4 | output_tab_size 8 5 | 6 | string_escape_char 92 7 | string_escape_char2 0 8 | 9 | # indenting 10 | indent_columns 2 11 | indent_with_tabs 0 12 | indent_align_string True 13 | indent_brace 2 14 | indent_braces false 15 | indent_braces_no_func True 16 | indent_func_call_param false 17 | indent_func_def_param false 18 | indent_func_proto_param false 19 | indent_switch_case 0 20 | indent_case_brace 2 21 | indent_paren_close 1 22 | 23 | # spacing 24 | sp_arith Add 25 | sp_assign Add 26 | sp_enum_assign Add 27 | sp_bool Add 28 | sp_compare Add 29 | sp_inside_paren Remove 30 | sp_inside_fparens Remove 31 | sp_func_def_paren Force 32 | sp_func_proto_paren Force 33 | sp_paren_paren Remove 34 | sp_balance_nested_parens False 35 | sp_paren_brace Remove 36 | sp_before_square Remove 37 | sp_before_squares Remove 38 | sp_inside_square Remove 39 | sp_before_ptr_star Add 40 | sp_between_ptr_star Remove 41 | sp_after_comma Add 42 | sp_before_comma Remove 43 | sp_after_cast Add 44 | sp_sizeof_paren Add 45 | sp_not Remove 46 | sp_inv Remove 47 | sp_addr Remove 48 | sp_member Remove 49 | sp_deref Remove 50 | sp_sign Remove 51 | sp_incdec Remove 52 | sp_attribute_paren remove 53 | sp_macro Force 54 | sp_func_call_paren Force 55 | sp_func_call_user_paren Remove 56 | set func_call_user _ N_ C_ g_autoptr g_auto 57 | sp_brace_typedef add 58 | sp_cond_colon add 59 | sp_cond_question add 60 | sp_defined_paren remove 61 | 62 | # alignment 63 | align_keep_tabs False 64 | align_with_tabs False 65 | align_on_tabstop False 66 | align_number_left True 67 | align_func_params True 68 | align_var_def_span 0 69 | align_var_def_amp_style 1 70 | align_var_def_colon true 71 | align_enum_equ_span 0 72 | align_var_struct_span 2 73 | align_var_def_star_style 2 74 | align_var_def_amp_style 2 75 | align_typedef_span 2 76 | align_typedef_func 0 77 | align_typedef_star_style 2 78 | align_typedef_amp_style 2 79 | 80 | # newlines 81 | nl_assign_leave_one_liners True 82 | nl_enum_leave_one_liners False 83 | nl_func_leave_one_liners False 84 | nl_if_leave_one_liners False 85 | nl_end_of_file Add 86 | nl_assign_brace Remove 87 | nl_func_var_def_blk 1 88 | nl_fcall_brace Add 89 | nl_enum_brace Remove 90 | nl_struct_brace Force 91 | nl_union_brace Force 92 | nl_if_brace Force 93 | nl_brace_else Force 94 | nl_elseif_brace Force 95 | nl_else_brace Add 96 | nl_for_brace Force 97 | nl_while_brace Force 98 | nl_do_brace Force 99 | nl_brace_while Force 100 | nl_switch_brace Force 101 | nl_before_case True 102 | nl_after_case False 103 | nl_func_type_name Force 104 | nl_func_proto_type_name Remove 105 | nl_func_paren Remove 106 | nl_func_decl_start Remove 107 | nl_func_decl_args Force 108 | nl_func_decl_end Remove 109 | nl_fdef_brace Force 110 | nl_after_return False 111 | nl_define_macro False 112 | nl_create_if_one_liner False 113 | nl_create_for_one_liner False 114 | nl_create_while_one_liner False 115 | nl_after_semicolon True 116 | nl_multi_line_cond true 117 | 118 | # mod 119 | # I'd like these to be remove, but that removes brackets in if { if { foo } }, which i dislike 120 | # Not clear what to do about that... 121 | mod_full_brace_for Remove 122 | mod_full_brace_if Remove 123 | mod_full_brace_if_chain True 124 | mod_full_brace_while Remove 125 | mod_full_brace_do Remove 126 | mod_full_brace_nl 3 127 | mod_paren_on_return Remove 128 | 129 | # line splitting 130 | #code_width = 78 131 | ls_for_split_full True 132 | ls_func_split_full True 133 | 134 | # positioning 135 | pos_bool Trail 136 | pos_conditional Trail 137 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'bubblewrap', 3 | 'c', 4 | version : '0.10.0', 5 | meson_version : '>=0.49.0', 6 | default_options : [ 7 | 'warning_level=2', 8 | ], 9 | ) 10 | 11 | cc = meson.get_compiler('c') 12 | add_project_arguments('-D_GNU_SOURCE', language : 'c') 13 | common_include_directories = include_directories('.') 14 | 15 | # Keep this in sync with ostree, except remove -Wall (part of Meson 16 | # warning_level 2) and -Werror=declaration-after-statement 17 | add_project_arguments( 18 | cc.get_supported_arguments([ 19 | '-Werror=shadow', 20 | '-Werror=empty-body', 21 | '-Werror=strict-prototypes', 22 | '-Werror=missing-prototypes', 23 | '-Werror=implicit-function-declaration', 24 | '-Werror=pointer-arith', 25 | '-Werror=init-self', 26 | '-Werror=missing-declarations', 27 | '-Werror=return-type', 28 | '-Werror=overflow', 29 | '-Werror=int-conversion', 30 | '-Werror=parenthesis', 31 | '-Werror=incompatible-pointer-types', 32 | '-Werror=misleading-indentation', 33 | '-Werror=missing-include-dirs', 34 | '-Werror=aggregate-return', 35 | 36 | # Extra warnings specific to bubblewrap 37 | '-Werror=switch-default', 38 | '-Wswitch-enum', 39 | 40 | # Deliberately not warning about these, ability to zero-initialize 41 | # a struct is a feature 42 | '-Wno-missing-field-initializers', 43 | '-Wno-error=missing-field-initializers', 44 | ]), 45 | language : 'c', 46 | ) 47 | 48 | if ( 49 | cc.has_argument('-Werror=format=2') 50 | and cc.has_argument('-Werror=format-security') 51 | and cc.has_argument('-Werror=format-nonliteral') 52 | ) 53 | add_project_arguments([ 54 | '-Werror=format=2', 55 | '-Werror=format-security', 56 | '-Werror=format-nonliteral', 57 | ], language : 'c') 58 | endif 59 | 60 | bash = find_program('bash', required : false) 61 | 62 | if get_option('python') == '' 63 | python = find_program('python3') 64 | else 65 | python = find_program(get_option('python')) 66 | endif 67 | 68 | libcap_dep = dependency('libcap', required : true) 69 | 70 | selinux_dep = dependency( 71 | 'libselinux', 72 | version : '>=2.1.9', 73 | # if disabled, Meson will behave as though libselinux was not found 74 | required : get_option('selinux'), 75 | ) 76 | 77 | cdata = configuration_data() 78 | cdata.set_quoted( 79 | 'PACKAGE_STRING', 80 | '@0@ @1@'.format(meson.project_name(), meson.project_version()), 81 | ) 82 | 83 | if selinux_dep.found() 84 | cdata.set('HAVE_SELINUX', 1) 85 | if selinux_dep.version().version_compare('>=2.3') 86 | cdata.set('HAVE_SELINUX_2_3', 1) 87 | endif 88 | endif 89 | 90 | if get_option('require_userns') 91 | cdata.set('ENABLE_REQUIRE_USERNS', 1) 92 | endif 93 | 94 | configure_file( 95 | output : 'config.h', 96 | configuration : cdata, 97 | ) 98 | 99 | if meson.is_subproject() and get_option('program_prefix') == '' 100 | error('program_prefix option must be set when bwrap is a subproject') 101 | endif 102 | 103 | if get_option('bwrapdir') != '' 104 | bwrapdir = get_option('bwrapdir') 105 | elif meson.is_subproject() 106 | bwrapdir = get_option('libexecdir') 107 | else 108 | bwrapdir = get_option('bindir') 109 | endif 110 | 111 | bwrap = executable( 112 | get_option('program_prefix') + 'bwrap', 113 | [ 114 | 'bubblewrap.c', 115 | 'bind-mount.c', 116 | 'network.c', 117 | 'utils.c', 118 | ], 119 | build_rpath : get_option('build_rpath'), 120 | install : true, 121 | install_dir : bwrapdir, 122 | install_rpath : get_option('install_rpath'), 123 | dependencies : [selinux_dep, libcap_dep], 124 | ) 125 | 126 | manpages_xsl = 'http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl' 127 | xsltproc = find_program('xsltproc', required : get_option('man')) 128 | build_man_page = false 129 | 130 | if xsltproc.found() and not meson.is_subproject() 131 | if run_command([ 132 | xsltproc, '--nonet', manpages_xsl, 133 | ], check : false).returncode() == 0 134 | message('Docbook XSL found, man page enabled') 135 | build_man_page = true 136 | elif get_option('man').enabled() 137 | error('Man page requested, but Docbook XSL stylesheets not found') 138 | else 139 | message('Docbook XSL not found, man page disabled automatically') 140 | endif 141 | endif 142 | 143 | if build_man_page 144 | custom_target( 145 | 'bwrap.1', 146 | output : 'bwrap.1', 147 | input : 'bwrap.xml', 148 | command : [ 149 | xsltproc, 150 | '--nonet', 151 | '--stringparam', 'man.output.quietly', '1', 152 | '--stringparam', 'funcsynopsis.style', 'ansi', 153 | '--stringparam', 'man.th.extra1.suppress', '1', 154 | '--stringparam', 'man.authors.section.enabled', '0', 155 | '--stringparam', 'man.copyright.section.enabled', '0', 156 | '-o', '@OUTPUT@', 157 | manpages_xsl, 158 | '@INPUT@', 159 | ], 160 | install : true, 161 | install_dir : get_option('mandir') / 'man1', 162 | ) 163 | endif 164 | 165 | if not meson.is_subproject() 166 | subdir('completions') 167 | endif 168 | 169 | if get_option('tests') 170 | subdir('tests') 171 | endif 172 | -------------------------------------------------------------------------------- /tests/try-syscall.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Simon McVittie 3 | * SPDX-License-Identifier: LGPL-2.0-or-later 4 | * 5 | * Try one or more system calls that might have been blocked by a 6 | * seccomp filter. Return the last value of errno seen. 7 | * 8 | * In general, we pass a bad fd or pointer to each syscall that will 9 | * accept one, so that it will fail with EBADF or EFAULT without side-effects. 10 | * 11 | * This helper is used for regression tests in both bubblewrap and flatpak. 12 | * Please keep both copies in sync. 13 | */ 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #if defined(_MIPS_SIM) 27 | # if _MIPS_SIM == _ABIO32 28 | # define MISSING_SYSCALL_BASE 4000 29 | # elif _MIPS_SIM == _ABI64 30 | # define MISSING_SYSCALL_BASE 5000 31 | # elif _MIPS_SIM == _ABIN32 32 | # define MISSING_SYSCALL_BASE 6000 33 | # else 34 | # error "Unknown MIPS ABI" 35 | # endif 36 | #endif 37 | 38 | #if defined(__ia64__) 39 | # define MISSING_SYSCALL_BASE 1024 40 | #endif 41 | 42 | #if defined(__alpha__) 43 | # define MISSING_SYSCALL_BASE 110 44 | #endif 45 | 46 | #if defined(__x86_64__) && defined(__ILP32__) 47 | # define MISSING_SYSCALL_BASE 0x40000000 48 | #endif 49 | 50 | /* 51 | * MISSING_SYSCALL_BASE: 52 | * 53 | * Number to add to the syscall numbers of recently-added syscalls 54 | * to get the appropriate syscall for the current ABI. 55 | */ 56 | #ifndef MISSING_SYSCALL_BASE 57 | # define MISSING_SYSCALL_BASE 0 58 | #endif 59 | 60 | #ifndef __NR_clone3 61 | # define __NR_clone3 (MISSING_SYSCALL_BASE + 435) 62 | #endif 63 | 64 | /* 65 | * The size of clone3's parameter (as of 2021) 66 | */ 67 | #define SIZEOF_STRUCT_CLONE_ARGS ((size_t) 88) 68 | 69 | /* 70 | * An invalid pointer that will cause syscalls to fail with EFAULT 71 | */ 72 | #define WRONG_POINTER ((char *) 1) 73 | 74 | #ifndef PR_GET_CHILD_SUBREAPER 75 | #define PR_GET_CHILD_SUBREAPER 37 76 | #endif 77 | 78 | int 79 | main (int argc, char **argv) 80 | { 81 | int errsv = 0; 82 | int i; 83 | 84 | for (i = 1; i < argc; i++) 85 | { 86 | const char *arg = argv[i]; 87 | 88 | if (strcmp (arg, "print-errno-values") == 0) 89 | { 90 | printf ("EBADF=%d\n", EBADF); 91 | printf ("EFAULT=%d\n", EFAULT); 92 | printf ("ENOENT=%d\n", ENOENT); 93 | printf ("ENOSYS=%d\n", ENOSYS); 94 | printf ("EPERM=%d\n", EPERM); 95 | } 96 | else if (strcmp (arg, "chmod") == 0) 97 | { 98 | /* If not blocked by seccomp, this will fail with EFAULT */ 99 | if (chmod (WRONG_POINTER, 0700) != 0) 100 | { 101 | errsv = errno; 102 | perror (arg); 103 | } 104 | } 105 | else if (strcmp (arg, "chroot") == 0) 106 | { 107 | /* If not blocked by seccomp, this will fail with EFAULT */ 108 | if (chroot (WRONG_POINTER) != 0) 109 | { 110 | errsv = errno; 111 | perror (arg); 112 | } 113 | } 114 | else if (strcmp (arg, "clone3") == 0) 115 | { 116 | /* If not blocked by seccomp, this will fail with EFAULT */ 117 | if (syscall (__NR_clone3, WRONG_POINTER, SIZEOF_STRUCT_CLONE_ARGS) != 0) 118 | { 119 | errsv = errno; 120 | perror (arg); 121 | } 122 | } 123 | else if (strcmp (arg, "ioctl TIOCNOTTY") == 0) 124 | { 125 | /* If not blocked by seccomp, this will fail with EBADF */ 126 | if (ioctl (-1, TIOCNOTTY) != 0) 127 | { 128 | errsv = errno; 129 | perror (arg); 130 | } 131 | } 132 | else if (strcmp (arg, "ioctl TIOCSTI") == 0) 133 | { 134 | /* If not blocked by seccomp, this will fail with EBADF */ 135 | if (ioctl (-1, TIOCSTI, WRONG_POINTER) != 0) 136 | { 137 | errsv = errno; 138 | perror (arg); 139 | } 140 | } 141 | #ifdef __LP64__ 142 | else if (strcmp (arg, "ioctl TIOCSTI CVE-2019-10063") == 0) 143 | { 144 | unsigned long not_TIOCSTI = (0x123UL << 32) | (unsigned long) TIOCSTI; 145 | 146 | /* If not blocked by seccomp, this will fail with EBADF */ 147 | if (syscall (__NR_ioctl, -1, not_TIOCSTI, WRONG_POINTER) != 0) 148 | { 149 | errsv = errno; 150 | perror (arg); 151 | } 152 | } 153 | #endif 154 | else if (strcmp (arg, "listen") == 0) 155 | { 156 | /* If not blocked by seccomp, this will fail with EBADF */ 157 | if (listen (-1, 42) != 0) 158 | { 159 | errsv = errno; 160 | perror (arg); 161 | } 162 | } 163 | else if (strcmp (arg, "prctl") == 0) 164 | { 165 | /* If not blocked by seccomp, this will fail with EFAULT */ 166 | if (prctl (PR_GET_CHILD_SUBREAPER, WRONG_POINTER, 0, 0, 0) != 0) 167 | { 168 | errsv = errno; 169 | perror (arg); 170 | } 171 | } 172 | else 173 | { 174 | fprintf (stderr, "Unsupported syscall \"%s\"\n", arg); 175 | errsv = ENOENT; 176 | } 177 | } 178 | 179 | return errsv; 180 | } 181 | -------------------------------------------------------------------------------- /tests/libtest-core.sh: -------------------------------------------------------------------------------- 1 | # Core source library for shell script tests; the 2 | # canonical version lives in: 3 | # 4 | # https://github.com/ostreedev/ostree 5 | # 6 | # Known copies are in the following repos: 7 | # 8 | # - https://github.com/containers/bubblewrap 9 | # - https://github.com/coreos/rpm-ostree 10 | # 11 | # Copyright (C) 2017 Colin Walters 12 | # 13 | # SPDX-License-Identifier: LGPL-2.0-or-later 14 | # 15 | # This library is free software; you can redistribute it and/or 16 | # modify it under the terms of the GNU Lesser General Public 17 | # License as published by the Free Software Foundation; either 18 | # version 2 of the License, or (at your option) any later version. 19 | # 20 | # This library is distributed in the hope that it will be useful, 21 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 23 | # Lesser General Public License for more details. 24 | # 25 | # You should have received a copy of the GNU Lesser General Public 26 | # License along with this library; if not, write to the 27 | # Free Software Foundation, Inc., 59 Temple Place - Suite 330, 28 | # Boston, MA 02111-1307, USA. 29 | 30 | fatal() { 31 | echo $@ 1>&2; exit 1 32 | } 33 | # fatal() is shorter to type, but retain this alias 34 | assert_not_reached () { 35 | fatal "$@" 36 | } 37 | 38 | # Some tests look for specific English strings. Use a UTF-8 version 39 | # of the C (POSIX) locale if we have one, or fall back to en_US.UTF-8 40 | # (https://sourceware.org/glibc/wiki/Proposals/C.UTF-8) 41 | # 42 | # If we can't find the locale command assume we have support for C.UTF-8 43 | # (e.g. musl based systems) 44 | if type -p locale >/dev/null; then 45 | export LC_ALL=$(locale -a | grep -iEe '^(C|en_US)\.(UTF-8|utf8)$' | head -n1 || true) 46 | if [ -z "${LC_ALL}" ]; then fatal "Can't find suitable UTF-8 locale"; fi 47 | else 48 | export LC_ALL=C.UTF-8 49 | fi 50 | # A GNU extension, used whenever LC_ALL is not C 51 | unset LANGUAGE 52 | 53 | # This should really be the default IMO 54 | export G_DEBUG=fatal-warnings 55 | 56 | assert_streq () { 57 | test "$1" = "$2" || fatal "$1 != $2" 58 | } 59 | 60 | assert_str_match () { 61 | if ! echo "$1" | grep -E -q "$2"; then 62 | fatal "$1 does not match regexp $2" 63 | fi 64 | } 65 | 66 | assert_not_streq () { 67 | (! test "$1" = "$2") || fatal "$1 == $2" 68 | } 69 | 70 | assert_has_file () { 71 | test -f "$1" || fatal "Couldn't find '$1'" 72 | } 73 | 74 | assert_has_dir () { 75 | test -d "$1" || fatal "Couldn't find '$1'" 76 | } 77 | 78 | # Dump ls -al + file contents to stderr, then fatal() 79 | _fatal_print_file() { 80 | file="$1" 81 | shift 82 | ls -al "$file" >&2 83 | sed -e 's/^/# /' < "$file" >&2 84 | fatal "$@" 85 | } 86 | 87 | _fatal_print_files() { 88 | file1="$1" 89 | shift 90 | file2="$1" 91 | shift 92 | ls -al "$file1" >&2 93 | sed -e 's/^/# /' < "$file1" >&2 94 | ls -al "$file2" >&2 95 | sed -e 's/^/# /' < "$file2" >&2 96 | fatal "$@" 97 | } 98 | 99 | assert_not_has_file () { 100 | if test -f "$1"; then 101 | _fatal_print_file "$1" "File '$1' exists" 102 | fi 103 | } 104 | 105 | assert_not_file_has_content () { 106 | fpath=$1 107 | shift 108 | for re in "$@"; do 109 | if grep -q -e "$re" "$fpath"; then 110 | _fatal_print_file "$fpath" "File '$fpath' matches regexp '$re'" 111 | fi 112 | done 113 | } 114 | 115 | assert_not_has_dir () { 116 | if test -d "$1"; then 117 | fatal "Directory '$1' exists" 118 | fi 119 | } 120 | 121 | assert_file_has_content () { 122 | fpath=$1 123 | shift 124 | for re in "$@"; do 125 | if ! grep -q -e "$re" "$fpath"; then 126 | _fatal_print_file "$fpath" "File '$fpath' doesn't match regexp '$re'" 127 | fi 128 | done 129 | } 130 | 131 | assert_file_has_content_once () { 132 | fpath=$1 133 | shift 134 | for re in "$@"; do 135 | if ! test $(grep -e "$re" "$fpath" | wc -l) = "1"; then 136 | _fatal_print_file "$fpath" "File '$fpath' doesn't match regexp '$re' exactly once" 137 | fi 138 | done 139 | } 140 | 141 | assert_file_has_content_literal () { 142 | fpath=$1; shift 143 | for s in "$@"; do 144 | if ! grep -q -F -e "$s" "$fpath"; then 145 | _fatal_print_file "$fpath" "File '$fpath' doesn't match fixed string list '$s'" 146 | fi 147 | done 148 | } 149 | 150 | assert_file_has_mode () { 151 | mode=$(stat -c '%a' $1) 152 | if [ "$mode" != "$2" ]; then 153 | fatal "File '$1' has wrong mode: expected $2, but got $mode" 154 | fi 155 | } 156 | 157 | assert_symlink_has_content () { 158 | if ! test -L "$1"; then 159 | fatal "File '$1' is not a symbolic link" 160 | fi 161 | if ! readlink "$1" | grep -q -e "$2"; then 162 | _fatal_print_file "$1" "Symbolic link '$1' doesn't match regexp '$2'" 163 | fi 164 | } 165 | 166 | assert_file_empty() { 167 | if test -s "$1"; then 168 | _fatal_print_file "$1" "File '$1' is not empty" 169 | fi 170 | } 171 | 172 | assert_files_equal() { 173 | if ! cmp "$1" "$2"; then 174 | _fatal_print_files "$1" "$2" "File '$1' and '$2' is not equal" 175 | fi 176 | } 177 | 178 | # Use to skip all of these tests 179 | skip() { 180 | echo "1..0 # SKIP" "$@" 181 | exit 0 182 | } 183 | 184 | report_err () { 185 | local exit_status="$?" 186 | { { local BASH_XTRACEFD=3; } 2> /dev/null 187 | echo "Unexpected nonzero exit status $exit_status while running: $BASH_COMMAND" >&2 188 | } 3> /dev/null 189 | } 190 | trap report_err ERR 191 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: CI checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | check: 13 | name: Build with Autotools and gcc, and test 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out 17 | uses: actions/checkout@v1 18 | - name: Install build-dependencies 19 | run: sudo ./ci/builddeps.sh 20 | - name: Create logs dir 21 | run: mkdir test-logs 22 | - name: autogen.sh 23 | run: NOCONFIGURE=1 ./autogen.sh 24 | - name: configure 25 | run: | 26 | mkdir _build 27 | pushd _build 28 | ../configure \ 29 | --enable-man \ 30 | --enable-selinux \ 31 | ${NULL+} 32 | popd 33 | env: 34 | CFLAGS: >- 35 | -O2 36 | -Wp,-D_FORTIFY_SOURCE=2 37 | -fsanitize=address 38 | -fsanitize=undefined 39 | - name: make 40 | run: make -C _build -j $(getconf _NPROCESSORS_ONLN) V=1 41 | - name: smoke-test 42 | run: | 43 | set -x 44 | ./_build/bwrap --bind / / --tmpfs /tmp true 45 | env: 46 | ASAN_OPTIONS: detect_leaks=0 47 | - name: check 48 | run: | 49 | make -C _build -j $(getconf _NPROCESSORS_ONLN) check VERBOSE=1 BWRAP_MUST_WORK=1 50 | env: 51 | ASAN_OPTIONS: detect_leaks=0 52 | - name: Collect overall test logs on failure 53 | if: failure() 54 | run: mv _build/test-suite.log test-logs/ || true 55 | - name: Collect individual test logs on cancel 56 | if: failure() || cancelled() 57 | run: mv _build/tests/*.log test-logs/ || true 58 | - name: Upload test logs 59 | uses: actions/upload-artifact@v1 60 | if: failure() || cancelled() 61 | with: 62 | name: test logs 63 | path: test-logs 64 | - name: install 65 | run: | 66 | make -C _build install DESTDIR="$(pwd)/DESTDIR" 67 | ( cd DESTDIR && find -ls ) 68 | - name: distcheck 69 | run: | 70 | make -C _build -j $(getconf _NPROCESSORS_ONLN) distcheck VERBOSE=1 BWRAP_MUST_WORK=1 71 | 72 | meson: 73 | name: Build with Meson and gcc, and test 74 | runs-on: ubuntu-latest 75 | steps: 76 | - name: Check out 77 | uses: actions/checkout@v1 78 | - name: Install build-dependencies 79 | run: sudo ./ci/builddeps.sh 80 | - name: Create logs dir 81 | run: mkdir test-logs 82 | - name: setup 83 | run: | 84 | meson _build 85 | env: 86 | CFLAGS: >- 87 | -O2 88 | -Wp,-D_FORTIFY_SOURCE=2 89 | -fsanitize=address 90 | -fsanitize=undefined 91 | - name: compile 92 | run: ninja -C _build -v 93 | - name: smoke-test 94 | run: | 95 | set -x 96 | ./_build/bwrap --bind / / --tmpfs /tmp true 97 | env: 98 | ASAN_OPTIONS: detect_leaks=0 99 | - name: test 100 | run: | 101 | BWRAP_MUST_WORK=1 meson test -C _build 102 | env: 103 | ASAN_OPTIONS: detect_leaks=0 104 | - name: Collect overall test logs on failure 105 | if: failure() 106 | run: mv _build/meson-logs/testlog.txt test-logs/ || true 107 | - name: install 108 | run: | 109 | DESTDIR="$(pwd)/DESTDIR" meson install -C _build 110 | ( cd DESTDIR && find -ls ) 111 | - name: dist 112 | run: | 113 | BWRAP_MUST_WORK=1 meson dist -C _build 114 | - name: Collect dist test logs on failure 115 | if: failure() 116 | run: mv _build/meson-private/dist-build/meson-logs/testlog.txt test-logs/disttestlog.txt || true 117 | - name: use as subproject 118 | run: | 119 | mkdir tests/use-as-subproject/subprojects 120 | tar -C tests/use-as-subproject/subprojects -xf _build/meson-dist/bubblewrap-*.tar.xz 121 | mv tests/use-as-subproject/subprojects/bubblewrap-* tests/use-as-subproject/subprojects/bubblewrap 122 | ( cd tests/use-as-subproject && meson _build ) 123 | ninja -C tests/use-as-subproject/_build -v 124 | meson test -C tests/use-as-subproject/_build 125 | DESTDIR="$(pwd)/DESTDIR-as-subproject" meson install -C tests/use-as-subproject/_build 126 | ( cd DESTDIR-as-subproject && find -ls ) 127 | test -x DESTDIR-as-subproject/usr/local/libexec/not-flatpak-bwrap 128 | test ! -e DESTDIR-as-subproject/usr/local/bin/bwrap 129 | test ! -e DESTDIR-as-subproject/usr/local/libexec/bwrap 130 | tests/use-as-subproject/assert-correct-rpath.py DESTDIR-as-subproject/usr/local/libexec/not-flatpak-bwrap 131 | - name: Upload test logs 132 | uses: actions/upload-artifact@v1 133 | if: failure() || cancelled() 134 | with: 135 | name: test logs 136 | path: test-logs 137 | 138 | clang: 139 | name: Build with clang and analyze 140 | runs-on: ubuntu-latest 141 | strategy: 142 | fail-fast: false 143 | matrix: 144 | language: 145 | - cpp 146 | steps: 147 | - name: Initialize CodeQL 148 | uses: github/codeql-action/init@v2 149 | with: 150 | languages: ${{ matrix.language }} 151 | - name: Check out 152 | uses: actions/checkout@v1 153 | - name: Install build-dependencies 154 | run: sudo ./ci/builddeps.sh --clang 155 | - name: autogen.sh 156 | run: NOCONFIGURE=1 ./autogen.sh 157 | - name: configure 158 | run: ./configure --enable-selinux 159 | env: 160 | CC: clang 161 | CFLAGS: >- 162 | -O2 163 | -Werror=unused-variable 164 | - name: make 165 | run: make -j $(getconf _NPROCESSORS_ONLN) V=1 166 | - name: CodeQL analysis 167 | uses: github/codeql-action/analyze@v2 168 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | AC_PREREQ([2.63]) 2 | AC_INIT([bubblewrap], [0.10.0], [atomic-devel@projectatomic.io]) 3 | AC_CONFIG_HEADER([config.h]) 4 | AC_CONFIG_MACRO_DIR([m4]) 5 | AC_CONFIG_AUX_DIR([build-aux]) 6 | 7 | AC_USE_SYSTEM_EXTENSIONS 8 | 9 | AM_INIT_AUTOMAKE([1.11 -Wno-portability foreign no-define tar-ustar no-dist-gzip dist-xz]) 10 | AM_MAINTAINER_MODE([enable]) 11 | AM_SILENT_RULES([yes]) 12 | 13 | AC_SYS_LARGEFILE 14 | 15 | AC_PROG_CC 16 | AM_PROG_CC_C_O 17 | PKG_PROG_PKG_CONFIG([]) 18 | 19 | AC_CHECK_HEADERS([sys/capability.h], [], [AC_MSG_ERROR([*** POSIX caps headers not found])]) 20 | 21 | AC_ARG_ENABLE(man, 22 | [AS_HELP_STRING([--enable-man], 23 | [generate man pages [default=auto]])],, 24 | enable_man=maybe) 25 | 26 | AS_IF([test "$enable_man" != no], [ 27 | AC_PATH_PROG([XSLTPROC], [xsltproc], []) 28 | AS_IF([test -z "$XSLTPROC"], [ 29 | AS_IF([test "$enable_man" = yes], [ 30 | AC_MSG_ERROR([xsltproc is required for --enable-man]) 31 | ]) 32 | enable_man=no 33 | ], [ 34 | enable_man=yes 35 | ]) 36 | ]) 37 | AM_CONDITIONAL(ENABLE_MAN, test "$enable_man" != no) 38 | 39 | AC_ARG_WITH([bash-completion-dir], 40 | AS_HELP_STRING([--with-bash-completion-dir[=PATH]], 41 | [Install the bash auto-completion script in this directory. @<:@default=yes@:>@]), 42 | [], 43 | [with_bash_completion_dir=yes]) 44 | 45 | AS_IF([test "x$with_bash_completion_dir" = "xyes"], 46 | [ 47 | PKG_CHECK_MODULES([BASH_COMPLETION], [bash-completion >= 2.0], 48 | [BASH_COMPLETION_DIR="`pkg-config --variable=completionsdir bash-completion`"], 49 | [BASH_COMPLETION_DIR="$datadir/bash-completion/completions"]) 50 | ], 51 | [ 52 | BASH_COMPLETION_DIR="$with_bash_completion_dir" 53 | ]) 54 | 55 | AC_SUBST([BASH_COMPLETION_DIR]) 56 | AM_CONDITIONAL([ENABLE_BASH_COMPLETION],[test "x$with_bash_completion_dir" != "xno"]) 57 | 58 | AC_ARG_WITH([zsh-completion-dir], 59 | AS_HELP_STRING([--with-zsh-completion-dir[=PATH]], 60 | [Install the zsh auto-completion script in this directory. @<:@default=yes@:>@]), 61 | [], 62 | [with_zsh_completion_dir=yes]) 63 | 64 | AS_IF([test "x$with_zsh_completion_dir" = "xyes"], 65 | [ZSH_COMPLETION_DIR="$datadir/zsh/site-functions"], 66 | [ZSH_COMPLETION_DIR="$with_zsh_completion_dir"]) 67 | 68 | 69 | AC_SUBST([ZSH_COMPLETION_DIR]) 70 | AM_CONDITIONAL([ENABLE_ZSH_COMPLETION], [test "x$with_zsh_completion_dir" != "xno"]) 71 | 72 | # ------------------------------------------------------------------------------ 73 | have_selinux=no 74 | AC_ARG_ENABLE(selinux, AS_HELP_STRING([--disable-selinux], [Disable optional SELINUX support])) 75 | AS_IF([test "x$enable_selinux" != "xno"], [ 76 | PKG_CHECK_MODULES([SELINUX], [libselinux >= 2.1.9], 77 | [AC_DEFINE(HAVE_SELINUX, 1, [Define if SELinux is available]) 78 | have_selinux=yes 79 | M4_DEFINES="$M4_DEFINES -DHAVE_SELINUX"], 80 | [have_selinux=no]) 81 | AS_IF([test "x$have_selinux" = xno && test "x$enable_selinux" = xyes], 82 | [AC_MSG_ERROR([*** SELinux support requested but libraries not found])]) 83 | PKG_CHECK_MODULES([SELINUX_2_3], [libselinux >= 2.3], 84 | [AC_DEFINE(HAVE_SELINUX_2_3, 1, [Define if SELinux is version >= 2.3])], 85 | [:]) 86 | ]) 87 | AM_CONDITIONAL(HAVE_SELINUX, [test "$have_selinux" = "yes"]) 88 | 89 | dnl Keep this in sync with ostree, except remove -Werror=declaration-after-statement 90 | CC_CHECK_FLAGS_APPEND([WARN_CFLAGS], [CFLAGS], [\ 91 | -pipe \ 92 | -Wall \ 93 | -Werror=shadow \ 94 | -Werror=empty-body \ 95 | -Werror=strict-prototypes \ 96 | -Werror=missing-prototypes \ 97 | -Werror=implicit-function-declaration \ 98 | "-Werror=format=2 -Werror=format-security -Werror=format-nonliteral" \ 99 | -Werror=pointer-arith -Werror=init-self \ 100 | -Werror=missing-declarations \ 101 | -Werror=return-type \ 102 | -Werror=overflow \ 103 | -Werror=int-conversion \ 104 | -Werror=parenthesis \ 105 | -Werror=incompatible-pointer-types \ 106 | -Werror=misleading-indentation \ 107 | -Werror=missing-include-dirs -Werror=aggregate-return \ 108 | -Werror=switch-default \ 109 | -Wswitch-enum \ 110 | ]) 111 | AC_SUBST(WARN_CFLAGS) 112 | 113 | AC_CHECK_LIB(cap, cap_from_text) 114 | 115 | AS_IF([test "$ac_cv_lib_cap_cap_from_text" != "yes"], 116 | [AC_MSG_ERROR([*** libcap requested but not found])]) 117 | 118 | AC_ARG_WITH(priv-mode, 119 | AS_HELP_STRING([--with-priv-mode=setuid/none], 120 | [How to set privilege-raising during make install]), 121 | [], 122 | [with_priv_mode="none"]) 123 | 124 | AM_CONDITIONAL(PRIV_MODE_SETUID, test "x$with_priv_mode" = "xsetuid") 125 | 126 | AC_ARG_ENABLE(sudo, 127 | AS_HELP_STRING([--enable-sudo],[Use sudo to set privileged mode on binaries during install (only needed if --with-priv-mode used)]), 128 | [SUDO_BIN="sudo"], [SUDO_BIN=""]) 129 | AC_SUBST([SUDO_BIN]) 130 | 131 | AC_ARG_ENABLE(require-userns, 132 | AS_HELP_STRING([--enable-require-userns=yes/no (default no)], 133 | [Require user namespaces by default when installed suid]), 134 | [], 135 | [enable_require_userns="no"]) 136 | 137 | AS_IF([ test "x$enable_require_userns" = "xyes" ], [ 138 | AC_DEFINE(ENABLE_REQUIRE_USERNS, 1, [Define if userns should be used by default in suid mode]) 139 | ]) 140 | 141 | AC_PROG_AWK 142 | AC_REQUIRE_AUX_FILE([tap-driver.sh]) 143 | 144 | AC_CONFIG_FILES([ 145 | Makefile 146 | ]) 147 | AC_OUTPUT 148 | 149 | echo " 150 | bubblewrap $VERSION 151 | =================== 152 | 153 | man pages (xsltproc): $enable_man 154 | SELinux: $have_selinux 155 | setuid mode on make install: $with_priv_mode 156 | require default userns: $enable_require_userns 157 | mysteriously satisfying to pop: yes" 158 | echo "" 159 | -------------------------------------------------------------------------------- /network.c: -------------------------------------------------------------------------------- 1 | /* bubblewrap 2 | * Copyright (C) 2016 Alexander Larsson 3 | * SPDX-License-Identifier: LGPL-2.0-or-later 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 2 of the License, or (at your option) any later version. 9 | * 10 | * This library is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this library. If not, see . 17 | * 18 | */ 19 | 20 | #include "config.h" 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | #include "utils.h" 30 | #include "network.h" 31 | 32 | static void * 33 | add_rta (struct nlmsghdr *header, 34 | int type, 35 | size_t size) 36 | { 37 | struct rtattr *rta; 38 | size_t rta_size = RTA_LENGTH (size); 39 | 40 | rta = (struct rtattr *) ((char *) header + NLMSG_ALIGN (header->nlmsg_len)); 41 | rta->rta_type = type; 42 | rta->rta_len = rta_size; 43 | 44 | header->nlmsg_len = NLMSG_ALIGN (header->nlmsg_len) + rta_size; 45 | 46 | return RTA_DATA (rta); 47 | } 48 | 49 | static int 50 | rtnl_send_request (int rtnl_fd, 51 | struct nlmsghdr *header) 52 | { 53 | struct sockaddr_nl dst_addr = { AF_NETLINK, 0 }; 54 | ssize_t sent; 55 | 56 | sent = sendto (rtnl_fd, (void *) header, header->nlmsg_len, 0, 57 | (struct sockaddr *) &dst_addr, sizeof (dst_addr)); 58 | if (sent < 0) 59 | return -1; 60 | 61 | return 0; 62 | } 63 | 64 | static int 65 | rtnl_read_reply (int rtnl_fd, 66 | unsigned int seq_nr) 67 | { 68 | char buffer[1024]; 69 | ssize_t received; 70 | struct nlmsghdr *rheader; 71 | 72 | while (1) 73 | { 74 | received = recv (rtnl_fd, buffer, sizeof (buffer), 0); 75 | if (received < 0) 76 | return -1; 77 | 78 | rheader = (struct nlmsghdr *) buffer; 79 | while (received >= NLMSG_HDRLEN) 80 | { 81 | if (rheader->nlmsg_seq != seq_nr) 82 | return -1; 83 | if ((pid_t)rheader->nlmsg_pid != getpid ()) 84 | return -1; 85 | if (rheader->nlmsg_type == NLMSG_ERROR) 86 | { 87 | uint32_t *err = NLMSG_DATA (rheader); 88 | if (*err == 0) 89 | return 0; 90 | 91 | return -1; 92 | } 93 | if (rheader->nlmsg_type == NLMSG_DONE) 94 | return 0; 95 | 96 | rheader = NLMSG_NEXT (rheader, received); 97 | } 98 | } 99 | } 100 | 101 | static int 102 | rtnl_do_request (int rtnl_fd, 103 | struct nlmsghdr *header) 104 | { 105 | if (rtnl_send_request (rtnl_fd, header) != 0) 106 | return -1; 107 | 108 | if (rtnl_read_reply (rtnl_fd, header->nlmsg_seq) != 0) 109 | return -1; 110 | 111 | return 0; 112 | } 113 | 114 | static struct nlmsghdr * 115 | rtnl_setup_request (char *buffer, 116 | int type, 117 | int flags, 118 | size_t size) 119 | { 120 | struct nlmsghdr *header; 121 | size_t len = NLMSG_LENGTH (size); 122 | static uint32_t counter = 0; 123 | 124 | memset (buffer, 0, len); 125 | 126 | header = (struct nlmsghdr *) buffer; 127 | header->nlmsg_len = len; 128 | header->nlmsg_type = type; 129 | header->nlmsg_flags = flags | NLM_F_REQUEST; 130 | header->nlmsg_seq = counter++; 131 | header->nlmsg_pid = getpid (); 132 | 133 | return header; 134 | } 135 | 136 | void 137 | loopback_setup (void) 138 | { 139 | int r, if_loopback; 140 | cleanup_fd int rtnl_fd = -1; 141 | char buffer[1024]; 142 | struct sockaddr_nl src_addr = { AF_NETLINK, 0 }; 143 | struct nlmsghdr *header; 144 | struct ifaddrmsg *addmsg; 145 | struct ifinfomsg *infomsg; 146 | struct in_addr *ip_addr; 147 | 148 | src_addr.nl_pid = getpid (); 149 | 150 | if_loopback = (int) if_nametoindex ("lo"); 151 | if (if_loopback <= 0) 152 | die_with_error ("loopback: Failed to look up lo"); 153 | 154 | rtnl_fd = socket (PF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); 155 | if (rtnl_fd < 0) 156 | die_with_error ("loopback: Failed to create NETLINK_ROUTE socket"); 157 | 158 | r = bind (rtnl_fd, (struct sockaddr *) &src_addr, sizeof (src_addr)); 159 | if (r < 0) 160 | die_with_error ("loopback: Failed to bind NETLINK_ROUTE socket"); 161 | 162 | header = rtnl_setup_request (buffer, RTM_NEWADDR, 163 | NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK, 164 | sizeof (struct ifaddrmsg)); 165 | addmsg = NLMSG_DATA (header); 166 | 167 | addmsg->ifa_family = AF_INET; 168 | addmsg->ifa_prefixlen = 8; 169 | addmsg->ifa_flags = IFA_F_PERMANENT; 170 | addmsg->ifa_scope = RT_SCOPE_HOST; 171 | addmsg->ifa_index = if_loopback; 172 | 173 | ip_addr = add_rta (header, IFA_LOCAL, sizeof (*ip_addr)); 174 | ip_addr->s_addr = htonl (INADDR_LOOPBACK); 175 | 176 | ip_addr = add_rta (header, IFA_ADDRESS, sizeof (*ip_addr)); 177 | ip_addr->s_addr = htonl (INADDR_LOOPBACK); 178 | 179 | assert (header->nlmsg_len < sizeof (buffer)); 180 | 181 | if (rtnl_do_request (rtnl_fd, header) != 0) 182 | die_with_error ("loopback: Failed RTM_NEWADDR"); 183 | 184 | header = rtnl_setup_request (buffer, RTM_NEWLINK, 185 | NLM_F_ACK, 186 | sizeof (struct ifinfomsg)); 187 | infomsg = NLMSG_DATA (header); 188 | 189 | infomsg->ifi_family = AF_UNSPEC; 190 | infomsg->ifi_type = 0; 191 | infomsg->ifi_index = if_loopback; 192 | infomsg->ifi_flags = IFF_UP; 193 | infomsg->ifi_change = IFF_UP; 194 | 195 | assert (header->nlmsg_len < sizeof (buffer)); 196 | 197 | if (rtnl_do_request (rtnl_fd, header) != 0) 198 | die_with_error ("loopback: Failed RTM_NEWLINK"); 199 | } 200 | -------------------------------------------------------------------------------- /utils.h: -------------------------------------------------------------------------------- 1 | /* bubblewrap 2 | * Copyright (C) 2016 Alexander Larsson 3 | * SPDX-License-Identifier: LGPL-2.0-or-later 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 2 of the License, or (at your option) any later version. 9 | * 10 | * This library is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this library. If not, see . 17 | * 18 | */ 19 | 20 | #pragma once 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | #if 0 35 | #define __debug__(x) printf x 36 | #else 37 | #define __debug__(x) 38 | #endif 39 | 40 | #define UNUSED __attribute__((__unused__)) 41 | 42 | #define N_ELEMENTS(arr) (sizeof (arr) / sizeof ((arr)[0])) 43 | 44 | #define TRUE 1 45 | #define FALSE 0 46 | typedef int bool; 47 | 48 | #define PIPE_READ_END 0 49 | #define PIPE_WRITE_END 1 50 | 51 | #ifndef PR_SET_CHILD_SUBREAPER 52 | #define PR_SET_CHILD_SUBREAPER 36 53 | #endif 54 | 55 | void warn (const char *format, 56 | ...) __attribute__((format (printf, 1, 2))); 57 | void die_with_error (const char *format, 58 | ...) __attribute__((__noreturn__)) __attribute__((format (printf, 1, 2))); 59 | void die_with_mount_error (const char *format, 60 | ...) __attribute__((__noreturn__)) __attribute__((format (printf, 1, 2))); 61 | void die (const char *format, 62 | ...) __attribute__((__noreturn__)) __attribute__((format (printf, 1, 2))); 63 | void die_oom (void) __attribute__((__noreturn__)); 64 | void die_unless_label_valid (const char *label); 65 | 66 | void fork_intermediate_child (void); 67 | 68 | void *xmalloc (size_t size); 69 | void *xcalloc (size_t nmemb, size_t size); 70 | void *xrealloc (void *ptr, 71 | size_t size); 72 | char *xstrdup (const char *str); 73 | void strfreev (char **str_array); 74 | void xclearenv (void); 75 | void xsetenv (const char *name, 76 | const char *value, 77 | int overwrite); 78 | void xunsetenv (const char *name); 79 | char *strconcat (const char *s1, 80 | const char *s2); 81 | char *strconcat3 (const char *s1, 82 | const char *s2, 83 | const char *s3); 84 | char * xasprintf (const char *format, 85 | ...) __attribute__((format (printf, 1, 2))); 86 | bool has_prefix (const char *str, 87 | const char *prefix); 88 | bool has_path_prefix (const char *str, 89 | const char *prefix); 90 | bool path_equal (const char *path1, 91 | const char *path2); 92 | int fdwalk (int proc_fd, 93 | int (*cb)(void *data, 94 | int fd), 95 | void *data); 96 | char *load_file_data (int fd, 97 | size_t *size); 98 | char *load_file_at (int dirfd, 99 | const char *path); 100 | int write_file_at (int dirfd, 101 | const char *path, 102 | const char *content); 103 | int write_to_fd (int fd, 104 | const char *content, 105 | ssize_t len); 106 | int copy_file_data (int sfd, 107 | int dfd); 108 | int copy_file (const char *src_path, 109 | const char *dst_path, 110 | mode_t mode); 111 | int create_file (const char *path, 112 | mode_t mode, 113 | const char *content); 114 | int ensure_file (const char *path, 115 | mode_t mode); 116 | int ensure_dir (const char *path, 117 | mode_t mode); 118 | int get_file_mode (const char *pathname); 119 | int mkdir_with_parents (const char *pathname, 120 | mode_t mode, 121 | bool create_last); 122 | void create_pid_socketpair (int sockets[2]); 123 | void send_pid_on_socket (int socket); 124 | int read_pid_from_socket (int socket); 125 | char *get_oldroot_path (const char *path); 126 | char *get_newroot_path (const char *path); 127 | char *readlink_malloc (const char *pathname); 128 | 129 | /* syscall wrappers */ 130 | int raw_clone (unsigned long flags, 131 | void *child_stack); 132 | int pivot_root (const char *new_root, 133 | const char *put_old); 134 | char *label_mount (const char *opt, 135 | const char *mount_label); 136 | int label_exec (const char *exec_label); 137 | int label_create_file (const char *file_label); 138 | 139 | const char *mount_strerror (int errsv); 140 | 141 | static inline void 142 | cleanup_freep (void *p) 143 | { 144 | void **pp = (void **) p; 145 | 146 | if (*pp) 147 | free (*pp); 148 | } 149 | 150 | static inline void 151 | cleanup_strvp (void *p) 152 | { 153 | void **pp = (void **) p; 154 | 155 | strfreev (*pp); 156 | } 157 | 158 | static inline void 159 | cleanup_fdp (int *fdp) 160 | { 161 | int fd; 162 | 163 | assert (fdp); 164 | 165 | fd = *fdp; 166 | if (fd != -1) 167 | (void) close (fd); 168 | } 169 | 170 | #define cleanup_free __attribute__((cleanup (cleanup_freep))) 171 | #define cleanup_fd __attribute__((cleanup (cleanup_fdp))) 172 | #define cleanup_strv __attribute__((cleanup (cleanup_strvp))) 173 | 174 | static inline void * 175 | steal_pointer (void *pp) 176 | { 177 | void **ptr = (void **) pp; 178 | void *ref; 179 | 180 | ref = *ptr; 181 | *ptr = NULL; 182 | 183 | return ref; 184 | } 185 | 186 | /* type safety */ 187 | #define steal_pointer(pp) \ 188 | (0 ? (*(pp)) : (steal_pointer) (pp)) 189 | -------------------------------------------------------------------------------- /tests/test-utils.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019-2021 Collabora Ltd. 3 | * 4 | * SPDX-License-Identifier: LGPL-2.0-or-later 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 2 of the License, or (at your option) any later version. 10 | * 11 | * This library is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this library. If not, see . 18 | */ 19 | 20 | #include 21 | #include 22 | #include 23 | 24 | #include "utils.h" 25 | 26 | /* A small implementation of TAP */ 27 | static unsigned int test_number = 0; 28 | 29 | __attribute__((format(printf, 1, 2))) 30 | static void 31 | ok (const char *format, ...) 32 | { 33 | va_list ap; 34 | 35 | printf ("ok %u - ", ++test_number); 36 | va_start (ap, format); 37 | vprintf (format, ap); 38 | va_end (ap); 39 | printf ("\n"); 40 | } 41 | 42 | /* for simplicity we always die immediately on failure */ 43 | #define not_ok(fmt, ...) die (fmt, ## __VA_ARGS__) 44 | 45 | /* approximately GLib-compatible helper macros */ 46 | #define g_test_message(fmt, ...) printf ("# " fmt "\n", ## __VA_ARGS__) 47 | #define g_assert_cmpstr(left_expr, op, right_expr) \ 48 | do { \ 49 | const char *left = (left_expr); \ 50 | const char *right = (right_expr); \ 51 | if (strcmp0 (left, right) op 0) \ 52 | ok ("%s (\"%s\") %s %s (\"%s\")", #left_expr, left, #op, #right_expr, right); \ 53 | else \ 54 | not_ok ("expected %s (\"%s\") %s %s (\"%s\")", \ 55 | #left_expr, left, #op, #right_expr, right); \ 56 | } while (0) 57 | #define g_assert_cmpint(left_expr, op, right_expr) \ 58 | do { \ 59 | intmax_t left = (left_expr); \ 60 | intmax_t right = (right_expr); \ 61 | if (left op right) \ 62 | ok ("%s (%ji) %s %s (%ji)", #left_expr, left, #op, #right_expr, right); \ 63 | else \ 64 | not_ok ("expected %s (%ji) %s %s (%ji)", \ 65 | #left_expr, left, #op, #right_expr, right); \ 66 | } while (0) 67 | #define g_assert_cmpuint(left_expr, op, right_expr) \ 68 | do { \ 69 | uintmax_t left = (left_expr); \ 70 | uintmax_t right = (right_expr); \ 71 | if (left op right) \ 72 | ok ("%s (%ju) %s %s (%ju)", #left_expr, left, #op, #right_expr, right); \ 73 | else \ 74 | not_ok ("expected %s (%ju) %s %s (%ju)", \ 75 | #left_expr, left, #op, #right_expr, right); \ 76 | } while (0) 77 | #define g_assert_true(expr) \ 78 | do { \ 79 | if ((expr)) \ 80 | ok ("%s", #expr); \ 81 | else \ 82 | not_ok ("expected %s to be true", #expr); \ 83 | } while (0) 84 | #define g_assert_false(expr) \ 85 | do { \ 86 | if (!(expr)) \ 87 | ok ("!(%s)", #expr); \ 88 | else \ 89 | not_ok ("expected %s to be false", #expr); \ 90 | } while (0) 91 | #define g_assert_null(expr) \ 92 | do { \ 93 | if ((expr) == NULL) \ 94 | ok ("%s was null", #expr); \ 95 | else \ 96 | not_ok ("expected %s to be null", #expr); \ 97 | } while (0) 98 | #define g_assert_nonnull(expr) \ 99 | do { \ 100 | if ((expr) != NULL) \ 101 | ok ("%s wasn't null", #expr); \ 102 | else \ 103 | not_ok ("expected %s to be non-null", #expr); \ 104 | } while (0) 105 | 106 | static int 107 | strcmp0 (const char *left, 108 | const char *right) 109 | { 110 | if (left == right) 111 | return 0; 112 | 113 | if (left == NULL) 114 | return -1; 115 | 116 | if (right == NULL) 117 | return 1; 118 | 119 | return strcmp (left, right); 120 | } 121 | 122 | static void 123 | test_n_elements (void) 124 | { 125 | int three[] = { 1, 2, 3 }; 126 | g_assert_cmpuint (N_ELEMENTS (three), ==, 3); 127 | } 128 | 129 | static void 130 | test_strconcat (void) 131 | { 132 | const char *a = "aaa"; 133 | const char *b = "bbb"; 134 | char *ab = strconcat (a, b); 135 | g_assert_cmpstr (ab, ==, "aaabbb"); 136 | free (ab); 137 | } 138 | 139 | static void 140 | test_strconcat3 (void) 141 | { 142 | const char *a = "aaa"; 143 | const char *b = "bbb"; 144 | const char *c = "ccc"; 145 | char *abc = strconcat3 (a, b, c); 146 | g_assert_cmpstr (abc, ==, "aaabbbccc"); 147 | free (abc); 148 | } 149 | 150 | static void 151 | test_has_prefix (void) 152 | { 153 | g_assert_true (has_prefix ("foo", "foo")); 154 | g_assert_true (has_prefix ("foobar", "foo")); 155 | g_assert_false (has_prefix ("foobar", "fool")); 156 | g_assert_false (has_prefix ("foo", "fool")); 157 | g_assert_true (has_prefix ("foo", "")); 158 | g_assert_true (has_prefix ("", "")); 159 | g_assert_false (has_prefix ("", "no")); 160 | g_assert_false (has_prefix ("yes", "no")); 161 | } 162 | 163 | static void 164 | test_has_path_prefix (void) 165 | { 166 | static const struct 167 | { 168 | const char *str; 169 | const char *prefix; 170 | bool expected; 171 | } tests[] = 172 | { 173 | { "/run/host/usr", "/run/host", TRUE }, 174 | { "/run/host/usr", "/run/host/", TRUE }, 175 | { "/run/host", "/run/host", TRUE }, 176 | { "////run///host////usr", "//run//host", TRUE }, 177 | { "////run///host////usr", "//run//host////", TRUE }, 178 | { "/run/hostage", "/run/host", FALSE }, 179 | /* Any number of leading slashes is ignored, even zero */ 180 | { "foo/bar", "/foo", TRUE }, 181 | { "/foo/bar", "foo", TRUE }, 182 | }; 183 | size_t i; 184 | 185 | for (i = 0; i < N_ELEMENTS (tests); i++) 186 | { 187 | const char *str = tests[i].str; 188 | const char *prefix = tests[i].prefix; 189 | bool expected = tests[i].expected; 190 | 191 | if (expected) 192 | g_test_message ("%s should have path prefix %s", str, prefix); 193 | else 194 | g_test_message ("%s should not have path prefix %s", str, prefix); 195 | 196 | if (expected) 197 | g_assert_true (has_path_prefix (str, prefix)); 198 | else 199 | g_assert_false (has_path_prefix (str, prefix)); 200 | } 201 | } 202 | 203 | int 204 | main (int argc UNUSED, 205 | char **argv UNUSED) 206 | { 207 | setvbuf (stdout, NULL, _IONBF, 0); 208 | test_n_elements (); 209 | test_strconcat (); 210 | test_strconcat3 (); 211 | test_has_prefix (); 212 | test_has_path_prefix (); 213 | printf ("1..%u\n", test_number); 214 | return 0; 215 | } 216 | -------------------------------------------------------------------------------- /completions/zsh/_bwrap: -------------------------------------------------------------------------------- 1 | #compdef bwrap 2 | 3 | _bwrap_args_after_perms_size=( 4 | # Please sort alphabetically (in LC_ALL=C order) by option name 5 | '--tmpfs[Mount new tmpfs on DEST]:mount point for tmpfs:_files -/' 6 | ) 7 | 8 | _bwrap_args_after_perms=( 9 | # Please sort alphabetically (in LC_ALL=C order) by option name 10 | '--bind-data[Copy from FD to file which is bind-mounted on DEST]: :_guard "[0-9]#" "file descriptor to read content":destination:_files' 11 | '--dir[Create dir at DEST]:directory to create:_files -/' 12 | '--file[Copy from FD to destination DEST]: :_guard "[0-9]#" "file descriptor to read content from":destination:_files' 13 | '--ro-bind-data[Copy from FD to file which is readonly bind-mounted on DEST]: :_guard "[0-9]#" "file descriptor to read content from":destination:_files' 14 | '--size[Set size in bytes for next action argument]: :->after_perms_size' 15 | '--tmpfs[Mount new tmpfs on DEST]:mount point for tmpfs:_files -/' 16 | ) 17 | 18 | _bwrap_args_after_size=( 19 | # Please sort alphabetically (in LC_ALL=C order) by option name 20 | '--perms[Set permissions for next action argument]: :_guard "[0-7]#" "permissions in octal": :->after_perms_size' 21 | '--tmpfs[Mount new tmpfs on DEST]:mount point for tmpfs:_files -/' 22 | ) 23 | 24 | _bwrap_args=( 25 | '*::arguments:_normal' 26 | $_bwrap_args_after_perms 27 | 28 | # Please sort alphabetically (in LC_ALL=C order) by option name 29 | '--add-seccomp-fd[Load and use seccomp rules from FD]: :_guard "[0-9]#" "file descriptor to read seccomp rules from"' 30 | '--assert-userns-disabled[Fail unless further use of user namespace inside sandbox is disabled]' 31 | '--args[Parse NUL-separated args from FD]: :_guard "[0-9]#" "file descriptor with NUL-separated arguments"' 32 | '--argv0[Set argv0 to the value VALUE before running the program]:value:' 33 | '--as-pid-1[Do not install a reaper process with PID=1]' 34 | '--bind-try[Equal to --bind but ignores non-existent SRC]:source:_files:destination:_files' 35 | '--bind[Bind mount the host path SRC on DEST]:source:_files:destination:_files' 36 | '--block-fd[Block on FD until some data to read is available]: :_guard "[0-9]#" "file descriptor to block on"' 37 | '--cap-add[Add cap CAP when running as privileged user]:capability to add:->caps' 38 | '--cap-drop[Drop cap CAP when running as privileged user]:capability to add:->caps' 39 | '--chdir[Change directory to DIR]:working directory for sandbox: _files -/' 40 | '--chmod[Set permissions]: :_guard "[0-7]#" "permissions in octal":path to set permissions:_files' 41 | '--clearenv[Unset all environment variables]' 42 | '--dev-bind-try[Equal to --dev-bind but ignores non-existent SRC]:source:_files:destination:_files' 43 | '--dev-bind[Bind mount the host path SRC on DEST, allowing device access]:source:_files:destination:_files' 44 | '--dev[Mount new dev on DEST]:mount point for /dev:_files -/' 45 | "--die-with-parent[Kills with SIGKILL child process (COMMAND) when bwrap or bwrap's parent dies.]" 46 | '--disable-userns[Disable further use of user namespaces inside sandbox]' 47 | '--exec-label[Exec label for the sandbox]:SELinux label:_selinux_contexts' 48 | '--file-label[File label for temporary sandbox content]:SELinux label:_selinux_contexts' 49 | '--gid[Custom gid in the sandbox (requires --unshare-user or --userns)]: :_guard "[0-9]#" "numeric group ID"' 50 | '--help[Print help and exit]' 51 | '--hostname[Custom hostname in the sandbox (requires --unshare-uts)]:hostname:' 52 | '--info-fd[Write information about the running container to FD]: :_guard "[0-9]#" "file descriptor to write to"' 53 | '--json-status-fd[Write container status to FD as multiple JSON documents]: :_guard "[0-9]#" "file descriptor to write to"' 54 | '--lock-file[Take a lock on DEST while sandbox is running]:lock file:_files' 55 | '--mqueue[Mount new mqueue on DEST]:mount point for mqueue:_files -/' 56 | '--new-session[Create a new terminal session]' 57 | '--perms[Set permissions for next action argument]: :_guard "[0-7]#" "permissions in octal": :->after_perms' 58 | '--pidns[Use this user namespace (as parent namespace if using --unshare-pid)]: :' 59 | '--proc[Mount new procfs on DEST]:mount point for procfs:_files -/' 60 | '--remount-ro[Remount DEST as readonly; does not recursively remount]:mount point to remount read-only:_files' 61 | '--ro-bind-try[Equal to --ro-bind but ignores non-existent SRC]:source:_files:destination:_files' 62 | '--ro-bind[Bind mount the host path SRC readonly on DEST]:source:_files:destination:_files' 63 | '--seccomp[Load and use seccomp rules from FD]: :_guard "[0-9]#" "file descriptor to read seccomp rules from"' 64 | '--setenv[Set an environment variable]:variable to set:_parameters -g "*export*":value of variable: :' 65 | '--size[Set size in bytes for next action argument]: :->after_size' 66 | '--symlink[Create symlink at DEST with target SRC]:symlink target:_files:symlink to create:_files:' 67 | '--sync-fd[Keep this fd open while sandbox is running]: :_guard "[0-9]#" "file descriptor to keep open"' 68 | '--uid[Custom uid in the sandbox (requires --unshare-user or --userns)]: :_guard "[0-9]#" "numeric group ID"' 69 | '(--clearenv)--unsetenv[Unset an environment variable]:variable to unset:_parameters -g "*export*"' 70 | '--unshare-all[Unshare every namespace we support by default]' 71 | '--unshare-cgroup-try[Create new cgroup namespace if possible else continue by skipping it]' 72 | '--unshare-cgroup[Create new cgroup namespace]' 73 | '--unshare-ipc[Create new ipc namespace]' 74 | '--unshare-net[Create new network namespace]' 75 | '--unshare-pid[Create new pid namespace]' 76 | '(--userns --userns2)--unshare-user[Create new user namespace (may be automatically implied if not setuid)]' 77 | '--unshare-user-try[Create new user namespace if possible else continue by skipping it]' 78 | '--unshare-uts[Create new uts namespace]' 79 | '(--unshare-user)--userns[Use this user namespace (cannot combine with --unshare-user)]: :' 80 | '--userns-block-fd[Block on FD until the user namespace is ready]: :_guard "[0-9]#" "file descriptor to block on"' 81 | '(--unshare-user)--userns2[After setup switch to this user namespace, only useful with --userns]: :' 82 | '--version[Print version]' 83 | ) 84 | 85 | _bwrap() { 86 | _arguments -S $_bwrap_args 87 | case "$state" in 88 | after_perms) 89 | _values -S ' ' 'option' $_bwrap_args_after_perms 90 | ;; 91 | 92 | after_size) 93 | _values -S ' ' 'option' $_bwrap_args_after_size 94 | ;; 95 | 96 | after_perms_size) 97 | _values -S ' ' 'option' $_bwrap_args_after_perms_size 98 | ;; 99 | 100 | caps) 101 | # $ grep -E '#define\sCAP_\w+\s+[0-9]+' /usr/include/linux/capability.h | awk '{print $2}' | xargs echo 102 | local all_caps=( 103 | CAP_CHOWN CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_FSETID \ 104 | CAP_KILL CAP_SETGID CAP_SETUID CAP_SETPCAP CAP_LINUX_IMMUTABLE \ 105 | CAP_NET_BIND_SERVICE CAP_NET_BROADCAST CAP_NET_ADMIN CAP_NET_RAW \ 106 | CAP_IPC_LOCK CAP_IPC_OWNER CAP_SYS_MODULE CAP_SYS_RAWIO CAP_SYS_CHROOT \ 107 | CAP_SYS_PTRACE CAP_SYS_PACCT CAP_SYS_ADMIN CAP_SYS_BOOT CAP_SYS_NICE \ 108 | CAP_SYS_RESOURCE CAP_SYS_TIME CAP_SYS_TTY_CONFIG CAP_MKNOD CAP_LEASE \ 109 | CAP_AUDIT_WRITE CAP_AUDIT_CONTROL CAP_SETFCAP CAP_MAC_OVERRIDE \ 110 | CAP_MAC_ADMIN CAP_SYSLOG CAP_WAKE_ALARM CAP_BLOCK_SUSPEND CAP_AUDIT_READ 111 | ) 112 | _values 'caps' $all_caps 113 | ;; 114 | esac 115 | } 116 | -------------------------------------------------------------------------------- /m4/attributes.m4: -------------------------------------------------------------------------------- 1 | dnl Macros to check the presence of generic (non-typed) symbols. 2 | dnl Copyright (c) 2006-2008 Diego Pettenò 3 | dnl Copyright (c) 2006-2008 xine project 4 | dnl Copyright (c) 2012 Lucas De Marchi 5 | dnl 6 | dnl This program is free software; you can redistribute it and/or modify 7 | dnl it under the terms of the GNU General Public License as published by 8 | dnl the Free Software Foundation; either version 2, or (at your option) 9 | dnl any later version. 10 | dnl 11 | dnl This program is distributed in the hope that it will be useful, 12 | dnl but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | dnl GNU General Public License for more details. 15 | dnl 16 | dnl You should have received a copy of the GNU General Public License 17 | dnl along with this program; if not, write to the Free Software 18 | dnl Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 19 | dnl 02110-1301, USA. 20 | dnl 21 | dnl As a special exception, the copyright owners of the 22 | dnl macro gives unlimited permission to copy, distribute and modify the 23 | dnl configure scripts that are the output of Autoconf when processing the 24 | dnl Macro. You need not follow the terms of the GNU General Public 25 | dnl License when using or distributing such scripts, even though portions 26 | dnl of the text of the Macro appear in them. The GNU General Public 27 | dnl License (GPL) does govern all other use of the material that 28 | dnl constitutes the Autoconf Macro. 29 | dnl 30 | dnl This special exception to the GPL applies to versions of the 31 | dnl Autoconf Macro released by this project. When you make and 32 | dnl distribute a modified version of the Autoconf Macro, you may extend 33 | dnl this special exception to the GPL to apply to your modified version as 34 | dnl well. 35 | 36 | dnl Check if FLAG in ENV-VAR is supported by compiler and append it 37 | dnl to WHERE-TO-APPEND variable. Note that we invert -Wno-* checks to 38 | dnl -W* as gcc cannot test for negated warnings. If a C snippet is passed, 39 | dnl use it, otherwise use a simple main() definition that just returns 0. 40 | dnl CC_CHECK_FLAG_APPEND([WHERE-TO-APPEND], [ENV-VAR], [FLAG], [C-SNIPPET]) 41 | 42 | AC_DEFUN([CC_CHECK_FLAG_APPEND], [ 43 | AC_CACHE_CHECK([if $CC supports flag $3 in envvar $2], 44 | AS_TR_SH([cc_cv_$2_$3]), 45 | [eval "AS_TR_SH([cc_save_$2])='${$2}'" 46 | eval "AS_TR_SH([$2])='${cc_save_$2} -Werror `echo "$3" | sed 's/^-Wno-/-W/'`'" 47 | AC_LINK_IFELSE([AC_LANG_SOURCE(ifelse([$4], [], 48 | [int main(void) { return 0; } ], 49 | [$4]))], 50 | [eval "AS_TR_SH([cc_cv_$2_$3])='yes'"], 51 | [eval "AS_TR_SH([cc_cv_$2_$3])='no'"]) 52 | eval "AS_TR_SH([$2])='$cc_save_$2'"]) 53 | 54 | AS_IF([eval test x$]AS_TR_SH([cc_cv_$2_$3])[ = xyes], 55 | [eval "$1='${$1} $3'"]) 56 | ]) 57 | 58 | dnl CC_CHECK_FLAGS_APPEND([WHERE-TO-APPEND], [ENV-VAR], [FLAG1 FLAG2], [C-SNIPPET]) 59 | AC_DEFUN([CC_CHECK_FLAGS_APPEND], [ 60 | for flag in [$3]; do 61 | CC_CHECK_FLAG_APPEND([$1], [$2], $flag, [$4]) 62 | done 63 | ]) 64 | 65 | dnl Check if the flag is supported by linker (cacheable) 66 | dnl CC_CHECK_LDFLAGS([FLAG], [ACTION-IF-FOUND],[ACTION-IF-NOT-FOUND]) 67 | 68 | AC_DEFUN([CC_CHECK_LDFLAGS], [ 69 | AC_CACHE_CHECK([if $CC supports $1 flag], 70 | AS_TR_SH([cc_cv_ldflags_$1]), 71 | [ac_save_LDFLAGS="$LDFLAGS" 72 | LDFLAGS="$LDFLAGS $1" 73 | AC_LINK_IFELSE([int main() { return 1; }], 74 | [eval "AS_TR_SH([cc_cv_ldflags_$1])='yes'"], 75 | [eval "AS_TR_SH([cc_cv_ldflags_$1])="]) 76 | LDFLAGS="$ac_save_LDFLAGS" 77 | ]) 78 | 79 | AS_IF([eval test x$]AS_TR_SH([cc_cv_ldflags_$1])[ = xyes], 80 | [$2], [$3]) 81 | ]) 82 | 83 | dnl define the LDFLAGS_NOUNDEFINED variable with the correct value for 84 | dnl the current linker to avoid undefined references in a shared object. 85 | AC_DEFUN([CC_NOUNDEFINED], [ 86 | dnl We check $host for which systems to enable this for. 87 | AC_REQUIRE([AC_CANONICAL_HOST]) 88 | 89 | case $host in 90 | dnl FreeBSD (et al.) does not complete linking for shared objects when pthreads 91 | dnl are requested, as different implementations are present; to avoid problems 92 | dnl use -Wl,-z,defs only for those platform not behaving this way. 93 | *-freebsd* | *-openbsd*) ;; 94 | *) 95 | dnl First of all check for the --no-undefined variant of GNU ld. This allows 96 | dnl for a much more readable command line, so that people can understand what 97 | dnl it does without going to look for what the heck -z defs does. 98 | for possible_flags in "-Wl,--no-undefined" "-Wl,-z,defs"; do 99 | CC_CHECK_LDFLAGS([$possible_flags], [LDFLAGS_NOUNDEFINED="$possible_flags"]) 100 | break 101 | done 102 | ;; 103 | esac 104 | 105 | AC_SUBST([LDFLAGS_NOUNDEFINED]) 106 | ]) 107 | 108 | dnl Check for a -Werror flag or equivalent. -Werror is the GCC 109 | dnl and ICC flag that tells the compiler to treat all the warnings 110 | dnl as fatal. We usually need this option to make sure that some 111 | dnl constructs (like attributes) are not simply ignored. 112 | dnl 113 | dnl Other compilers don't support -Werror per se, but they support 114 | dnl an equivalent flag: 115 | dnl - Sun Studio compiler supports -errwarn=%all 116 | AC_DEFUN([CC_CHECK_WERROR], [ 117 | AC_CACHE_CHECK( 118 | [for $CC way to treat warnings as errors], 119 | [cc_cv_werror], 120 | [CC_CHECK_CFLAGS_SILENT([-Werror], [cc_cv_werror=-Werror], 121 | [CC_CHECK_CFLAGS_SILENT([-errwarn=%all], [cc_cv_werror=-errwarn=%all])]) 122 | ]) 123 | ]) 124 | 125 | AC_DEFUN([CC_CHECK_ATTRIBUTE], [ 126 | AC_REQUIRE([CC_CHECK_WERROR]) 127 | AC_CACHE_CHECK([if $CC supports __attribute__(( ifelse([$2], , [$1], [$2]) ))], 128 | AS_TR_SH([cc_cv_attribute_$1]), 129 | [ac_save_CFLAGS="$CFLAGS" 130 | CFLAGS="$CFLAGS $cc_cv_werror" 131 | AC_COMPILE_IFELSE([AC_LANG_SOURCE([$3])], 132 | [eval "AS_TR_SH([cc_cv_attribute_$1])='yes'"], 133 | [eval "AS_TR_SH([cc_cv_attribute_$1])='no'"]) 134 | CFLAGS="$ac_save_CFLAGS" 135 | ]) 136 | 137 | AS_IF([eval test x$]AS_TR_SH([cc_cv_attribute_$1])[ = xyes], 138 | [AC_DEFINE( 139 | AS_TR_CPP([SUPPORT_ATTRIBUTE_$1]), 1, 140 | [Define this if the compiler supports __attribute__(( ifelse([$2], , [$1], [$2]) ))] 141 | ) 142 | $4], 143 | [$5]) 144 | ]) 145 | 146 | AC_DEFUN([CC_ATTRIBUTE_CONSTRUCTOR], [ 147 | CC_CHECK_ATTRIBUTE( 148 | [constructor],, 149 | [void __attribute__((constructor)) ctor() { int a; }], 150 | [$1], [$2]) 151 | ]) 152 | 153 | AC_DEFUN([CC_ATTRIBUTE_FORMAT], [ 154 | CC_CHECK_ATTRIBUTE( 155 | [format], [format(printf, n, n)], 156 | [void __attribute__((format(printf, 1, 2))) printflike(const char *fmt, ...) { fmt = (void *)0; }], 157 | [$1], [$2]) 158 | ]) 159 | 160 | AC_DEFUN([CC_ATTRIBUTE_FORMAT_ARG], [ 161 | CC_CHECK_ATTRIBUTE( 162 | [format_arg], [format_arg(printf)], 163 | [char *__attribute__((format_arg(1))) gettextlike(const char *fmt) { fmt = (void *)0; }], 164 | [$1], [$2]) 165 | ]) 166 | 167 | AC_DEFUN([CC_ATTRIBUTE_VISIBILITY], [ 168 | CC_CHECK_ATTRIBUTE( 169 | [visibility_$1], [visibility("$1")], 170 | [void __attribute__((visibility("$1"))) $1_function() { }], 171 | [$2], [$3]) 172 | ]) 173 | 174 | AC_DEFUN([CC_ATTRIBUTE_NONNULL], [ 175 | CC_CHECK_ATTRIBUTE( 176 | [nonnull], [nonnull()], 177 | [void __attribute__((nonnull())) some_function(void *foo, void *bar) { foo = (void*)0; bar = (void*)0; }], 178 | [$1], [$2]) 179 | ]) 180 | 181 | AC_DEFUN([CC_ATTRIBUTE_UNUSED], [ 182 | CC_CHECK_ATTRIBUTE( 183 | [unused], , 184 | [void some_function(void *foo, __attribute__((unused)) void *bar);], 185 | [$1], [$2]) 186 | ]) 187 | 188 | AC_DEFUN([CC_ATTRIBUTE_SENTINEL], [ 189 | CC_CHECK_ATTRIBUTE( 190 | [sentinel], , 191 | [void some_function(void *foo, ...) __attribute__((sentinel));], 192 | [$1], [$2]) 193 | ]) 194 | 195 | AC_DEFUN([CC_ATTRIBUTE_DEPRECATED], [ 196 | CC_CHECK_ATTRIBUTE( 197 | [deprecated], , 198 | [void some_function(void *foo, ...) __attribute__((deprecated));], 199 | [$1], [$2]) 200 | ]) 201 | 202 | AC_DEFUN([CC_ATTRIBUTE_ALIAS], [ 203 | CC_CHECK_ATTRIBUTE( 204 | [alias], [weak, alias], 205 | [void other_function(void *foo) { } 206 | void some_function(void *foo) __attribute__((weak, alias("other_function")));], 207 | [$1], [$2]) 208 | ]) 209 | 210 | AC_DEFUN([CC_ATTRIBUTE_MALLOC], [ 211 | CC_CHECK_ATTRIBUTE( 212 | [malloc], , 213 | [void * __attribute__((malloc)) my_alloc(int n);], 214 | [$1], [$2]) 215 | ]) 216 | 217 | AC_DEFUN([CC_ATTRIBUTE_PACKED], [ 218 | CC_CHECK_ATTRIBUTE( 219 | [packed], , 220 | [struct astructure { char a; int b; long c; void *d; } __attribute__((packed));], 221 | [$1], [$2]) 222 | ]) 223 | 224 | AC_DEFUN([CC_ATTRIBUTE_CONST], [ 225 | CC_CHECK_ATTRIBUTE( 226 | [const], , 227 | [int __attribute__((const)) twopow(int n) { return 1 << n; } ], 228 | [$1], [$2]) 229 | ]) 230 | 231 | AC_DEFUN([CC_FLAG_VISIBILITY], [ 232 | AC_REQUIRE([CC_CHECK_WERROR]) 233 | AC_CACHE_CHECK([if $CC supports -fvisibility=hidden], 234 | [cc_cv_flag_visibility], 235 | [cc_flag_visibility_save_CFLAGS="$CFLAGS" 236 | CFLAGS="$CFLAGS $cc_cv_werror" 237 | CC_CHECK_CFLAGS_SILENT([-fvisibility=hidden], 238 | cc_cv_flag_visibility='yes', 239 | cc_cv_flag_visibility='no') 240 | CFLAGS="$cc_flag_visibility_save_CFLAGS"]) 241 | 242 | AS_IF([test "x$cc_cv_flag_visibility" = "xyes"], 243 | [AC_DEFINE([SUPPORT_FLAG_VISIBILITY], 1, 244 | [Define this if the compiler supports the -fvisibility flag]) 245 | $1], 246 | [$2]) 247 | ]) 248 | 249 | AC_DEFUN([CC_FUNC_EXPECT], [ 250 | AC_REQUIRE([CC_CHECK_WERROR]) 251 | AC_CACHE_CHECK([if compiler has __builtin_expect function], 252 | [cc_cv_func_expect], 253 | [ac_save_CFLAGS="$CFLAGS" 254 | CFLAGS="$CFLAGS $cc_cv_werror" 255 | AC_COMPILE_IFELSE([AC_LANG_SOURCE( 256 | [int some_function() { 257 | int a = 3; 258 | return (int)__builtin_expect(a, 3); 259 | }])], 260 | [cc_cv_func_expect=yes], 261 | [cc_cv_func_expect=no]) 262 | CFLAGS="$ac_save_CFLAGS" 263 | ]) 264 | 265 | AS_IF([test "x$cc_cv_func_expect" = "xyes"], 266 | [AC_DEFINE([SUPPORT__BUILTIN_EXPECT], 1, 267 | [Define this if the compiler supports __builtin_expect() function]) 268 | $1], 269 | [$2]) 270 | ]) 271 | 272 | AC_DEFUN([CC_ATTRIBUTE_ALIGNED], [ 273 | AC_REQUIRE([CC_CHECK_WERROR]) 274 | AC_CACHE_CHECK([highest __attribute__ ((aligned ())) supported], 275 | [cc_cv_attribute_aligned], 276 | [ac_save_CFLAGS="$CFLAGS" 277 | CFLAGS="$CFLAGS $cc_cv_werror" 278 | for cc_attribute_align_try in 64 32 16 8 4 2; do 279 | AC_COMPILE_IFELSE([AC_LANG_SOURCE([ 280 | int main() { 281 | static char c __attribute__ ((aligned($cc_attribute_align_try))) = 0; 282 | return c; 283 | }])], [cc_cv_attribute_aligned=$cc_attribute_align_try; break]) 284 | done 285 | CFLAGS="$ac_save_CFLAGS" 286 | ]) 287 | 288 | if test "x$cc_cv_attribute_aligned" != "x"; then 289 | AC_DEFINE_UNQUOTED([ATTRIBUTE_ALIGNED_MAX], [$cc_cv_attribute_aligned], 290 | [Define the highest alignment supported]) 291 | fi 292 | ]) 293 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Bubblewrap 2 | ========== 3 | 4 | Many container runtime tools like `systemd-nspawn`, `docker`, 5 | etc. focus on providing infrastructure for system administrators and 6 | orchestration tools (e.g. Kubernetes) to run containers. 7 | 8 | These tools are not suitable to give to unprivileged users, because it 9 | is trivial to turn such access into a fully privileged root shell 10 | on the host. 11 | 12 | User namespaces 13 | --------------- 14 | 15 | There is an effort in the Linux kernel called 16 | [user namespaces](https://www.google.com/search?q=user+namespaces+site%3Ahttps%3A%2F%2Flwn.net) 17 | which attempts to allow unprivileged users to use container features. 18 | While significant progress has been made, there are 19 | [still concerns](https://lwn.net/Articles/673597/) about it, and 20 | it is not available to unprivileged users in several production distributions 21 | such as CentOS/Red Hat Enterprise Linux 7, Debian Jessie, etc. 22 | 23 | See for example 24 | [CVE-2016-3135](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-3135) 25 | which is a local root vulnerability introduced by userns. 26 | [This March 2016 post](https://lkml.org/lkml/2016/3/9/555) has some 27 | more discussion. 28 | 29 | Bubblewrap could be viewed as setuid implementation of a *subset* of 30 | user namespaces. Emphasis on subset - specifically relevant to the 31 | above CVE, bubblewrap does not allow control over iptables. 32 | 33 | The original bubblewrap code existed before user namespaces - it inherits code from 34 | [xdg-app helper](https://cgit.freedesktop.org/xdg-app/xdg-app/tree/common/xdg-app-helper.c?id=4c3bf179e2e4a2a298cd1db1d045adaf3f564532) 35 | which in turn distantly derives from 36 | [linux-user-chroot](https://git.gnome.org/browse/linux-user-chroot). 37 | 38 | System security 39 | --------------- 40 | 41 | The maintainers of this tool believe that it does not, even when used 42 | in combination with typical software installed on that distribution, 43 | allow privilege escalation. It may increase the ability of a logged 44 | in user to perform denial of service attacks, however. 45 | 46 | In particular, bubblewrap uses `PR_SET_NO_NEW_PRIVS` to turn off 47 | setuid binaries, which is the [traditional way](https://en.wikipedia.org/wiki/Chroot#Limitations) to get out of things 48 | like chroots. 49 | 50 | Sandbox security 51 | ---------------- 52 | 53 | bubblewrap is a tool for constructing sandbox environments. 54 | bubblewrap is not a complete, ready-made sandbox with a specific security 55 | policy. 56 | 57 | Some of bubblewrap's use-cases want a security boundary between the sandbox 58 | and the real system; other use-cases want the ability to change the layout of 59 | the filesystem for processes inside the sandbox, but do not aim to be a 60 | security boundary. 61 | As a result, the level of protection between the sandboxed processes and 62 | the host system is entirely determined by the arguments passed to 63 | bubblewrap. 64 | 65 | Whatever program constructs the command-line arguments for bubblewrap 66 | (often a larger framework like Flatpak, libgnome-desktop, sandwine 67 | or an ad-hoc script) is responsible for defining its own security model, 68 | and choosing appropriate bubblewrap command-line arguments to implement 69 | that security model. 70 | 71 | Some aspects of sandbox security that require particular care are described 72 | in the [Limitations](#limitations) section below. 73 | 74 | Users 75 | ----- 76 | 77 | This program can be shared by all container tools which perform 78 | non-root operation, such as: 79 | 80 | - [Flatpak](http://www.flatpak.org) 81 | - [rpm-ostree unprivileged](https://github.com/projectatomic/rpm-ostree/pull/209) 82 | - [bwrap-oci](https://github.com/projectatomic/bwrap-oci) 83 | 84 | We would also like to see this be available in Kubernetes/OpenShift 85 | clusters. Having the ability for unprivileged users to use container 86 | features would make it significantly easier to do interactive 87 | debugging scenarios and the like. 88 | 89 | Installation 90 | ------------ 91 | 92 | bubblewrap is available in the package repositories of the most Linux distributions 93 | and can be installed from there. 94 | 95 | If you need to build bubblewrap from source, you can do this with meson or autotools. 96 | 97 | meson: 98 | 99 | ``` 100 | meson _builddir 101 | meson compile -C _builddir 102 | meson test -C _builddir 103 | meson install -C _builddir 104 | ``` 105 | 106 | autotools: 107 | 108 | ``` 109 | ./autogen.sh 110 | make 111 | sudo make install 112 | ``` 113 | 114 | Usage 115 | ----- 116 | 117 | bubblewrap works by creating a new, completely empty, mount 118 | namespace where the root is on a tmpfs that is invisible from the 119 | host, and will be automatically cleaned up when the last process 120 | exits. You can then use commandline options to construct the root 121 | filesystem and process environment and command to run in the 122 | namespace. 123 | 124 | There's a larger [demo script](./demos/bubblewrap-shell.sh) in the 125 | source code, but here's a trimmed down version which runs 126 | a new shell reusing the host's `/usr`. 127 | 128 | ``` 129 | bwrap \ 130 | --ro-bind /usr /usr \ 131 | --symlink usr/lib64 /lib64 \ 132 | --proc /proc \ 133 | --dev /dev \ 134 | --unshare-pid \ 135 | --new-session \ 136 | bash 137 | ``` 138 | 139 | This is an incomplete example, but useful for purposes of 140 | illustration. More often, rather than creating a container using the 141 | host's filesystem tree, you want to target a chroot. There, rather 142 | than creating the symlink `lib64 -> usr/lib64` in the tmpfs, you might 143 | have already created it in the target rootfs. 144 | 145 | Sandboxing 146 | ---------- 147 | 148 | The goal of bubblewrap is to run an application in a sandbox, where it 149 | has restricted access to parts of the operating system or user data 150 | such as the home directory. 151 | 152 | bubblewrap always creates a new mount namespace, and the user can specify 153 | exactly what parts of the filesystem should be visible in the sandbox. 154 | Any such directories you specify mounted `nodev` by default, and can be made readonly. 155 | 156 | Additionally you can use these kernel features: 157 | 158 | User namespaces ([CLONE_NEWUSER](http://linux.die.net/man/2/clone)): This hides all but the current uid and gid from the 159 | sandbox. You can also change what the value of uid/gid should be in the sandbox. 160 | 161 | IPC namespaces ([CLONE_NEWIPC](http://linux.die.net/man/2/clone)): The sandbox will get its own copy of all the 162 | different forms of IPCs, like SysV shared memory and semaphores. 163 | 164 | PID namespaces ([CLONE_NEWPID](http://linux.die.net/man/2/clone)): The sandbox will not see any processes outside the sandbox. Additionally, bubblewrap will run a trivial pid1 inside your container to handle the requirements of reaping children in the sandbox. This avoids what is known now as the [Docker pid 1 problem](https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/). 165 | 166 | 167 | Network namespaces ([CLONE_NEWNET](http://linux.die.net/man/2/clone)): The sandbox will not see the network. Instead it will have its own network namespace with only a loopback device. 168 | 169 | UTS namespace ([CLONE_NEWUTS](http://linux.die.net/man/2/clone)): The sandbox will have its own hostname. 170 | 171 | Seccomp filters: You can pass in seccomp filters that limit which syscalls can be done in the sandbox. For more information, see [Seccomp](https://en.wikipedia.org/wiki/Seccomp). 172 | 173 | Limitations 174 | ----------- 175 | 176 | As noted in the [Sandbox security](#sandbox-security) section above, 177 | the level of protection between the sandboxed processes and the host system 178 | is entirely determined by the arguments passed to bubblewrap. 179 | Some aspects that require special care are noted here. 180 | 181 | - If you are not filtering out `TIOCSTI` commands using seccomp filters, 182 | argument `--new-session` is needed to protect against out-of-sandbox 183 | command execution 184 | (see [CVE-2017-5226](https://github.com/containers/bubblewrap/issues/142)). 185 | 186 | - Everything mounted into the sandbox can potentially be used to escalate 187 | privileges. 188 | For example, if you bind a D-Bus socket into the sandbox, it can be used to 189 | execute commands via systemd. You can use 190 | [xdg-dbus-proxy](https://github.com/flatpak/xdg-dbus-proxy) to filter 191 | D-Bus communication. 192 | 193 | - Some applications deploy their own sandboxing mechanisms, and these can be 194 | restricted by the constraints imposed by bubblewrap's sandboxing. 195 | For example, some web browsers which configure their child proccesses via 196 | seccomp to not have access to the filesystem. If you limit the syscalls and 197 | don't allow the seccomp syscall, a browser cannot apply these restrictions. 198 | Similarly, if these rules were compiled into a file that is not available in 199 | the sandbox, the browser cannot load these rules from this file and cannot 200 | apply these restrictions. 201 | 202 | Related project comparison: Firejail 203 | ------------------------------------ 204 | 205 | [Firejail](https://github.com/netblue30/firejail/tree/HEAD/src/firejail) 206 | is similar to Flatpak before bubblewrap was split out in that it combines 207 | a setuid tool with a lot of desktop-specific sandboxing features. For 208 | example, Firejail knows about Pulseaudio, whereas bubblewrap does not. 209 | 210 | The bubblewrap authors believe it's much easier to audit a small 211 | setuid program, and keep features such as Pulseaudio filtering as an 212 | unprivileged process, as now occurs in Flatpak. 213 | 214 | Also, @cgwalters thinks trying to 215 | [whitelist file paths](https://github.com/netblue30/firejail/blob/37a5a3545ef6d8d03dad8bbd888f53e13274c9e5/src/firejail/fs_whitelist.c#L176) 216 | is a bad idea given the myriad ways users have to manipulate paths, 217 | and the myriad ways in which system administrators may configure a 218 | system. The bubblewrap approach is to only retain a few specific 219 | Linux capabilities such as `CAP_SYS_ADMIN`, but to always access the 220 | filesystem as the invoking uid. This entirely closes 221 | [TOCTTOU attacks](https://cwe.mitre.org/data/definitions/367.html) and 222 | such. 223 | 224 | Related project comparison: Sandstorm.io 225 | ---------------------------------------- 226 | 227 | [Sandstorm.io](https://sandstorm.io/) requires unprivileged user 228 | namespaces to set up its sandbox, though it could easily be adapted 229 | to operate in a setuid mode as well. @cgwalters believes their code is 230 | fairly good, but it could still make sense to unify on bubblewrap. 231 | However, @kentonv (of Sandstorm) feels that while this makes sense 232 | in principle, the switching cost outweighs the practical benefits for 233 | now. This decision could be re-evaluated in the future, but it is not 234 | being actively pursued today. 235 | 236 | Related project comparison: runc/binctr 237 | ---------------------------------------- 238 | 239 | [runC](https://github.com/opencontainers/runc) is currently working on 240 | supporting [rootless containers](https://github.com/opencontainers/runc/pull/774), 241 | without needing `setuid` or any other privileges during installation of 242 | runC (using unprivileged user namespaces rather than `setuid`), 243 | creation, and management of containers. However, the standard mode of 244 | using runC is similar to [systemd nspawn](https://www.freedesktop.org/software/systemd/man/systemd-nspawn.html) 245 | in that it is tooling intended to be invoked by root. 246 | 247 | The bubblewrap authors believe that runc and systemd-nspawn are not 248 | designed to be made setuid, and are distant from supporting such a mode. 249 | However with rootless containers, runC will be able to fulfill certain usecases 250 | that bubblewrap supports (with the added benefit of being a standardised and 251 | complete OCI runtime). 252 | 253 | [binctr](https://github.com/jfrazelle/binctr) is just a wrapper for 254 | runC, so inherits all of its design tradeoffs. 255 | 256 | What's with the name?! 257 | ---------------------- 258 | 259 | The name bubblewrap was chosen to convey that this 260 | tool runs as the parent of the application (so wraps it in some sense) and creates 261 | a protective layer (the sandbox) around it. 262 | 263 | ![](bubblewrap.jpg) 264 | 265 | (Bubblewrap cat by [dancing_stupidity](https://www.flickr.com/photos/27549668@N03/)) 266 | -------------------------------------------------------------------------------- /git.mk: -------------------------------------------------------------------------------- 1 | # git.mk, a small Makefile to autogenerate .gitignore files 2 | # for autotools-based projects. 3 | # 4 | # Copyright 2009, Red Hat, Inc. 5 | # Copyright 2010,2011,2012,2013 Behdad Esfahbod 6 | # Written by Behdad Esfahbod 7 | # 8 | # Copying and distribution of this file, with or without modification, 9 | # is permitted in any medium without royalty provided the copyright 10 | # notice and this notice are preserved. 11 | # 12 | # The latest version of this file can be downloaded from: 13 | GIT_MK_URL = https://raw.githubusercontent.com/behdad/git.mk/master/git.mk 14 | # 15 | # Bugs, etc, should be reported upstream at: 16 | # https://github.com/behdad/git.mk 17 | # 18 | # To use in your project, import this file in your git repo's toplevel, 19 | # then do "make -f git.mk". This modifies all Makefile.am files in 20 | # your project to -include git.mk. Remember to add that line to new 21 | # Makefile.am files you create in your project, or just rerun the 22 | # "make -f git.mk". 23 | # 24 | # This enables automatic .gitignore generation. If you need to ignore 25 | # more files, add them to the GITIGNOREFILES variable in your Makefile.am. 26 | # But think twice before doing that. If a file has to be in .gitignore, 27 | # chances are very high that it's a generated file and should be in one 28 | # of MOSTLYCLEANFILES, CLEANFILES, DISTCLEANFILES, or MAINTAINERCLEANFILES. 29 | # 30 | # The only case that you need to manually add a file to GITIGNOREFILES is 31 | # when remove files in one of mostlyclean-local, clean-local, distclean-local, 32 | # or maintainer-clean-local make targets. 33 | # 34 | # Note that for files like editor backup, etc, there are better places to 35 | # ignore them. See "man gitignore". 36 | # 37 | # If "make maintainer-clean" removes the files but they are not recognized 38 | # by this script (that is, if "git status" shows untracked files still), send 39 | # me the output of "git status" as well as your Makefile.am and Makefile for 40 | # the directories involved and I'll diagnose. 41 | # 42 | # For a list of toplevel files that should be in MAINTAINERCLEANFILES, see 43 | # Makefile.am.sample in the git.mk git repo. 44 | # 45 | # Don't EXTRA_DIST this file. It is supposed to only live in git clones, 46 | # not tarballs. It serves no useful purpose in tarballs and clutters the 47 | # build dir. 48 | # 49 | # This file knows how to handle autoconf, automake, libtool, gtk-doc, 50 | # gnome-doc-utils, yelp.m4, mallard, intltool, gsettings, dejagnu, appdata, 51 | # appstream. 52 | # 53 | # This makefile provides the following targets: 54 | # 55 | # - all: "make all" will build all gitignore files. 56 | # - gitignore: makes all gitignore files in the current dir and subdirs. 57 | # - .gitignore: make gitignore file for the current dir. 58 | # - gitignore-recurse: makes all gitignore files in the subdirs. 59 | # 60 | # KNOWN ISSUES: 61 | # 62 | # - Recursive configure doesn't work as $(top_srcdir)/git.mk inside the 63 | # submodule doesn't find us. If you have configure.{in,ac} files in 64 | # subdirs, add a proxy git.mk file in those dirs that simply does: 65 | # "include $(top_srcdir)/../git.mk". Add more ..'s to your taste. 66 | # And add those files to git. See vte/gnome-pty-helper/git.mk for 67 | # example. 68 | # 69 | 70 | 71 | 72 | ############################################################################### 73 | # Variables user modules may want to add to toplevel MAINTAINERCLEANFILES: 74 | ############################################################################### 75 | 76 | # 77 | # Most autotools-using modules should be fine including this variable in their 78 | # toplevel MAINTAINERCLEANFILES: 79 | GITIGNORE_MAINTAINERCLEANFILES_TOPLEVEL = \ 80 | $(srcdir)/aclocal.m4 \ 81 | $(srcdir)/autoscan.log \ 82 | $(srcdir)/configure.scan \ 83 | `AUX_DIR=$(srcdir)/$$(cd $(top_srcdir); $(AUTOCONF) --trace 'AC_CONFIG_AUX_DIR:$$1' ./configure.ac); \ 84 | test "x$$AUX_DIR" = "x$(srcdir)/" && AUX_DIR=$(srcdir); \ 85 | for x in \ 86 | ar-lib \ 87 | compile \ 88 | config.guess \ 89 | config.sub \ 90 | depcomp \ 91 | install-sh \ 92 | ltmain.sh \ 93 | missing \ 94 | mkinstalldirs \ 95 | test-driver \ 96 | ylwrap \ 97 | ; do echo "$$AUX_DIR/$$x"; done` \ 98 | `cd $(top_srcdir); $(AUTOCONF) --trace 'AC_CONFIG_HEADERS:$$1' ./configure.ac | \ 99 | head -n 1 | while read f; do echo "$(srcdir)/$$f.in"; done` 100 | # 101 | # All modules should also be fine including the following variable, which 102 | # removes automake-generated Makefile.in files: 103 | GITIGNORE_MAINTAINERCLEANFILES_MAKEFILE_IN = \ 104 | `cd $(top_srcdir); $(AUTOCONF) --trace 'AC_CONFIG_FILES:$$1' ./configure.ac | \ 105 | while read f; do \ 106 | case $$f in Makefile|*/Makefile) \ 107 | test -f "$(srcdir)/$$f.am" && echo "$(srcdir)/$$f.in";; esac; \ 108 | done` 109 | # 110 | # Modules that use libtool and use AC_CONFIG_MACRO_DIR() may also include this, 111 | # though it's harmless to include regardless. 112 | GITIGNORE_MAINTAINERCLEANFILES_M4_LIBTOOL = \ 113 | `MACRO_DIR=$(srcdir)/$$(cd $(top_srcdir); $(AUTOCONF) --trace 'AC_CONFIG_MACRO_DIR:$$1' ./configure.ac); \ 114 | if test "x$$MACRO_DIR" != "x$(srcdir)/"; then \ 115 | for x in \ 116 | libtool.m4 \ 117 | ltoptions.m4 \ 118 | ltsugar.m4 \ 119 | ltversion.m4 \ 120 | lt~obsolete.m4 \ 121 | ; do echo "$$MACRO_DIR/$$x"; done; \ 122 | fi` 123 | 124 | 125 | 126 | ############################################################################### 127 | # Default rule is to install ourselves in all Makefile.am files: 128 | ############################################################################### 129 | 130 | git-all: git-mk-install 131 | 132 | git-mk-install: 133 | @echo "Installing git makefile" 134 | @any_failed=; \ 135 | find "`test -z "$(top_srcdir)" && echo . || echo "$(top_srcdir)"`" -name Makefile.am | while read x; do \ 136 | if grep 'include .*/git.mk' $$x >/dev/null; then \ 137 | echo "$$x already includes git.mk"; \ 138 | else \ 139 | failed=; \ 140 | echo "Updating $$x"; \ 141 | { cat $$x; \ 142 | echo ''; \ 143 | echo '-include $$(top_srcdir)/git.mk'; \ 144 | } > $$x.tmp || failed=1; \ 145 | if test x$$failed = x; then \ 146 | mv $$x.tmp $$x || failed=1; \ 147 | fi; \ 148 | if test x$$failed = x; then : else \ 149 | echo "Failed updating $$x"; >&2 \ 150 | any_failed=1; \ 151 | fi; \ 152 | fi; done; test -z "$$any_failed" 153 | 154 | git-mk-update: 155 | wget $(GIT_MK_URL) -O $(top_srcdir)/git.mk 156 | 157 | .PHONY: git-all git-mk-install git-mk-update 158 | 159 | 160 | 161 | ############################################################################### 162 | # Actual .gitignore generation: 163 | ############################################################################### 164 | 165 | $(srcdir)/.gitignore: Makefile.am $(top_srcdir)/git.mk 166 | @echo "git.mk: Generating $@" 167 | @{ \ 168 | if test "x$(DOC_MODULE)" = x -o "x$(DOC_MAIN_SGML_FILE)" = x; then :; else \ 169 | for x in \ 170 | $(DOC_MODULE)-decl-list.txt \ 171 | $(DOC_MODULE)-decl.txt \ 172 | tmpl/$(DOC_MODULE)-unused.sgml \ 173 | "tmpl/*.bak" \ 174 | $(REPORT_FILES) \ 175 | $(DOC_MODULE).pdf \ 176 | xml html \ 177 | ; do echo "/$$x"; done; \ 178 | FLAVOR=$$(cd $(top_srcdir); $(AUTOCONF) --trace 'GTK_DOC_CHECK:$$2' ./configure.ac); \ 179 | case $$FLAVOR in *no-tmpl*) echo /tmpl;; esac; \ 180 | if echo "$(SCAN_OPTIONS)" | grep -q "\-\-rebuild-types"; then \ 181 | echo "/$(DOC_MODULE).types"; \ 182 | fi; \ 183 | if echo "$(SCAN_OPTIONS)" | grep -q "\-\-rebuild-sections"; then \ 184 | echo "/$(DOC_MODULE)-sections.txt"; \ 185 | fi; \ 186 | if test "$(abs_srcdir)" != "$(abs_builddir)" ; then \ 187 | for x in \ 188 | $(SETUP_FILES) \ 189 | $(DOC_MODULE).types \ 190 | ; do echo "/$$x"; done; \ 191 | fi; \ 192 | fi; \ 193 | if test "x$(DOC_MODULE)$(DOC_ID)" = x -o "x$(DOC_LINGUAS)" = x; then :; else \ 194 | for lc in $(DOC_LINGUAS); do \ 195 | for x in \ 196 | $(if $(DOC_MODULE),$(DOC_MODULE).xml) \ 197 | $(DOC_PAGES) \ 198 | $(DOC_INCLUDES) \ 199 | ; do echo "/$$lc/$$x"; done; \ 200 | done; \ 201 | for x in \ 202 | $(_DOC_OMF_ALL) \ 203 | $(_DOC_DSK_ALL) \ 204 | $(_DOC_HTML_ALL) \ 205 | $(_DOC_MOFILES) \ 206 | $(DOC_H_FILE) \ 207 | "*/.xml2po.mo" \ 208 | "*/*.omf.out" \ 209 | ; do echo /$$x; done; \ 210 | fi; \ 211 | if test "x$(HELP_ID)" = x -o "x$(HELP_LINGUAS)" = x; then :; else \ 212 | for lc in $(HELP_LINGUAS); do \ 213 | for x in \ 214 | $(HELP_FILES) \ 215 | "$$lc.stamp" \ 216 | "$$lc.mo" \ 217 | ; do echo "/$$lc/$$x"; done; \ 218 | done; \ 219 | fi; \ 220 | if test "x$(gsettings_SCHEMAS)" = x; then :; else \ 221 | for x in \ 222 | $(gsettings_SCHEMAS:.xml=.valid) \ 223 | $(gsettings__enum_file) \ 224 | ; do echo "/$$x"; done; \ 225 | fi; \ 226 | if test "x$(appdata_XML)" = x; then :; else \ 227 | for x in \ 228 | $(appdata_XML:.xml=.valid) \ 229 | ; do echo "/$$x"; done; \ 230 | fi; \ 231 | if test "x$(appstream_XML)" = x; then :; else \ 232 | for x in \ 233 | $(appstream_XML:.xml=.valid) \ 234 | ; do echo "/$$x"; done; \ 235 | fi; \ 236 | if test -f $(srcdir)/po/Makefile.in.in; then \ 237 | for x in \ 238 | po/Makefile.in.in \ 239 | po/Makefile.in.in~ \ 240 | po/Makefile.in \ 241 | po/Makefile \ 242 | po/Makevars.template \ 243 | po/POTFILES \ 244 | po/Rules-quot \ 245 | po/stamp-it \ 246 | po/stamp-po \ 247 | po/.intltool-merge-cache \ 248 | "po/*.gmo" \ 249 | "po/*.header" \ 250 | "po/*.mo" \ 251 | "po/*.sed" \ 252 | "po/*.sin" \ 253 | po/$(GETTEXT_PACKAGE).pot \ 254 | intltool-extract.in \ 255 | intltool-merge.in \ 256 | intltool-update.in \ 257 | ; do echo "/$$x"; done; \ 258 | fi; \ 259 | if test -f $(srcdir)/configure; then \ 260 | for x in \ 261 | autom4te.cache \ 262 | configure \ 263 | config.h \ 264 | stamp-h1 \ 265 | libtool \ 266 | config.lt \ 267 | ; do echo "/$$x"; done; \ 268 | fi; \ 269 | if test "x$(DEJATOOL)" = x; then :; else \ 270 | for x in \ 271 | $(DEJATOOL) \ 272 | ; do echo "/$$x.sum"; echo "/$$x.log"; done; \ 273 | echo /site.exp; \ 274 | fi; \ 275 | if test "x$(am__dirstamp)" = x; then :; else \ 276 | echo "$(am__dirstamp)"; \ 277 | fi; \ 278 | if test "x$(LTCOMPILE)" = x -a "x$(LTCXXCOMPILE)" = x -a "x$(GTKDOC_RUN)" = x; then :; else \ 279 | for x in \ 280 | "*.lo" \ 281 | ".libs" "_libs" \ 282 | ; do echo "$$x"; done; \ 283 | fi; \ 284 | for x in \ 285 | .gitignore \ 286 | $(GITIGNOREFILES) \ 287 | $(CLEANFILES) \ 288 | $(PROGRAMS) $(check_PROGRAMS) $(EXTRA_PROGRAMS) \ 289 | $(LIBRARIES) $(check_LIBRARIES) $(EXTRA_LIBRARIES) \ 290 | $(LTLIBRARIES) $(check_LTLIBRARIES) $(EXTRA_LTLIBRARIES) \ 291 | so_locations \ 292 | $(MOSTLYCLEANFILES) \ 293 | $(TEST_LOGS) \ 294 | $(TEST_LOGS:.log=.trs) \ 295 | $(TEST_SUITE_LOG) \ 296 | $(TESTS:=.test) \ 297 | "*.gcda" \ 298 | "*.gcno" \ 299 | $(DISTCLEANFILES) \ 300 | $(am__CONFIG_DISTCLEAN_FILES) \ 301 | $(CONFIG_CLEAN_FILES) \ 302 | TAGS ID GTAGS GRTAGS GSYMS GPATH tags \ 303 | "*.tab.c" \ 304 | $(MAINTAINERCLEANFILES) \ 305 | $(BUILT_SOURCES) \ 306 | $(patsubst %.vala,%.c,$(filter %.vala,$(SOURCES))) \ 307 | $(filter %_vala.stamp,$(DIST_COMMON)) \ 308 | $(filter %.vapi,$(DIST_COMMON)) \ 309 | $(filter $(addprefix %,$(notdir $(patsubst %.vapi,%.h,$(filter %.vapi,$(DIST_COMMON))))),$(DIST_COMMON)) \ 310 | Makefile \ 311 | Makefile.in \ 312 | "*.orig" \ 313 | "*.rej" \ 314 | "*.bak" \ 315 | "*~" \ 316 | ".*.sw[nop]" \ 317 | ".dirstamp" \ 318 | ; do echo "/$$x"; done; \ 319 | for x in \ 320 | "*.$(OBJEXT)" \ 321 | $(DEPDIR) \ 322 | ; do echo "$$x"; done; \ 323 | } | \ 324 | sed "s@^/`echo "$(srcdir)" | sed 's/\(.\)/[\1]/g'`/@/@" | \ 325 | sed 's@/[.]/@/@g' | \ 326 | LC_ALL=C sort | uniq > $@.tmp && \ 327 | mv $@.tmp $@; 328 | 329 | all: $(srcdir)/.gitignore gitignore-recurse-maybe 330 | gitignore: $(srcdir)/.gitignore gitignore-recurse 331 | 332 | gitignore-recurse-maybe: 333 | @for subdir in $(DIST_SUBDIRS); do \ 334 | case " $(SUBDIRS) " in \ 335 | *" $$subdir "*) :;; \ 336 | *) test "$$subdir" = . -o -e "$$subdir/.git" || (cd $$subdir && $(MAKE) $(AM_MAKEFLAGS) gitignore || echo "Skipping $$subdir");; \ 337 | esac; \ 338 | done 339 | gitignore-recurse: 340 | @for subdir in $(DIST_SUBDIRS); do \ 341 | test "$$subdir" = . -o -e "$$subdir/.git" || (cd $$subdir && $(MAKE) $(AM_MAKEFLAGS) gitignore || echo "Skipping $$subdir"); \ 342 | done 343 | 344 | maintainer-clean: gitignore-clean 345 | gitignore-clean: 346 | -rm -f $(srcdir)/.gitignore 347 | 348 | .PHONY: gitignore-clean gitignore gitignore-recurse gitignore-recurse-maybe 349 | -------------------------------------------------------------------------------- /bind-mount.c: -------------------------------------------------------------------------------- 1 | /* bubblewrap 2 | * Copyright (C) 2016 Alexander Larsson 3 | * SPDX-License-Identifier: LGPL-2.0-or-later 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 2 of the License, or (at your option) any later version. 9 | * 10 | * This library is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this library. If not, see . 17 | * 18 | */ 19 | 20 | #include "config.h" 21 | 22 | #include 23 | 24 | #include "utils.h" 25 | #include "bind-mount.h" 26 | 27 | static char * 28 | skip_token (char *line, bool eat_whitespace) 29 | { 30 | while (*line != ' ' && *line != '\n') 31 | line++; 32 | 33 | if (eat_whitespace && *line == ' ') 34 | line++; 35 | 36 | return line; 37 | } 38 | 39 | static char * 40 | unescape_inline (char *escaped) 41 | { 42 | char *unescaped, *res; 43 | const char *end; 44 | 45 | res = escaped; 46 | end = escaped + strlen (escaped); 47 | 48 | unescaped = escaped; 49 | while (escaped < end) 50 | { 51 | if (*escaped == '\\') 52 | { 53 | *unescaped++ = 54 | ((escaped[1] - '0') << 6) | 55 | ((escaped[2] - '0') << 3) | 56 | ((escaped[3] - '0') << 0); 57 | escaped += 4; 58 | } 59 | else 60 | { 61 | *unescaped++ = *escaped++; 62 | } 63 | } 64 | *unescaped = 0; 65 | return res; 66 | } 67 | 68 | static bool 69 | match_token (const char *token, const char *token_end, const char *str) 70 | { 71 | while (token != token_end && *token == *str) 72 | { 73 | token++; 74 | str++; 75 | } 76 | if (token == token_end) 77 | return *str == 0; 78 | 79 | return FALSE; 80 | } 81 | 82 | static unsigned long 83 | decode_mountoptions (const char *options) 84 | { 85 | const char *token, *end_token; 86 | int i; 87 | unsigned long flags = 0; 88 | static const struct { int flag; 89 | const char *name; 90 | } flags_data[] = { 91 | { 0, "rw" }, 92 | { MS_RDONLY, "ro" }, 93 | { MS_NOSUID, "nosuid" }, 94 | { MS_NODEV, "nodev" }, 95 | { MS_NOEXEC, "noexec" }, 96 | { MS_NOATIME, "noatime" }, 97 | { MS_NODIRATIME, "nodiratime" }, 98 | { MS_RELATIME, "relatime" }, 99 | { 0, NULL } 100 | }; 101 | 102 | token = options; 103 | do 104 | { 105 | end_token = strchr (token, ','); 106 | if (end_token == NULL) 107 | end_token = token + strlen (token); 108 | 109 | for (i = 0; flags_data[i].name != NULL; i++) 110 | { 111 | if (match_token (token, end_token, flags_data[i].name)) 112 | { 113 | flags |= flags_data[i].flag; 114 | break; 115 | } 116 | } 117 | 118 | if (*end_token != 0) 119 | token = end_token + 1; 120 | else 121 | token = NULL; 122 | } 123 | while (token != NULL); 124 | 125 | return flags; 126 | } 127 | 128 | typedef struct MountInfo MountInfo; 129 | struct MountInfo { 130 | char *mountpoint; 131 | unsigned long options; 132 | }; 133 | 134 | typedef MountInfo *MountTab; 135 | 136 | static void 137 | mount_tab_free (MountTab tab) 138 | { 139 | int i; 140 | 141 | for (i = 0; tab[i].mountpoint != NULL; i++) 142 | free (tab[i].mountpoint); 143 | free (tab); 144 | } 145 | 146 | static inline void 147 | cleanup_mount_tabp (void *p) 148 | { 149 | void **pp = (void **) p; 150 | 151 | if (*pp) 152 | mount_tab_free ((MountTab)*pp); 153 | } 154 | 155 | #define cleanup_mount_tab __attribute__((cleanup (cleanup_mount_tabp))) 156 | 157 | typedef struct MountInfoLine MountInfoLine; 158 | struct MountInfoLine { 159 | const char *mountpoint; 160 | const char *options; 161 | bool covered; 162 | int id; 163 | int parent_id; 164 | MountInfoLine *first_child; 165 | MountInfoLine *next_sibling; 166 | }; 167 | 168 | static unsigned int 169 | count_lines (const char *data) 170 | { 171 | unsigned int count = 0; 172 | const char *p = data; 173 | 174 | while (*p != 0) 175 | { 176 | if (*p == '\n') 177 | count++; 178 | p++; 179 | } 180 | 181 | /* If missing final newline, add one */ 182 | if (p > data && *(p-1) != '\n') 183 | count++; 184 | 185 | return count; 186 | } 187 | 188 | static int 189 | count_mounts (MountInfoLine *line) 190 | { 191 | MountInfoLine *child; 192 | int res = 0; 193 | 194 | if (!line->covered) 195 | res += 1; 196 | 197 | child = line->first_child; 198 | while (child != NULL) 199 | { 200 | res += count_mounts (child); 201 | child = child->next_sibling; 202 | } 203 | 204 | return res; 205 | } 206 | 207 | static MountInfo * 208 | collect_mounts (MountInfo *info, MountInfoLine *line) 209 | { 210 | MountInfoLine *child; 211 | 212 | if (!line->covered) 213 | { 214 | info->mountpoint = xstrdup (line->mountpoint); 215 | info->options = decode_mountoptions (line->options); 216 | info ++; 217 | } 218 | 219 | child = line->first_child; 220 | while (child != NULL) 221 | { 222 | info = collect_mounts (info, child); 223 | child = child->next_sibling; 224 | } 225 | 226 | return info; 227 | } 228 | 229 | static MountTab 230 | parse_mountinfo (int proc_fd, 231 | const char *root_mount) 232 | { 233 | cleanup_free char *mountinfo = NULL; 234 | cleanup_free MountInfoLine *lines = NULL; 235 | cleanup_free MountInfoLine **by_id = NULL; 236 | cleanup_mount_tab MountTab mount_tab = NULL; 237 | MountInfo *end_tab; 238 | int n_mounts; 239 | char *line; 240 | unsigned int i; 241 | int max_id; 242 | unsigned int n_lines; 243 | int root; 244 | 245 | mountinfo = load_file_at (proc_fd, "self/mountinfo"); 246 | if (mountinfo == NULL) 247 | die_with_error ("Can't open /proc/self/mountinfo"); 248 | 249 | n_lines = count_lines (mountinfo); 250 | lines = xcalloc (n_lines, sizeof (MountInfoLine)); 251 | 252 | max_id = 0; 253 | line = mountinfo; 254 | i = 0; 255 | root = -1; 256 | while (*line != 0) 257 | { 258 | int rc, consumed = 0; 259 | unsigned int maj, min; 260 | char *end; 261 | char *rest; 262 | char *mountpoint; 263 | char *mountpoint_end; 264 | char *options; 265 | char *options_end; 266 | char *next_line; 267 | 268 | assert (i < n_lines); 269 | 270 | end = strchr (line, '\n'); 271 | if (end != NULL) 272 | { 273 | *end = 0; 274 | next_line = end + 1; 275 | } 276 | else 277 | next_line = line + strlen (line); 278 | 279 | rc = sscanf (line, "%d %d %u:%u %n", &lines[i].id, &lines[i].parent_id, &maj, &min, &consumed); 280 | if (rc != 4) 281 | die ("Can't parse mountinfo line"); 282 | rest = line + consumed; 283 | 284 | rest = skip_token (rest, TRUE); /* mountroot */ 285 | mountpoint = rest; 286 | rest = skip_token (rest, FALSE); /* mountpoint */ 287 | mountpoint_end = rest++; 288 | options = rest; 289 | rest = skip_token (rest, FALSE); /* vfs options */ 290 | options_end = rest; 291 | 292 | *mountpoint_end = 0; 293 | lines[i].mountpoint = unescape_inline (mountpoint); 294 | 295 | *options_end = 0; 296 | lines[i].options = options; 297 | 298 | if (lines[i].id > max_id) 299 | max_id = lines[i].id; 300 | if (lines[i].parent_id > max_id) 301 | max_id = lines[i].parent_id; 302 | 303 | if (path_equal (lines[i].mountpoint, root_mount)) 304 | root = i; 305 | 306 | i++; 307 | line = next_line; 308 | } 309 | assert (i == n_lines); 310 | 311 | if (root == -1) 312 | { 313 | mount_tab = xcalloc (1, sizeof (MountInfo)); 314 | return steal_pointer (&mount_tab); 315 | } 316 | 317 | by_id = xcalloc (max_id + 1, sizeof (MountInfoLine*)); 318 | for (i = 0; i < n_lines; i++) 319 | by_id[lines[i].id] = &lines[i]; 320 | 321 | for (i = 0; i < n_lines; i++) 322 | { 323 | MountInfoLine *this = &lines[i]; 324 | MountInfoLine *parent = by_id[this->parent_id]; 325 | MountInfoLine **to_sibling; 326 | MountInfoLine *sibling; 327 | bool covered = FALSE; 328 | 329 | if (!has_path_prefix (this->mountpoint, root_mount)) 330 | continue; 331 | 332 | if (parent == NULL) 333 | continue; 334 | 335 | if (strcmp (parent->mountpoint, this->mountpoint) == 0) 336 | parent->covered = TRUE; 337 | 338 | to_sibling = &parent->first_child; 339 | sibling = parent->first_child; 340 | while (sibling != NULL) 341 | { 342 | /* If this mountpoint is a path prefix of the sibling, 343 | * say this->mp=/foo/bar and sibling->mp=/foo, then it is 344 | * covered by the sibling, and we drop it. */ 345 | if (has_path_prefix (this->mountpoint, sibling->mountpoint)) 346 | { 347 | covered = TRUE; 348 | break; 349 | } 350 | 351 | /* If the sibling is a path prefix of this mount point, 352 | * say this->mp=/foo and sibling->mp=/foo/bar, then the sibling 353 | * is covered, and we drop it. 354 | */ 355 | if (has_path_prefix (sibling->mountpoint, this->mountpoint)) 356 | *to_sibling = sibling->next_sibling; 357 | else 358 | to_sibling = &sibling->next_sibling; 359 | sibling = sibling->next_sibling; 360 | } 361 | 362 | if (covered) 363 | continue; 364 | 365 | *to_sibling = this; 366 | } 367 | 368 | n_mounts = count_mounts (&lines[root]); 369 | mount_tab = xcalloc (n_mounts + 1, sizeof (MountInfo)); 370 | 371 | end_tab = collect_mounts (&mount_tab[0], &lines[root]); 372 | assert (end_tab == &mount_tab[n_mounts]); 373 | 374 | return steal_pointer (&mount_tab); 375 | } 376 | 377 | bind_mount_result 378 | bind_mount (int proc_fd, 379 | const char *src, 380 | const char *dest, 381 | bind_option_t options, 382 | char **failing_path) 383 | { 384 | bool readonly = (options & BIND_READONLY) != 0; 385 | bool devices = (options & BIND_DEVICES) != 0; 386 | bool recursive = (options & BIND_RECURSIVE) != 0; 387 | unsigned long current_flags, new_flags; 388 | cleanup_mount_tab MountTab mount_tab = NULL; 389 | cleanup_free char *resolved_dest = NULL; 390 | cleanup_free char *dest_proc = NULL; 391 | cleanup_free char *oldroot_dest_proc = NULL; 392 | cleanup_free char *kernel_case_combination = NULL; 393 | cleanup_fd int dest_fd = -1; 394 | int i; 395 | 396 | if (src) 397 | { 398 | if (mount (src, dest, NULL, MS_SILENT | MS_BIND | (recursive ? MS_REC : 0), NULL) != 0) 399 | return BIND_MOUNT_ERROR_MOUNT; 400 | } 401 | 402 | /* The mount operation will resolve any symlinks in the destination 403 | path, so to find it in the mount table we need to do that too. */ 404 | resolved_dest = realpath (dest, NULL); 405 | if (resolved_dest == NULL) 406 | return BIND_MOUNT_ERROR_REALPATH_DEST; 407 | 408 | dest_fd = open (resolved_dest, O_PATH | O_CLOEXEC); 409 | if (dest_fd < 0) 410 | { 411 | if (failing_path != NULL) 412 | *failing_path = steal_pointer (&resolved_dest); 413 | 414 | return BIND_MOUNT_ERROR_REOPEN_DEST; 415 | } 416 | 417 | /* If we are in a case-insensitive filesystem, mountinfo might contain a 418 | * different case combination of the path we requested to mount. 419 | * This is due to the fact that the kernel, as of the beginning of 2021, 420 | * populates mountinfo with whatever case combination first appeared in the 421 | * dcache; kernel developers plan to change this in future so that it 422 | * reflects the on-disk encoding instead. 423 | * To avoid throwing an error when this happens, we use readlink() result 424 | * instead of the provided @root_mount, so that we can compare the mountinfo 425 | * entries with the same case combination that the kernel is expected to 426 | * use. */ 427 | dest_proc = xasprintf ("/proc/self/fd/%d", dest_fd); 428 | oldroot_dest_proc = get_oldroot_path (dest_proc); 429 | kernel_case_combination = readlink_malloc (oldroot_dest_proc); 430 | if (kernel_case_combination == NULL) 431 | { 432 | if (failing_path != NULL) 433 | *failing_path = steal_pointer (&resolved_dest); 434 | 435 | return BIND_MOUNT_ERROR_READLINK_DEST_PROC_FD; 436 | } 437 | 438 | mount_tab = parse_mountinfo (proc_fd, kernel_case_combination); 439 | if (mount_tab[0].mountpoint == NULL) 440 | { 441 | if (failing_path != NULL) 442 | *failing_path = steal_pointer (&kernel_case_combination); 443 | 444 | errno = EINVAL; 445 | return BIND_MOUNT_ERROR_FIND_DEST_MOUNT; 446 | } 447 | 448 | assert (path_equal (mount_tab[0].mountpoint, kernel_case_combination)); 449 | current_flags = mount_tab[0].options; 450 | new_flags = current_flags | (devices ? 0 : MS_NODEV) | MS_NOSUID | (readonly ? MS_RDONLY : 0); 451 | if (new_flags != current_flags && 452 | mount ("none", resolved_dest, 453 | NULL, MS_SILENT | MS_BIND | MS_REMOUNT | new_flags, NULL) != 0) 454 | { 455 | if (failing_path != NULL) 456 | *failing_path = steal_pointer (&resolved_dest); 457 | 458 | return BIND_MOUNT_ERROR_REMOUNT_DEST; 459 | } 460 | 461 | /* We need to work around the fact that a bind mount does not apply the flags, so we need to manually 462 | * apply the flags to all submounts in the recursive case. 463 | * Note: This does not apply the flags to mounts which are later propagated into this namespace. 464 | */ 465 | if (recursive) 466 | { 467 | for (i = 1; mount_tab[i].mountpoint != NULL; i++) 468 | { 469 | current_flags = mount_tab[i].options; 470 | new_flags = current_flags | (devices ? 0 : MS_NODEV) | MS_NOSUID | (readonly ? MS_RDONLY : 0); 471 | if (new_flags != current_flags && 472 | mount ("none", mount_tab[i].mountpoint, 473 | NULL, MS_SILENT | MS_BIND | MS_REMOUNT | new_flags, NULL) != 0) 474 | { 475 | /* If we can't read the mountpoint we can't remount it, but that should 476 | be safe to ignore because its not something the user can access. */ 477 | if (errno != EACCES) 478 | { 479 | if (failing_path != NULL) 480 | *failing_path = xstrdup (mount_tab[i].mountpoint); 481 | 482 | return BIND_MOUNT_ERROR_REMOUNT_SUBMOUNT; 483 | } 484 | } 485 | } 486 | } 487 | 488 | return BIND_MOUNT_SUCCESS; 489 | } 490 | 491 | /** 492 | * Return a string representing bind_mount_result, like strerror(). 493 | * If want_errno_p is non-NULL, *want_errno_p is used to indicate whether 494 | * it would make sense to print strerror(saved_errno). 495 | */ 496 | static char * 497 | bind_mount_result_to_string (bind_mount_result res, 498 | const char *failing_path, 499 | bool *want_errno_p) 500 | { 501 | char *string = NULL; 502 | bool want_errno = TRUE; 503 | 504 | switch (res) 505 | { 506 | case BIND_MOUNT_ERROR_MOUNT: 507 | string = xstrdup ("Unable to mount source on destination"); 508 | break; 509 | 510 | case BIND_MOUNT_ERROR_REALPATH_DEST: 511 | string = xstrdup ("realpath(destination)"); 512 | break; 513 | 514 | case BIND_MOUNT_ERROR_REOPEN_DEST: 515 | string = xasprintf ("open(\"%s\", O_PATH)", failing_path); 516 | break; 517 | 518 | case BIND_MOUNT_ERROR_READLINK_DEST_PROC_FD: 519 | string = xasprintf ("readlink(/proc/self/fd/N) for \"%s\"", failing_path); 520 | break; 521 | 522 | case BIND_MOUNT_ERROR_FIND_DEST_MOUNT: 523 | string = xasprintf ("Unable to find \"%s\" in mount table", failing_path); 524 | want_errno = FALSE; 525 | break; 526 | 527 | case BIND_MOUNT_ERROR_REMOUNT_DEST: 528 | string = xasprintf ("Unable to remount destination \"%s\" with correct flags", 529 | failing_path); 530 | break; 531 | 532 | case BIND_MOUNT_ERROR_REMOUNT_SUBMOUNT: 533 | string = xasprintf ("Unable to apply mount flags: remount \"%s\"", 534 | failing_path); 535 | break; 536 | 537 | case BIND_MOUNT_SUCCESS: 538 | string = xstrdup ("Success"); 539 | break; 540 | 541 | default: 542 | string = xstrdup ("(unknown/invalid bind_mount_result)"); 543 | break; 544 | } 545 | 546 | if (want_errno_p != NULL) 547 | *want_errno_p = want_errno; 548 | 549 | return string; 550 | } 551 | 552 | void 553 | die_with_bind_result (bind_mount_result res, 554 | int saved_errno, 555 | const char *failing_path, 556 | const char *format, 557 | ...) 558 | { 559 | va_list args; 560 | bool want_errno = TRUE; 561 | char *message; 562 | 563 | fprintf (stderr, "bwrap: "); 564 | 565 | va_start (args, format); 566 | vfprintf (stderr, format, args); 567 | va_end (args); 568 | 569 | message = bind_mount_result_to_string (res, failing_path, &want_errno); 570 | fprintf (stderr, ": %s", message); 571 | /* message is leaked, but we're exiting unsuccessfully anyway, so ignore */ 572 | 573 | if (want_errno) 574 | { 575 | switch (res) 576 | { 577 | case BIND_MOUNT_ERROR_MOUNT: 578 | case BIND_MOUNT_ERROR_REMOUNT_DEST: 579 | case BIND_MOUNT_ERROR_REMOUNT_SUBMOUNT: 580 | fprintf (stderr, ": %s", mount_strerror (saved_errno)); 581 | break; 582 | 583 | case BIND_MOUNT_ERROR_REALPATH_DEST: 584 | case BIND_MOUNT_ERROR_REOPEN_DEST: 585 | case BIND_MOUNT_ERROR_READLINK_DEST_PROC_FD: 586 | case BIND_MOUNT_ERROR_FIND_DEST_MOUNT: 587 | case BIND_MOUNT_SUCCESS: 588 | default: 589 | fprintf (stderr, ": %s", strerror (saved_errno)); 590 | } 591 | } 592 | 593 | fprintf (stderr, "\n"); 594 | exit (1); 595 | } 596 | -------------------------------------------------------------------------------- /tests/test-seccomp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright 2021 Simon McVittie 3 | # SPDX-License-Identifier: LGPL-2.0-or-later 4 | 5 | import errno 6 | import logging 7 | import os 8 | import subprocess 9 | import sys 10 | import tempfile 11 | import termios 12 | import unittest 13 | 14 | try: 15 | import seccomp 16 | except ImportError: 17 | print('1..0 # SKIP cannot import seccomp Python module') 18 | sys.exit(0) 19 | 20 | 21 | # This is the @default set from systemd as of 2021-10-11 22 | DEFAULT_SET = set(''' 23 | brk 24 | cacheflush 25 | clock_getres 26 | clock_getres_time64 27 | clock_gettime 28 | clock_gettime64 29 | clock_nanosleep 30 | clock_nanosleep_time64 31 | execve 32 | exit 33 | exit_group 34 | futex 35 | futex_time64 36 | get_robust_list 37 | get_thread_area 38 | getegid 39 | getegid32 40 | geteuid 41 | geteuid32 42 | getgid 43 | getgid32 44 | getgroups 45 | getgroups32 46 | getpgid 47 | getpgrp 48 | getpid 49 | getppid 50 | getrandom 51 | getresgid 52 | getresgid32 53 | getresuid 54 | getresuid32 55 | getrlimit 56 | getsid 57 | gettid 58 | gettimeofday 59 | getuid 60 | getuid32 61 | membarrier 62 | mmap 63 | mmap2 64 | munmap 65 | nanosleep 66 | pause 67 | prlimit64 68 | restart_syscall 69 | rseq 70 | rt_sigreturn 71 | sched_getaffinity 72 | sched_yield 73 | set_robust_list 74 | set_thread_area 75 | set_tid_address 76 | set_tls 77 | sigreturn 78 | time 79 | ugetrlimit 80 | '''.split()) 81 | 82 | # This is the @basic-io set from systemd 83 | BASIC_IO_SET = set(''' 84 | _llseek 85 | close 86 | close_range 87 | dup 88 | dup2 89 | dup3 90 | lseek 91 | pread64 92 | preadv 93 | preadv2 94 | pwrite64 95 | pwritev 96 | pwritev2 97 | read 98 | readv 99 | write 100 | writev 101 | '''.split()) 102 | 103 | # This is the @filesystem-io set from systemd 104 | FILESYSTEM_SET = set(''' 105 | access 106 | chdir 107 | chmod 108 | close 109 | creat 110 | faccessat 111 | faccessat2 112 | fallocate 113 | fchdir 114 | fchmod 115 | fchmodat 116 | fcntl 117 | fcntl64 118 | fgetxattr 119 | flistxattr 120 | fremovexattr 121 | fsetxattr 122 | fstat 123 | fstat64 124 | fstatat64 125 | fstatfs 126 | fstatfs64 127 | ftruncate 128 | ftruncate64 129 | futimesat 130 | getcwd 131 | getdents 132 | getdents64 133 | getxattr 134 | inotify_add_watch 135 | inotify_init 136 | inotify_init1 137 | inotify_rm_watch 138 | lgetxattr 139 | link 140 | linkat 141 | listxattr 142 | llistxattr 143 | lremovexattr 144 | lsetxattr 145 | lstat 146 | lstat64 147 | mkdir 148 | mkdirat 149 | mknod 150 | mknodat 151 | newfstatat 152 | oldfstat 153 | oldlstat 154 | oldstat 155 | open 156 | openat 157 | openat2 158 | readlink 159 | readlinkat 160 | removexattr 161 | rename 162 | renameat 163 | renameat2 164 | rmdir 165 | setxattr 166 | stat 167 | stat64 168 | statfs 169 | statfs64 170 | statx 171 | symlink 172 | symlinkat 173 | truncate 174 | truncate64 175 | unlink 176 | unlinkat 177 | utime 178 | utimensat 179 | utimensat_time64 180 | utimes 181 | '''.split()) 182 | 183 | # Miscellaneous syscalls used during process startup, at least on x86_64 184 | ALLOWED = DEFAULT_SET | BASIC_IO_SET | FILESYSTEM_SET | set(''' 185 | arch_prctl 186 | ioctl 187 | madvise 188 | mprotect 189 | mremap 190 | prctl 191 | readdir 192 | umask 193 | '''.split()) 194 | 195 | # Syscalls we will try to use, expecting them to be either allowed or 196 | # blocked by our allow and/or deny lists 197 | TRY_SYSCALLS = [ 198 | 'chmod', 199 | 'chroot', 200 | 'clone3', 201 | 'ioctl TIOCNOTTY', 202 | 'ioctl TIOCSTI CVE-2019-10063', 203 | 'ioctl TIOCSTI', 204 | 'listen', 205 | 'prctl', 206 | ] 207 | 208 | 209 | class Test(unittest.TestCase): 210 | def setUp(self) -> None: 211 | here = os.path.dirname(os.path.abspath(__file__)) 212 | 213 | if 'G_TEST_SRCDIR' in os.environ: 214 | self.test_srcdir = os.getenv('G_TEST_SRCDIR') + '/tests' 215 | else: 216 | self.test_srcdir = here 217 | 218 | if 'G_TEST_BUILDDIR' in os.environ: 219 | self.test_builddir = os.getenv('G_TEST_BUILDDIR') + '/tests' 220 | else: 221 | self.test_builddir = here 222 | 223 | self.bwrap = os.getenv('BWRAP', 'bwrap') 224 | self.try_syscall = os.path.join(self.test_builddir, 'try-syscall') 225 | 226 | completed = subprocess.run( 227 | [ 228 | self.bwrap, 229 | '--ro-bind', '/', '/', 230 | 'true', 231 | ], 232 | stdin=subprocess.DEVNULL, 233 | stdout=subprocess.DEVNULL, 234 | stderr=2, 235 | ) 236 | 237 | if completed.returncode != 0: 238 | raise unittest.SkipTest( 239 | 'cannot run bwrap (does it need to be setuid?)' 240 | ) 241 | 242 | def tearDown(self) -> None: 243 | pass 244 | 245 | def test_no_seccomp(self) -> None: 246 | for syscall in TRY_SYSCALLS: 247 | print('# {} without seccomp'.format(syscall)) 248 | completed = subprocess.run( 249 | [ 250 | self.bwrap, 251 | '--ro-bind', '/', '/', 252 | self.try_syscall, syscall, 253 | ], 254 | stdin=subprocess.DEVNULL, 255 | stdout=subprocess.DEVNULL, 256 | stderr=2, 257 | ) 258 | 259 | if ( 260 | syscall == 'ioctl TIOCSTI CVE-2019-10063' 261 | and completed.returncode == errno.ENOENT 262 | ): 263 | print('# Cannot test 64-bit syscall parameter on 32-bit') 264 | continue 265 | 266 | if syscall == 'clone3': 267 | # If the kernel supports it, we didn't block it so 268 | # it fails with EFAULT. If the kernel doesn't support it, 269 | # it'll fail with ENOSYS instead. 270 | self.assertIn( 271 | completed.returncode, 272 | (errno.ENOSYS, errno.EFAULT), 273 | ) 274 | elif syscall.startswith('ioctl') or syscall == 'listen': 275 | self.assertEqual(completed.returncode, errno.EBADF) 276 | else: 277 | self.assertEqual(completed.returncode, errno.EFAULT) 278 | 279 | def test_seccomp_allowlist(self) -> None: 280 | with tempfile.TemporaryFile() as allowlist_temp: 281 | allowlist = seccomp.SyscallFilter(seccomp.ERRNO(errno.ENOSYS)) 282 | 283 | if os.uname().machine == 'x86_64': 284 | # Allow Python and try-syscall to be different word sizes 285 | allowlist.add_arch(seccomp.Arch.X86) 286 | 287 | for syscall in ALLOWED: 288 | try: 289 | allowlist.add_rule(seccomp.ALLOW, syscall) 290 | except Exception as e: 291 | print('# Cannot add {} to allowlist: {!r}'.format(syscall, e)) 292 | 293 | allowlist.export_bpf(allowlist_temp) 294 | 295 | for syscall in TRY_SYSCALLS: 296 | print('# allowlist vs. {}'.format(syscall)) 297 | allowlist_temp.seek(0, os.SEEK_SET) 298 | 299 | completed = subprocess.run( 300 | [ 301 | self.bwrap, 302 | '--ro-bind', '/', '/', 303 | '--seccomp', str(allowlist_temp.fileno()), 304 | self.try_syscall, syscall, 305 | ], 306 | pass_fds=(allowlist_temp.fileno(),), 307 | stdin=subprocess.DEVNULL, 308 | stdout=subprocess.DEVNULL, 309 | stderr=2, 310 | ) 311 | 312 | if ( 313 | syscall == 'ioctl TIOCSTI CVE-2019-10063' 314 | and completed.returncode == errno.ENOENT 315 | ): 316 | print('# Cannot test 64-bit syscall parameter on 32-bit') 317 | continue 318 | 319 | if syscall.startswith('ioctl'): 320 | # We allow this, so it is executed (and in this simple 321 | # example, immediately fails) 322 | self.assertEqual(completed.returncode, errno.EBADF) 323 | elif syscall in ('chroot', 'listen', 'clone3'): 324 | # We don't allow these, so they fail with ENOSYS. 325 | # clone3 might also be failing with ENOSYS because 326 | # the kernel genuinely doesn't support it. 327 | self.assertEqual(completed.returncode, errno.ENOSYS) 328 | else: 329 | # We allow this, so it is executed (and in this simple 330 | # example, immediately fails) 331 | self.assertEqual(completed.returncode, errno.EFAULT) 332 | 333 | def test_seccomp_denylist(self) -> None: 334 | with tempfile.TemporaryFile() as denylist_temp: 335 | denylist = seccomp.SyscallFilter(seccomp.ALLOW) 336 | 337 | if os.uname().machine == 'x86_64': 338 | # Allow Python and try-syscall to be different word sizes 339 | denylist.add_arch(seccomp.Arch.X86) 340 | 341 | # Using ECONNREFUSED here because it's unlikely that any of 342 | # these syscalls will legitimately fail with that code, so 343 | # if they fail like this, it will be as a result of seccomp. 344 | denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'chmod') 345 | denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'chroot') 346 | denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'prctl') 347 | denylist.add_rule( 348 | seccomp.ERRNO(errno.ECONNREFUSED), 'ioctl', 349 | seccomp.Arg(1, seccomp.MASKED_EQ, 0xffffffff, termios.TIOCSTI), 350 | ) 351 | 352 | denylist.export_bpf(denylist_temp) 353 | 354 | for syscall in TRY_SYSCALLS: 355 | print('# denylist vs. {}'.format(syscall)) 356 | denylist_temp.seek(0, os.SEEK_SET) 357 | 358 | completed = subprocess.run( 359 | [ 360 | self.bwrap, 361 | '--ro-bind', '/', '/', 362 | '--seccomp', str(denylist_temp.fileno()), 363 | self.try_syscall, syscall, 364 | ], 365 | pass_fds=(denylist_temp.fileno(),), 366 | stdin=subprocess.DEVNULL, 367 | stdout=subprocess.DEVNULL, 368 | stderr=2, 369 | ) 370 | 371 | if ( 372 | syscall == 'ioctl TIOCSTI CVE-2019-10063' 373 | and completed.returncode == errno.ENOENT 374 | ): 375 | print('# Cannot test 64-bit syscall parameter on 32-bit') 376 | continue 377 | 378 | if syscall == 'clone3': 379 | # If the kernel supports it, we didn't block it so 380 | # it fails with EFAULT. If the kernel doesn't support it, 381 | # it'll fail with ENOSYS instead. 382 | self.assertIn( 383 | completed.returncode, 384 | (errno.ENOSYS, errno.EFAULT), 385 | ) 386 | elif syscall in ('ioctl TIOCNOTTY', 'listen'): 387 | # Not on the denylist 388 | self.assertEqual(completed.returncode, errno.EBADF) 389 | else: 390 | # We blocked all of these 391 | self.assertEqual(completed.returncode, errno.ECONNREFUSED) 392 | 393 | def test_seccomp_stacked(self, allowlist_first=False) -> None: 394 | with tempfile.TemporaryFile( 395 | ) as allowlist_temp, tempfile.TemporaryFile( 396 | ) as denylist_temp: 397 | # This filter is a simplified version of what Flatpak wants 398 | 399 | allowlist = seccomp.SyscallFilter(seccomp.ERRNO(errno.ENOSYS)) 400 | denylist = seccomp.SyscallFilter(seccomp.ALLOW) 401 | 402 | if os.uname().machine == 'x86_64': 403 | # Allow Python and try-syscall to be different word sizes 404 | allowlist.add_arch(seccomp.Arch.X86) 405 | denylist.add_arch(seccomp.Arch.X86) 406 | 407 | for syscall in ALLOWED: 408 | try: 409 | allowlist.add_rule(seccomp.ALLOW, syscall) 410 | except Exception as e: 411 | print('# Cannot add {} to allowlist: {!r}'.format(syscall, e)) 412 | 413 | denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'chmod') 414 | denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'chroot') 415 | denylist.add_rule( 416 | seccomp.ERRNO(errno.ECONNREFUSED), 'ioctl', 417 | seccomp.Arg(1, seccomp.MASKED_EQ, 0xffffffff, termios.TIOCSTI), 418 | ) 419 | 420 | # All seccomp programs except the last must allow prctl(), 421 | # because otherwise we wouldn't be able to add the remaining 422 | # seccomp programs. We document that the last program can 423 | # block prctl, so test that. 424 | if allowlist_first: 425 | denylist.add_rule(seccomp.ERRNO(errno.ECONNREFUSED), 'prctl') 426 | 427 | allowlist.export_bpf(allowlist_temp) 428 | denylist.export_bpf(denylist_temp) 429 | 430 | for syscall in TRY_SYSCALLS: 431 | print('# stacked vs. {}'.format(syscall)) 432 | allowlist_temp.seek(0, os.SEEK_SET) 433 | denylist_temp.seek(0, os.SEEK_SET) 434 | 435 | if allowlist_first: 436 | fds = [allowlist_temp.fileno(), denylist_temp.fileno()] 437 | else: 438 | fds = [denylist_temp.fileno(), allowlist_temp.fileno()] 439 | 440 | completed = subprocess.run( 441 | [ 442 | self.bwrap, 443 | '--ro-bind', '/', '/', 444 | '--add-seccomp-fd', str(fds[0]), 445 | '--add-seccomp-fd', str(fds[1]), 446 | self.try_syscall, syscall, 447 | ], 448 | pass_fds=fds, 449 | stdin=subprocess.DEVNULL, 450 | stdout=subprocess.DEVNULL, 451 | stderr=2, 452 | ) 453 | 454 | if ( 455 | syscall == 'ioctl TIOCSTI CVE-2019-10063' 456 | and completed.returncode == errno.ENOENT 457 | ): 458 | print('# Cannot test 64-bit syscall parameter on 32-bit') 459 | continue 460 | 461 | if syscall == 'ioctl TIOCNOTTY': 462 | # Not denied by the denylist, and allowed by the allowlist 463 | self.assertEqual(completed.returncode, errno.EBADF) 464 | elif syscall in ('clone3', 'listen'): 465 | # We didn't deny these, so the denylist has no effect 466 | # and we fall back to the allowlist, which doesn't 467 | # include them either. 468 | # clone3 might also be failing with ENOSYS because 469 | # the kernel genuinely doesn't support it. 470 | self.assertEqual(completed.returncode, errno.ENOSYS) 471 | elif syscall == 'chroot': 472 | # This is denied by the denylist *and* not allowed by 473 | # the allowlist. The result depends which one we added 474 | # first: the most-recently-added filter "wins". 475 | if allowlist_first: 476 | self.assertEqual( 477 | completed.returncode, 478 | errno.ECONNREFUSED, 479 | ) 480 | else: 481 | self.assertEqual(completed.returncode, errno.ENOSYS) 482 | elif syscall == 'prctl': 483 | # We can only put this on the denylist if the denylist 484 | # is the last to be added. 485 | if allowlist_first: 486 | self.assertEqual( 487 | completed.returncode, 488 | errno.ECONNREFUSED, 489 | ) 490 | else: 491 | self.assertEqual(completed.returncode, errno.EFAULT) 492 | else: 493 | # chmod is allowed by the allowlist but blocked by the 494 | # denylist. Denying takes precedence over allowing, 495 | # regardless of order. 496 | self.assertEqual(completed.returncode, errno.ECONNREFUSED) 497 | 498 | def test_seccomp_stacked_allowlist_first(self) -> None: 499 | self.test_seccomp_stacked(allowlist_first=True) 500 | 501 | def test_seccomp_invalid(self) -> None: 502 | with tempfile.TemporaryFile( 503 | ) as allowlist_temp, tempfile.TemporaryFile( 504 | ) as denylist_temp: 505 | completed = subprocess.run( 506 | [ 507 | self.bwrap, 508 | '--ro-bind', '/', '/', 509 | '--add-seccomp-fd', '-1', 510 | 'true', 511 | ], 512 | stdin=subprocess.DEVNULL, 513 | stdout=subprocess.DEVNULL, 514 | stderr=subprocess.PIPE, 515 | ) 516 | self.assertIn(b'bwrap: Invalid fd: -1\n', completed.stderr) 517 | self.assertEqual(completed.returncode, 1) 518 | 519 | completed = subprocess.run( 520 | [ 521 | self.bwrap, 522 | '--ro-bind', '/', '/', 523 | '--seccomp', '0a', 524 | 'true', 525 | ], 526 | stdin=subprocess.DEVNULL, 527 | stdout=subprocess.DEVNULL, 528 | stderr=subprocess.PIPE, 529 | ) 530 | self.assertIn(b'bwrap: Invalid fd: 0a\n', completed.stderr) 531 | self.assertEqual(completed.returncode, 1) 532 | 533 | completed = subprocess.run( 534 | [ 535 | self.bwrap, 536 | '--ro-bind', '/', '/', 537 | '--add-seccomp-fd', str(denylist_temp.fileno()), 538 | '--seccomp', str(allowlist_temp.fileno()), 539 | 'true', 540 | ], 541 | pass_fds=(allowlist_temp.fileno(), denylist_temp.fileno()), 542 | stdin=subprocess.DEVNULL, 543 | stdout=subprocess.DEVNULL, 544 | stderr=subprocess.PIPE, 545 | ) 546 | self.assertIn( 547 | b'bwrap: --seccomp cannot be combined with --add-seccomp-fd\n', 548 | completed.stderr, 549 | ) 550 | self.assertEqual(completed.returncode, 1) 551 | 552 | completed = subprocess.run( 553 | [ 554 | self.bwrap, 555 | '--ro-bind', '/', '/', 556 | '--seccomp', str(allowlist_temp.fileno()), 557 | '--add-seccomp-fd', str(denylist_temp.fileno()), 558 | 'true', 559 | ], 560 | pass_fds=(allowlist_temp.fileno(), denylist_temp.fileno()), 561 | stdin=subprocess.DEVNULL, 562 | stdout=subprocess.DEVNULL, 563 | stderr=subprocess.PIPE, 564 | ) 565 | self.assertIn( 566 | b'--add-seccomp-fd cannot be combined with --seccomp', 567 | completed.stderr, 568 | ) 569 | self.assertEqual(completed.returncode, 1) 570 | 571 | completed = subprocess.run( 572 | [ 573 | self.bwrap, 574 | '--ro-bind', '/', '/', 575 | '--add-seccomp-fd', str(allowlist_temp.fileno()), 576 | '--add-seccomp-fd', str(allowlist_temp.fileno()), 577 | 'true', 578 | ], 579 | pass_fds=(allowlist_temp.fileno(), allowlist_temp.fileno()), 580 | stdin=subprocess.DEVNULL, 581 | stdout=subprocess.DEVNULL, 582 | stderr=subprocess.PIPE, 583 | ) 584 | self.assertIn( 585 | b"bwrap: Can't read seccomp data: ", 586 | completed.stderr, 587 | ) 588 | self.assertEqual(completed.returncode, 1) 589 | 590 | allowlist_temp.write(b'\x01') 591 | allowlist_temp.seek(0, os.SEEK_SET) 592 | completed = subprocess.run( 593 | [ 594 | self.bwrap, 595 | '--ro-bind', '/', '/', 596 | '--add-seccomp-fd', str(denylist_temp.fileno()), 597 | '--add-seccomp-fd', str(allowlist_temp.fileno()), 598 | 'true', 599 | ], 600 | pass_fds=(allowlist_temp.fileno(), denylist_temp.fileno()), 601 | stdin=subprocess.DEVNULL, 602 | stdout=subprocess.DEVNULL, 603 | stderr=subprocess.PIPE, 604 | ) 605 | self.assertIn( 606 | b'bwrap: Invalid seccomp data, must be multiple of 8\n', 607 | completed.stderr, 608 | ) 609 | self.assertEqual(completed.returncode, 1) 610 | 611 | 612 | def main(): 613 | logging.basicConfig(level=logging.DEBUG) 614 | 615 | try: 616 | from tap.runner import TAPTestRunner 617 | except ImportError: 618 | TAPTestRunner = None # type: ignore 619 | 620 | if TAPTestRunner is not None: 621 | runner = TAPTestRunner() 622 | runner.set_stream(True) 623 | unittest.main(testRunner=runner) 624 | else: 625 | print('# tap.runner not available, using simple TAP output') 626 | print('1..1') 627 | program = unittest.main(exit=False) 628 | if program.result.wasSuccessful(): 629 | print('ok 1 - %r' % program.result) 630 | else: 631 | print('not ok 1 - %r' % program.result) 632 | sys.exit(1) 633 | 634 | if __name__ == '__main__': 635 | main() 636 | -------------------------------------------------------------------------------- /utils.c: -------------------------------------------------------------------------------- 1 | /* bubblewrap 2 | * Copyright (C) 2016 Alexander Larsson 3 | * SPDX-License-Identifier: LGPL-2.0-or-later 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 2 of the License, or (at your option) any later version. 9 | * 10 | * This library is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this library. If not, see . 17 | * 18 | */ 19 | #include "config.h" 20 | 21 | #include "utils.h" 22 | #include 23 | #include 24 | #include 25 | #include 26 | #ifdef HAVE_SELINUX 27 | #include 28 | #endif 29 | 30 | #ifndef HAVE_SELINUX_2_3 31 | /* libselinux older than 2.3 weren't const-correct */ 32 | #define setexeccon(x) setexeccon ((security_context_t) x) 33 | #define setfscreatecon(x) setfscreatecon ((security_context_t) x) 34 | #define security_check_context(x) security_check_context ((security_context_t) x) 35 | #endif 36 | 37 | __attribute__((format(printf, 1, 0))) static void 38 | warnv (const char *format, 39 | va_list args, 40 | const char *detail) 41 | { 42 | fprintf (stderr, "bwrap: "); 43 | vfprintf (stderr, format, args); 44 | 45 | if (detail != NULL) 46 | fprintf (stderr, ": %s", detail); 47 | 48 | fprintf (stderr, "\n"); 49 | } 50 | 51 | void 52 | warn (const char *format, ...) 53 | { 54 | va_list args; 55 | 56 | va_start (args, format); 57 | warnv (format, args, NULL); 58 | va_end (args); 59 | } 60 | 61 | void 62 | die_with_error (const char *format, ...) 63 | { 64 | va_list args; 65 | int errsv; 66 | 67 | errsv = errno; 68 | 69 | va_start (args, format); 70 | warnv (format, args, strerror (errsv)); 71 | va_end (args); 72 | 73 | exit (1); 74 | } 75 | 76 | void 77 | die_with_mount_error (const char *format, ...) 78 | { 79 | va_list args; 80 | int errsv; 81 | 82 | errsv = errno; 83 | 84 | va_start (args, format); 85 | warnv (format, args, mount_strerror (errsv)); 86 | va_end (args); 87 | 88 | exit (1); 89 | } 90 | 91 | void 92 | die (const char *format, ...) 93 | { 94 | va_list args; 95 | 96 | va_start (args, format); 97 | warnv (format, args, NULL); 98 | va_end (args); 99 | 100 | exit (1); 101 | } 102 | 103 | void 104 | die_unless_label_valid (UNUSED const char *label) 105 | { 106 | #ifdef HAVE_SELINUX 107 | if (is_selinux_enabled () == 1) 108 | { 109 | if (security_check_context (label) < 0) 110 | die_with_error ("invalid label %s", label); 111 | return; 112 | } 113 | #endif 114 | die ("labeling not supported on this system"); 115 | } 116 | 117 | void 118 | die_oom (void) 119 | { 120 | fputs ("Out of memory\n", stderr); 121 | exit (1); 122 | } 123 | 124 | /* Fork, return in child, exiting the previous parent */ 125 | void 126 | fork_intermediate_child (void) 127 | { 128 | int pid = fork (); 129 | if (pid == -1) 130 | die_with_error ("Can't fork for --pidns"); 131 | 132 | /* Parent is an process not needed */ 133 | if (pid != 0) 134 | exit (0); 135 | } 136 | 137 | void * 138 | xmalloc (size_t size) 139 | { 140 | void *res = malloc (size); 141 | 142 | if (res == NULL) 143 | die_oom (); 144 | return res; 145 | } 146 | 147 | void * 148 | xcalloc (size_t nmemb, size_t size) 149 | { 150 | void *res = calloc (nmemb, size); 151 | 152 | if (res == NULL) 153 | die_oom (); 154 | return res; 155 | } 156 | 157 | void * 158 | xrealloc (void *ptr, size_t size) 159 | { 160 | void *res; 161 | 162 | assert (size != 0); 163 | 164 | res = realloc (ptr, size); 165 | 166 | if (res == NULL) 167 | die_oom (); 168 | return res; 169 | } 170 | 171 | char * 172 | xstrdup (const char *str) 173 | { 174 | char *res; 175 | 176 | assert (str != NULL); 177 | 178 | res = strdup (str); 179 | if (res == NULL) 180 | die_oom (); 181 | 182 | return res; 183 | } 184 | 185 | void 186 | strfreev (char **str_array) 187 | { 188 | if (str_array) 189 | { 190 | int i; 191 | 192 | for (i = 0; str_array[i] != NULL; i++) 193 | free (str_array[i]); 194 | 195 | free (str_array); 196 | } 197 | } 198 | 199 | /* Compares if str has a specific path prefix. This differs 200 | from a regular prefix in two ways. First of all there may 201 | be multiple slashes separating the path elements, and 202 | secondly, if a prefix is matched that has to be en entire 203 | path element. For instance /a/prefix matches /a/prefix/foo/bar, 204 | but not /a/prefixfoo/bar. */ 205 | bool 206 | has_path_prefix (const char *str, 207 | const char *prefix) 208 | { 209 | while (TRUE) 210 | { 211 | /* Skip consecutive slashes to reach next path 212 | element */ 213 | while (*str == '/') 214 | str++; 215 | while (*prefix == '/') 216 | prefix++; 217 | 218 | /* No more prefix path elements? Done! */ 219 | if (*prefix == 0) 220 | return TRUE; 221 | 222 | /* Compare path element */ 223 | while (*prefix != 0 && *prefix != '/') 224 | { 225 | if (*str != *prefix) 226 | return FALSE; 227 | str++; 228 | prefix++; 229 | } 230 | 231 | /* Matched prefix path element, 232 | must be entire str path element */ 233 | if (*str != '/' && *str != 0) 234 | return FALSE; 235 | } 236 | } 237 | 238 | bool 239 | path_equal (const char *path1, 240 | const char *path2) 241 | { 242 | while (TRUE) 243 | { 244 | /* Skip consecutive slashes to reach next path 245 | element */ 246 | while (*path1 == '/') 247 | path1++; 248 | while (*path2 == '/') 249 | path2++; 250 | 251 | /* No more prefix path elements? Done! */ 252 | if (*path1 == 0 || *path2 == 0) 253 | return *path1 == 0 && *path2 == 0; 254 | 255 | /* Compare path element */ 256 | while (*path1 != 0 && *path1 != '/') 257 | { 258 | if (*path1 != *path2) 259 | return FALSE; 260 | path1++; 261 | path2++; 262 | } 263 | 264 | /* Matched path1 path element, must be entire path element */ 265 | if (*path2 != '/' && *path2 != 0) 266 | return FALSE; 267 | } 268 | } 269 | 270 | 271 | bool 272 | has_prefix (const char *str, 273 | const char *prefix) 274 | { 275 | return strncmp (str, prefix, strlen (prefix)) == 0; 276 | } 277 | 278 | void 279 | xclearenv (void) 280 | { 281 | if (clearenv () != 0) 282 | die_with_error ("clearenv failed"); 283 | } 284 | 285 | void 286 | xsetenv (const char *name, const char *value, int overwrite) 287 | { 288 | if (setenv (name, value, overwrite)) 289 | die ("setenv failed"); 290 | } 291 | 292 | void 293 | xunsetenv (const char *name) 294 | { 295 | if (unsetenv (name)) 296 | die ("unsetenv failed"); 297 | } 298 | 299 | char * 300 | strconcat (const char *s1, 301 | const char *s2) 302 | { 303 | size_t len = 0; 304 | char *res; 305 | 306 | if (s1) 307 | len += strlen (s1); 308 | if (s2) 309 | len += strlen (s2); 310 | 311 | res = xmalloc (len + 1); 312 | *res = 0; 313 | if (s1) 314 | strcat (res, s1); 315 | if (s2) 316 | strcat (res, s2); 317 | 318 | return res; 319 | } 320 | 321 | char * 322 | strconcat3 (const char *s1, 323 | const char *s2, 324 | const char *s3) 325 | { 326 | size_t len = 0; 327 | char *res; 328 | 329 | if (s1) 330 | len += strlen (s1); 331 | if (s2) 332 | len += strlen (s2); 333 | if (s3) 334 | len += strlen (s3); 335 | 336 | res = xmalloc (len + 1); 337 | *res = 0; 338 | if (s1) 339 | strcat (res, s1); 340 | if (s2) 341 | strcat (res, s2); 342 | if (s3) 343 | strcat (res, s3); 344 | 345 | return res; 346 | } 347 | 348 | char * 349 | xasprintf (const char *format, 350 | ...) 351 | { 352 | char *buffer = NULL; 353 | va_list args; 354 | 355 | va_start (args, format); 356 | if (vasprintf (&buffer, format, args) == -1) 357 | die_oom (); 358 | va_end (args); 359 | 360 | return buffer; 361 | } 362 | 363 | int 364 | fdwalk (int proc_fd, int (*cb)(void *data, 365 | int fd), void *data) 366 | { 367 | int open_max; 368 | int fd; 369 | int dfd; 370 | int res = 0; 371 | DIR *d; 372 | 373 | dfd = openat (proc_fd, "self/fd", O_DIRECTORY | O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY); 374 | if (dfd == -1) 375 | return res; 376 | 377 | if ((d = fdopendir (dfd))) 378 | { 379 | struct dirent *de; 380 | 381 | while ((de = readdir (d))) 382 | { 383 | long l; 384 | char *e = NULL; 385 | 386 | if (de->d_name[0] == '.') 387 | continue; 388 | 389 | errno = 0; 390 | l = strtol (de->d_name, &e, 10); 391 | if (errno != 0 || !e || *e) 392 | continue; 393 | 394 | fd = (int) l; 395 | 396 | if ((long) fd != l) 397 | continue; 398 | 399 | if (fd == dirfd (d)) 400 | continue; 401 | 402 | if ((res = cb (data, fd)) != 0) 403 | break; 404 | } 405 | 406 | closedir (d); 407 | return res; 408 | } 409 | 410 | open_max = sysconf (_SC_OPEN_MAX); 411 | 412 | for (fd = 0; fd < open_max; fd++) 413 | if ((res = cb (data, fd)) != 0) 414 | break; 415 | 416 | return res; 417 | } 418 | 419 | /* Sets errno on error (!= 0), ENOSPC on short write */ 420 | int 421 | write_to_fd (int fd, 422 | const char *content, 423 | ssize_t len) 424 | { 425 | ssize_t res; 426 | 427 | while (len > 0) 428 | { 429 | res = write (fd, content, len); 430 | if (res < 0 && errno == EINTR) 431 | continue; 432 | if (res <= 0) 433 | { 434 | if (res == 0) /* Unexpected short write, should not happen when writing to a file */ 435 | errno = ENOSPC; 436 | return -1; 437 | } 438 | len -= res; 439 | content += res; 440 | } 441 | 442 | return 0; 443 | } 444 | 445 | /* Sets errno on error (!= 0), ENOSPC on short write */ 446 | int 447 | write_file_at (int dirfd, 448 | const char *path, 449 | const char *content) 450 | { 451 | int fd; 452 | bool res; 453 | int errsv; 454 | 455 | fd = openat (dirfd, path, O_RDWR | O_CLOEXEC, 0); 456 | if (fd == -1) 457 | return -1; 458 | 459 | res = 0; 460 | if (content) 461 | res = write_to_fd (fd, content, strlen (content)); 462 | 463 | errsv = errno; 464 | close (fd); 465 | errno = errsv; 466 | 467 | return res; 468 | } 469 | 470 | /* Sets errno on error (!= 0), ENOSPC on short write */ 471 | int 472 | create_file (const char *path, 473 | mode_t mode, 474 | const char *content) 475 | { 476 | int fd; 477 | int res; 478 | int errsv; 479 | 480 | fd = creat (path, mode); 481 | if (fd == -1) 482 | return -1; 483 | 484 | res = 0; 485 | if (content) 486 | res = write_to_fd (fd, content, strlen (content)); 487 | 488 | errsv = errno; 489 | close (fd); 490 | errno = errsv; 491 | 492 | return res; 493 | } 494 | 495 | int 496 | ensure_file (const char *path, 497 | mode_t mode) 498 | { 499 | struct stat buf; 500 | 501 | /* We check this ahead of time, otherwise 502 | the create file will fail in the read-only 503 | case with EROFS instead of EEXIST. 504 | 505 | We're trying to set up a mount point for a non-directory, so any 506 | non-directory, non-symlink is acceptable - it doesn't necessarily 507 | have to be a regular file. */ 508 | if (stat (path, &buf) == 0 && 509 | !S_ISDIR (buf.st_mode) && 510 | !S_ISLNK (buf.st_mode)) 511 | return 0; 512 | 513 | if (create_file (path, mode, NULL) != 0 && errno != EEXIST) 514 | return -1; 515 | 516 | return 0; 517 | } 518 | 519 | 520 | #define BUFSIZE 8192 521 | /* Sets errno on error (!= 0), ENOSPC on short write */ 522 | int 523 | copy_file_data (int sfd, 524 | int dfd) 525 | { 526 | char buffer[BUFSIZE]; 527 | ssize_t bytes_read; 528 | 529 | while (TRUE) 530 | { 531 | bytes_read = read (sfd, buffer, BUFSIZE); 532 | if (bytes_read == -1) 533 | { 534 | if (errno == EINTR) 535 | continue; 536 | 537 | return -1; 538 | } 539 | 540 | if (bytes_read == 0) 541 | break; 542 | 543 | if (write_to_fd (dfd, buffer, bytes_read) != 0) 544 | return -1; 545 | } 546 | 547 | return 0; 548 | } 549 | 550 | /* Sets errno on error (!= 0), ENOSPC on short write */ 551 | int 552 | copy_file (const char *src_path, 553 | const char *dst_path, 554 | mode_t mode) 555 | { 556 | int sfd; 557 | int dfd; 558 | int res; 559 | int errsv; 560 | 561 | sfd = open (src_path, O_CLOEXEC | O_RDONLY); 562 | if (sfd == -1) 563 | return -1; 564 | 565 | dfd = creat (dst_path, mode); 566 | if (dfd == -1) 567 | { 568 | errsv = errno; 569 | close (sfd); 570 | errno = errsv; 571 | return -1; 572 | } 573 | 574 | res = copy_file_data (sfd, dfd); 575 | 576 | errsv = errno; 577 | close (sfd); 578 | close (dfd); 579 | errno = errsv; 580 | 581 | return res; 582 | } 583 | 584 | /* Sets errno on error (== NULL), 585 | * Always ensures terminating zero */ 586 | char * 587 | load_file_data (int fd, 588 | size_t *size) 589 | { 590 | cleanup_free char *data = NULL; 591 | ssize_t data_read; 592 | ssize_t data_len; 593 | ssize_t res; 594 | 595 | data_read = 0; 596 | data_len = 4080; 597 | data = xmalloc (data_len); 598 | 599 | do 600 | { 601 | if (data_len == data_read + 1) 602 | { 603 | if (data_len > SSIZE_MAX / 2) 604 | { 605 | errno = EFBIG; 606 | return NULL; 607 | } 608 | 609 | data_len *= 2; 610 | data = xrealloc (data, data_len); 611 | } 612 | 613 | do 614 | res = read (fd, data + data_read, data_len - data_read - 1); 615 | while (res < 0 && errno == EINTR); 616 | 617 | if (res < 0) 618 | return NULL; 619 | 620 | data_read += res; 621 | } 622 | while (res > 0); 623 | 624 | data[data_read] = 0; 625 | 626 | if (size) 627 | *size = (size_t) data_read; 628 | 629 | return steal_pointer (&data); 630 | } 631 | 632 | /* Sets errno on error (== NULL), 633 | * Always ensures terminating zero */ 634 | char * 635 | load_file_at (int dirfd, 636 | const char *path) 637 | { 638 | int fd; 639 | char *data; 640 | int errsv; 641 | 642 | fd = openat (dirfd, path, O_CLOEXEC | O_RDONLY); 643 | if (fd == -1) 644 | return NULL; 645 | 646 | data = load_file_data (fd, NULL); 647 | 648 | errsv = errno; 649 | close (fd); 650 | errno = errsv; 651 | 652 | return data; 653 | } 654 | 655 | /* Sets errno on error (< 0) */ 656 | int 657 | get_file_mode (const char *pathname) 658 | { 659 | struct stat buf; 660 | 661 | if (stat (pathname, &buf) != 0) 662 | return -1; 663 | 664 | return buf.st_mode & S_IFMT; 665 | } 666 | 667 | int 668 | ensure_dir (const char *path, 669 | mode_t mode) 670 | { 671 | struct stat buf; 672 | 673 | /* We check this ahead of time, otherwise 674 | the mkdir call can fail in the read-only 675 | case with EROFS instead of EEXIST on some 676 | filesystems (such as NFS) */ 677 | if (stat (path, &buf) == 0) 678 | { 679 | if (!S_ISDIR (buf.st_mode)) 680 | { 681 | errno = ENOTDIR; 682 | return -1; 683 | } 684 | 685 | return 0; 686 | } 687 | 688 | if (mkdir (path, mode) == -1 && errno != EEXIST) 689 | return -1; 690 | 691 | return 0; 692 | } 693 | 694 | 695 | /* Sets errno on error (!= 0) */ 696 | int 697 | mkdir_with_parents (const char *pathname, 698 | mode_t mode, 699 | bool create_last) 700 | { 701 | cleanup_free char *fn = NULL; 702 | char *p; 703 | 704 | if (pathname == NULL || *pathname == '\0') 705 | { 706 | errno = EINVAL; 707 | return -1; 708 | } 709 | 710 | fn = xstrdup (pathname); 711 | 712 | p = fn; 713 | while (*p == '/') 714 | p++; 715 | 716 | do 717 | { 718 | while (*p && *p != '/') 719 | p++; 720 | 721 | if (!*p) 722 | p = NULL; 723 | else 724 | *p = '\0'; 725 | 726 | if (!create_last && p == NULL) 727 | break; 728 | 729 | if (ensure_dir (fn, mode) != 0) 730 | return -1; 731 | 732 | if (p) 733 | { 734 | *p++ = '/'; 735 | while (*p && *p == '/') 736 | p++; 737 | } 738 | } 739 | while (p); 740 | 741 | return 0; 742 | } 743 | 744 | /* Send an ucred with current pid/uid/gid over a socket, it can be 745 | read back with read_pid_from_socket(), and then the kernel has 746 | translated it between namespaces as needed. */ 747 | void 748 | send_pid_on_socket (int socket) 749 | { 750 | char buf[1] = { 0 }; 751 | struct msghdr msg = {}; 752 | struct iovec iov = { buf, sizeof (buf) }; 753 | const ssize_t control_len_snd = CMSG_SPACE(sizeof(struct ucred)); 754 | char control_buf_snd[control_len_snd]; 755 | struct cmsghdr *cmsg; 756 | struct ucred *cred; 757 | 758 | msg.msg_iov = &iov; 759 | msg.msg_iovlen = 1; 760 | msg.msg_control = control_buf_snd; 761 | msg.msg_controllen = control_len_snd; 762 | 763 | cmsg = CMSG_FIRSTHDR(&msg); 764 | cmsg->cmsg_level = SOL_SOCKET; 765 | cmsg->cmsg_type = SCM_CREDENTIALS; 766 | cmsg->cmsg_len = CMSG_LEN(sizeof(struct ucred)); 767 | cred = (struct ucred *)CMSG_DATA(cmsg); 768 | 769 | cred->pid = getpid (); 770 | cred->uid = geteuid (); 771 | cred->gid = getegid (); 772 | 773 | if (sendmsg (socket, &msg, 0) < 0) 774 | die_with_error ("Can't send pid"); 775 | } 776 | 777 | void 778 | create_pid_socketpair (int sockets[2]) 779 | { 780 | int enable = 1; 781 | 782 | if (socketpair (AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, sockets) != 0) 783 | die_with_error ("Can't create intermediate pids socket"); 784 | 785 | if (setsockopt (sockets[0], SOL_SOCKET, SO_PASSCRED, &enable, sizeof (enable)) < 0) 786 | die_with_error ("Can't set SO_PASSCRED"); 787 | } 788 | 789 | int 790 | read_pid_from_socket (int socket) 791 | { 792 | char recv_buf[1] = { 0 }; 793 | struct msghdr msg = {}; 794 | struct iovec iov = { recv_buf, sizeof (recv_buf) }; 795 | const ssize_t control_len_rcv = CMSG_SPACE(sizeof(struct ucred)); 796 | char control_buf_rcv[control_len_rcv]; 797 | struct cmsghdr* cmsg; 798 | 799 | msg.msg_iov = &iov; 800 | msg.msg_iovlen = 1; 801 | msg.msg_control = control_buf_rcv; 802 | msg.msg_controllen = control_len_rcv; 803 | 804 | if (recvmsg (socket, &msg, 0) < 0) 805 | die_with_error ("Can't read pid from socket"); 806 | 807 | if (msg.msg_controllen <= 0) 808 | die ("Unexpected short read from pid socket"); 809 | 810 | for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) 811 | { 812 | const unsigned payload_len = cmsg->cmsg_len - CMSG_LEN(0); 813 | if (cmsg->cmsg_level == SOL_SOCKET && 814 | cmsg->cmsg_type == SCM_CREDENTIALS && 815 | payload_len == sizeof(struct ucred)) 816 | { 817 | struct ucred *cred = (struct ucred *)CMSG_DATA(cmsg); 818 | return cred->pid; 819 | } 820 | } 821 | die ("No pid returned on socket"); 822 | } 823 | 824 | /* Sets errno on error (== NULL), 825 | * Always ensures terminating zero */ 826 | char * 827 | readlink_malloc (const char *pathname) 828 | { 829 | size_t size = 50; 830 | ssize_t n; 831 | cleanup_free char *value = NULL; 832 | 833 | do 834 | { 835 | if (size > SIZE_MAX / 2) 836 | die ("Symbolic link target pathname too long"); 837 | size *= 2; 838 | value = xrealloc (value, size); 839 | n = readlink (pathname, value, size - 1); 840 | if (n < 0) 841 | return NULL; 842 | } 843 | while (size - 2 < (size_t)n); 844 | 845 | value[n] = 0; 846 | return steal_pointer (&value); 847 | } 848 | 849 | char * 850 | get_oldroot_path (const char *path) 851 | { 852 | while (*path == '/') 853 | path++; 854 | return strconcat ("/oldroot/", path); 855 | } 856 | 857 | char * 858 | get_newroot_path (const char *path) 859 | { 860 | while (*path == '/') 861 | path++; 862 | return strconcat ("/newroot/", path); 863 | } 864 | 865 | int 866 | raw_clone (unsigned long flags, 867 | void *child_stack) 868 | { 869 | #if defined(__s390__) || defined(__CRIS__) 870 | /* On s390 and cris the order of the first and second arguments 871 | * of the raw clone() system call is reversed. */ 872 | return (int) syscall (__NR_clone, child_stack, flags); 873 | #else 874 | return (int) syscall (__NR_clone, flags, child_stack); 875 | #endif 876 | } 877 | 878 | int 879 | pivot_root (const char * new_root, const char * put_old) 880 | { 881 | #ifdef __NR_pivot_root 882 | return syscall (__NR_pivot_root, new_root, put_old); 883 | #else 884 | errno = ENOSYS; 885 | return -1; 886 | #endif 887 | } 888 | 889 | char * 890 | label_mount (const char *opt, UNUSED const char *mount_label) 891 | { 892 | #ifdef HAVE_SELINUX 893 | if (mount_label) 894 | { 895 | if (opt) 896 | return xasprintf ("%s,context=\"%s\"", opt, mount_label); 897 | else 898 | return xasprintf ("context=\"%s\"", mount_label); 899 | } 900 | #endif 901 | if (opt) 902 | return xstrdup (opt); 903 | return NULL; 904 | } 905 | 906 | int 907 | label_create_file (UNUSED const char *file_label) 908 | { 909 | #ifdef HAVE_SELINUX 910 | if (is_selinux_enabled () > 0 && file_label) 911 | return setfscreatecon (file_label); 912 | #endif 913 | return 0; 914 | } 915 | 916 | int 917 | label_exec (UNUSED const char *exec_label) 918 | { 919 | #ifdef HAVE_SELINUX 920 | if (is_selinux_enabled () > 0 && exec_label) 921 | return setexeccon (exec_label); 922 | #endif 923 | return 0; 924 | } 925 | 926 | /* 927 | * Like strerror(), but specialized for a failed mount(2) call. 928 | */ 929 | const char * 930 | mount_strerror (int errsv) 931 | { 932 | switch (errsv) 933 | { 934 | case ENOSPC: 935 | /* "No space left on device" misleads users into thinking there 936 | * is some sort of disk-space problem, but mount(2) uses that 937 | * errno value to mean something more like "limit exceeded". */ 938 | return ("Limit exceeded (ENOSPC). " 939 | "(Hint: Check that /proc/sys/fs/mount-max is sufficient, " 940 | "typically 100000)"); 941 | 942 | default: 943 | return strerror (errsv); 944 | } 945 | } 946 | -------------------------------------------------------------------------------- /tests/test-run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xeuo pipefail 4 | 5 | srcd=$(cd $(dirname "$0") && pwd) 6 | 7 | . ${srcd}/libtest.sh 8 | 9 | bn=$(basename "$0") 10 | 11 | test_count=0 12 | ok () { 13 | test_count=$((test_count + 1)) 14 | echo ok $test_count "$@" 15 | } 16 | ok_skip () { 17 | ok "# SKIP" "$@" 18 | } 19 | done_testing () { 20 | echo "1..$test_count" 21 | } 22 | 23 | # Test help 24 | ${BWRAP} --help > help.txt 25 | assert_file_has_content help.txt "usage: ${BWRAP}" 26 | ok "Help works" 27 | 28 | for ALT in "" "--unshare-user-try" "--unshare-pid" "--unshare-user-try --unshare-pid"; do 29 | # Test fuse fs as bind source 30 | if [ "x$FUSE_DIR" != "x" ]; then 31 | $RUN $ALT --proc /proc --dev /dev --bind $FUSE_DIR /tmp/foo true 32 | ok "can bind-mount a FUSE directory with $ALT" 33 | else 34 | ok_skip "no FUSE support" 35 | fi 36 | # no --dev => no devpts => no map_root workaround 37 | $RUN $ALT --proc /proc true 38 | ok "can mount /proc with $ALT" 39 | # No network 40 | $RUN $ALT --unshare-net --proc /proc --dev /dev true 41 | ok "can unshare network, create new /dev with $ALT" 42 | # Unreadable file 43 | echo -n "expect EPERM: " >&2 44 | 45 | # Test caps when bwrap is not setuid 46 | if test -n "${bwrap_is_suid:-}"; then 47 | CAP="--cap-add ALL" 48 | else 49 | CAP="" 50 | fi 51 | 52 | if ! cat /etc/shadow >/dev/null && 53 | $RUN $CAP $ALT --unshare-net --proc /proc --bind /etc/shadow /tmp/foo cat /tmp/foo; then 54 | assert_not_reached Could read /etc/shadow via /tmp/foo bind-mount 55 | fi 56 | 57 | if ! cat /etc/shadow >/dev/null && 58 | $RUN $CAP $ALT --unshare-net --proc /proc --bind /etc/shadow /tmp/foo cat /etc/shadow; then 59 | assert_not_reached Could read /etc/shadow 60 | fi 61 | 62 | ok "cannot read /etc/shadow with $ALT" 63 | # Unreadable dir 64 | if [ "x$UNREADABLE" != "x" ]; then 65 | echo -n "expect EPERM: " >&2 66 | if $RUN $ALT --unshare-net --proc /proc --dev /dev --bind $UNREADABLE /tmp/foo cat /tmp/foo; then 67 | assert_not_reached Could read $UNREADABLE 68 | fi 69 | ok "cannot read $UNREADABLE with $ALT" 70 | else 71 | ok_skip "not sure what unreadable file to use" 72 | fi 73 | 74 | # bind dest in symlink (https://github.com/projectatomic/bubblewrap/pull/119) 75 | $RUN $ALT --dir /tmp/dir --symlink dir /tmp/link --bind /etc /tmp/link true 76 | ok "can bind a destination over a symlink" 77 | done 78 | 79 | # Test symlink behaviour 80 | rm -f ./symlink 81 | $RUN --ro-bind / / --bind "$(pwd)" "$(pwd)" --symlink /dev/null "$(pwd)/symlink" true >&2 82 | readlink ./symlink > target.txt 83 | assert_file_has_content target.txt /dev/null 84 | ok "--symlink works" 85 | $RUN --ro-bind / / --bind "$(pwd)" "$(pwd)" --symlink /dev/null "$(pwd)/symlink" true >&2 86 | ok "--symlink is idempotent" 87 | if $RUN --ro-bind / / --bind "$(pwd)" "$(pwd)" --symlink /dev/full "$(pwd)/symlink" true 2>err.txt; then 88 | fatal "creating a conflicting symlink should have failed" 89 | else 90 | assert_file_has_content err.txt "Can't make symlink .*: existing destination is /dev/null" 91 | fi 92 | ok "--symlink doesn't overwrite a conflicting symlink" 93 | 94 | # Test devices 95 | $RUN --unshare-pid --dev /dev ls -al /dev/{stdin,stdout,stderr,null,random,urandom,fd,core} >/dev/null 96 | ok "all expected devices were created" 97 | 98 | # Test --as-pid-1 99 | $RUN --unshare-pid --as-pid-1 --bind / / bash -c 'echo $$' > as_pid_1.txt 100 | assert_file_has_content as_pid_1.txt "1" 101 | ok "can run as pid 1" 102 | 103 | # Test --info-fd and --json-status-fd 104 | 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 105 | fatal "should have been exit 42" 106 | fi 107 | assert_file_has_content info.json '"child-pid": [0-9]' 108 | assert_file_has_content json-status.json '"child-pid": [0-9]' 109 | assert_file_has_content_literal json-status.json '"exit-code": 42' 110 | ok "info and json-status fd" 111 | 112 | DATA=$($RUN --proc /proc --unshare-all --info-fd 42 --json-status-fd 43 -- bash -c 'stat -L --format "%n %i" /proc/self/ns/*' 42>info.json 43>json-status.json 2>err.txt) 113 | 114 | for NS in "ipc" "mnt" "net" "pid" "uts"; do 115 | 116 | want=$(echo "$DATA" | grep "/proc/self/ns/$NS" | awk '{print $2}') 117 | assert_file_has_content info.json "$want" 118 | assert_file_has_content json-status.json "$want" 119 | done 120 | 121 | ok "namespace id info in info and json-status fd" 122 | 123 | if ! command -v strace >/dev/null || ! strace -h | grep -v -e default | grep -e fault >/dev/null; then 124 | ok_skip "no strace fault injection" 125 | else 126 | ! 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 127 | assert_not_file_has_content json-status.json '"exit-code": [0-9]' 128 | ok "pre-exec failure doesn't include exit-code in json-status" 129 | fi 130 | 131 | notanexecutable=/ 132 | $RUN --json-status-fd 42 $notanexecutable 42>json-status.json || true 133 | assert_not_file_has_content json-status.json '"exit-code": [0-9]' 134 | ok "exec failure doesn't include exit-code in json-status" 135 | 136 | # These tests require --unshare-user 137 | if test -n "${bwrap_is_suid:-}"; then 138 | ok_skip "no --cap-add support" 139 | ok_skip "no --cap-add support" 140 | ok_skip "no --disable-userns" 141 | else 142 | BWRAP_RECURSE="$BWRAP --unshare-user --uid 0 --gid 0 --cap-add ALL --bind / / --bind /proc /proc" 143 | 144 | # $BWRAP May be inaccessible due to the user namespace so use /proc/self/exe 145 | $BWRAP_RECURSE -- /proc/self/exe --unshare-all --bind / / --bind /proc /proc echo hello > recursive_proc.txt 146 | assert_file_has_content recursive_proc.txt "hello" 147 | ok "can mount /proc recursively" 148 | 149 | $BWRAP_RECURSE -- /proc/self/exe --unshare-all ${BWRAP_RO_HOST_ARGS} findmnt > recursive-newroot.txt 150 | assert_file_has_content recursive-newroot.txt "/usr" 151 | ok "can pivot to new rootfs recursively" 152 | 153 | $BWRAP --dev-bind / / -- true 154 | ! $BWRAP --assert-userns-disabled --dev-bind / / -- true 155 | $BWRAP --unshare-user --disable-userns --dev-bind / / -- true 156 | ! $BWRAP --unshare-user --disable-userns --dev-bind / / -- $BWRAP --dev-bind / / -- true 157 | $BWRAP --unshare-user --disable-userns --dev-bind / / -- sh -c "echo 2 > /proc/sys/user/max_user_namespaces || true; ! $BWRAP --unshare-user --dev-bind / / -- true" 158 | $BWRAP --unshare-user --disable-userns --dev-bind / / -- sh -c "echo 100 > /proc/sys/user/max_user_namespaces || true; ! $BWRAP --unshare-user --dev-bind / / -- true" 159 | $BWRAP --unshare-user --disable-userns --dev-bind / / -- sh -c "! $BWRAP --unshare-user --dev-bind / / --assert-userns-disabled -- true" 160 | 161 | $BWRAP_RECURSE --dev-bind / / -- true 162 | ! $BWRAP_RECURSE --assert-userns-disabled --dev-bind / / -- true 163 | $BWRAP_RECURSE --unshare-user --disable-userns --dev-bind / / -- true 164 | ! $BWRAP_RECURSE --unshare-user --disable-userns --dev-bind / / -- /proc/self/exe --dev-bind / / -- true 165 | $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" 166 | $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" 167 | $BWRAP_RECURSE --unshare-user --disable-userns --dev-bind / / -- sh -c "! $BWRAP --unshare-user --dev-bind / / --assert-userns-disabled -- true" 168 | 169 | ok "can disable nested userns" 170 | fi 171 | 172 | # Test error prefixing 173 | if $RUN --unshare-pid --bind /source-enoent /dest true 2>err.txt; then 174 | assert_not_reached "bound nonexistent source" 175 | fi 176 | assert_file_has_content err.txt "^bwrap: Can't find source path.*source-enoent" 177 | ok "error prefixing" 178 | 179 | if ! ${is_uidzero}; then 180 | # When invoked as non-root, check that by default we have no caps left 181 | for OPT in "" "--unshare-user-try --as-pid-1" "--unshare-user-try" "--as-pid-1"; do 182 | e=0 183 | $RUN $OPT --unshare-pid getpcaps 1 >&2 2> caps.test || e=$? 184 | sed -e 's/^/# /' < caps.test >&2 185 | test "$e" = 0 186 | assert_not_file_has_content caps.test ': =.*cap' 187 | done 188 | ok "we have no caps as uid != 0" 189 | else 190 | capsh --print | sed -e 's/no-new-privs=0/no-new-privs=1/' > caps.expected 191 | 192 | for OPT in "" "--as-pid-1"; do 193 | $RUN $OPT --unshare-pid capsh --print >caps.test 194 | diff -u caps.expected caps.test 195 | done 196 | # And test that we can drop all, as well as specific caps 197 | $RUN $OPT --cap-drop ALL --unshare-pid capsh --print >caps.test 198 | assert_file_has_content caps.test 'Current: =$' 199 | # Check for dropping kill/fowner (we assume all uid 0 callers have this) 200 | # But we should still have net_bind_service for example 201 | $RUN $OPT --cap-drop CAP_KILL --cap-drop CAP_FOWNER --unshare-pid capsh --print >caps.test 202 | # capsh's output format changed from v2.29 -> drops are now indicated with -eip 203 | if grep 'Current: =.*+eip$' caps.test; then 204 | assert_not_file_has_content caps.test '^Current: =.*cap_kill.*+eip$' 205 | assert_not_file_has_content caps.test '^Current: =.*cap_fowner.*+eip$' 206 | assert_file_has_content caps.test '^Current: =.*cap_net_bind_service.*+eip$' 207 | else 208 | assert_file_has_content caps.test '^Current: =eip.*cap_kill.*-eip$' 209 | assert_file_has_content caps.test '^Current: =eip.*cap_fowner.*-eip$' 210 | assert_not_file_has_content caps.test '^Current: =.*cap_net_bind_service.*-eip$' 211 | fi 212 | ok "we have the expected caps as uid 0" 213 | fi 214 | 215 | # Test --die-with-parent 216 | 217 | cat >lockf-n.py < test.args 263 | printf '%s--dir\0/tmp/hello/world2\0' '' > test.args2 264 | printf '%s--dir\0/tmp/hello/world3\0' '' > test.args3 265 | $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 bin/--inadvisable-executable-name-- 270 | echo "echo hello" >> bin/--inadvisable-executable-name-- 271 | chmod +x bin/--inadvisable-executable-name-- 272 | PATH="${srcd}:$PATH" $RUN -- sh -c "echo hello" > stdout 273 | assert_file_has_content stdout hello 274 | ok "we can run with --" 275 | PATH="$(pwd)/bin:$PATH" $RUN -- --inadvisable-executable-name-- > stdout 276 | assert_file_has_content stdout hello 277 | ok "we can run an inadvisable executable name with --" 278 | if $RUN -- --dev-bind /dev /dev sh -c 'echo should not have run'; then 279 | assert_not_reached "'--dev-bind' should have been interpreted as a (silly) executable name" 280 | fi 281 | ok "options like --dev-bind are defanged by --" 282 | 283 | if command -v mktemp > /dev/null; then 284 | tempfile="$(mktemp /tmp/bwrap-test-XXXXXXXX)" 285 | echo "hello" > "$tempfile" 286 | $BWRAP --bind / / cat "$tempfile" > stdout 287 | assert_file_has_content stdout hello 288 | ok "bind-mount of / exposes real /tmp" 289 | $BWRAP --bind / / --bind /tmp /tmp cat "$tempfile" > stdout 290 | assert_file_has_content stdout hello 291 | ok "bind-mount of /tmp exposes real /tmp" 292 | if [ -d /mnt ] && [ ! -L /mnt ]; then 293 | $BWRAP --bind / / --bind /tmp /mnt cat "/mnt/${tempfile#/tmp/}" > stdout 294 | assert_file_has_content stdout hello 295 | ok "bind-mount of /tmp onto /mnt exposes real /tmp" 296 | else 297 | ok_skip "/mnt does not exist or is a symlink" 298 | fi 299 | else 300 | ok_skip "mktemp not found" 301 | ok_skip "mktemp not found" 302 | ok_skip "mktemp not found" 303 | fi 304 | 305 | if $RUN test -d /tmp/oldroot; then 306 | assert_not_reached "/tmp/oldroot should not be visible" 307 | fi 308 | if $RUN test -d /tmp/newroot; then 309 | assert_not_reached "/tmp/newroot should not be visible" 310 | fi 311 | 312 | echo "hello" > input.$$ 313 | $BWRAP --bind / / --bind "$(pwd)" /tmp cat /tmp/input.$$ > stdout 314 | assert_file_has_content stdout hello 315 | if $BWRAP --bind / / --bind "$(pwd)" /tmp test -d /tmp/oldroot; then 316 | assert_not_reached "/tmp/oldroot should not be visible" 317 | fi 318 | if $BWRAP --bind / / --bind "$(pwd)" /tmp test -d /tmp/newroot; then 319 | assert_not_reached "/tmp/newroot should not be visible" 320 | fi 321 | ok "we can mount another directory onto /tmp" 322 | 323 | echo "hello" > input.$$ 324 | $RUN --bind "$(pwd)" /tmp/here cat /tmp/here/input.$$ > stdout 325 | assert_file_has_content stdout hello 326 | if $RUN --bind "$(pwd)" /tmp/here test -d /tmp/oldroot; then 327 | assert_not_reached "/tmp/oldroot should not be visible" 328 | fi 329 | if $RUN --bind "$(pwd)" /tmp/here test -d /tmp/newroot; then 330 | assert_not_reached "/tmp/newroot should not be visible" 331 | fi 332 | ok "we can mount another directory inside /tmp" 333 | 334 | touch some-file 335 | mkdir -p some-dir 336 | rm -fr new-dir-mountpoint 337 | rm -fr new-file-mountpoint 338 | $RUN \ 339 | --bind "$(pwd -P)/some-dir" "$(pwd -P)/new-dir-mountpoint" \ 340 | --bind "$(pwd -P)/some-file" "$(pwd -P)/new-file-mountpoint" \ 341 | true 342 | command stat -c '%a' new-dir-mountpoint > new-dir-permissions 343 | assert_file_has_content new-dir-permissions 755 344 | command stat -c '%a' new-file-mountpoint > new-file-permissions 345 | assert_file_has_content new-file-permissions 444 346 | ok "Files and directories created as mount points have expected permissions" 347 | 348 | 349 | if [ -S /dev/log ]; then 350 | $RUN --bind / / --bind "$(realpath /dev/log)" "$(realpath /dev/log)" true 351 | ok "Can bind-mount a socket (/dev/log) onto a socket" 352 | else 353 | ok_skip "- /dev/log is not a socket, cannot test bubblewrap#409" 354 | fi 355 | 356 | mkdir -p dir-already-existed 357 | chmod 0710 dir-already-existed 358 | mkdir -p dir-already-existed2 359 | chmod 0754 dir-already-existed2 360 | rm -fr new-dir-default-perms 361 | rm -fr new-dir-set-perms 362 | $RUN \ 363 | --perms 1741 --dir "$(pwd -P)/new-dir-set-perms" \ 364 | --dir "$(pwd -P)/dir-already-existed" \ 365 | --perms 0741 --dir "$(pwd -P)/dir-already-existed2" \ 366 | --dir "$(pwd -P)/dir-chmod" \ 367 | --chmod 1755 "$(pwd -P)/dir-chmod" \ 368 | --dir "$(pwd -P)/new-dir-default-perms" \ 369 | true 370 | command stat -c '%a' new-dir-default-perms > new-dir-permissions 371 | assert_file_has_content new-dir-permissions '^755$' 372 | command stat -c '%a' new-dir-set-perms > new-dir-permissions 373 | assert_file_has_content new-dir-permissions '^1741$' 374 | command stat -c '%a' dir-already-existed > dir-permissions 375 | assert_file_has_content dir-permissions '^710$' 376 | command stat -c '%a' dir-already-existed2 > dir-permissions 377 | assert_file_has_content dir-permissions '^754$' 378 | command stat -c '%a' dir-chmod > dir-permissions 379 | assert_file_has_content dir-permissions '^1755$' 380 | ok "Directories created explicitly have expected permissions" 381 | 382 | rm -fr parent 383 | rm -fr parent-of-1777 384 | rm -fr parent-of-0755 385 | rm -fr parent-of-0644 386 | rm -fr parent-of-0750 387 | rm -fr parent-of-0710 388 | rm -fr parent-of-0720 389 | rm -fr parent-of-0640 390 | rm -fr parent-of-0700 391 | rm -fr parent-of-0600 392 | rm -fr parent-of-0705 393 | rm -fr parent-of-0604 394 | rm -fr parent-of-0000 395 | $RUN \ 396 | --dir "$(pwd -P)"/parent/dir \ 397 | --perms 1777 --dir "$(pwd -P)"/parent-of-1777/dir \ 398 | --perms 0755 --dir "$(pwd -P)"/parent-of-0755/dir \ 399 | --perms 0644 --dir "$(pwd -P)"/parent-of-0644/dir \ 400 | --perms 0750 --dir "$(pwd -P)"/parent-of-0750/dir \ 401 | --perms 0710 --dir "$(pwd -P)"/parent-of-0710/dir \ 402 | --perms 0720 --dir "$(pwd -P)"/parent-of-0720/dir \ 403 | --perms 0640 --dir "$(pwd -P)"/parent-of-0640/dir \ 404 | --perms 0700 --dir "$(pwd -P)"/parent-of-0700/dir \ 405 | --perms 0600 --dir "$(pwd -P)"/parent-of-0600/dir \ 406 | --perms 0705 --dir "$(pwd -P)"/parent-of-0705/dir \ 407 | --perms 0604 --dir "$(pwd -P)"/parent-of-0604/dir \ 408 | --perms 0000 --dir "$(pwd -P)"/parent-of-0000/dir \ 409 | true 410 | command stat -c '%a' parent > dir-permissions 411 | assert_file_has_content dir-permissions '^755$' 412 | command stat -c '%a' parent-of-1777 > dir-permissions 413 | assert_file_has_content dir-permissions '^755$' 414 | command stat -c '%a' parent-of-0755 > dir-permissions 415 | assert_file_has_content dir-permissions '^755$' 416 | command stat -c '%a' parent-of-0644 > dir-permissions 417 | assert_file_has_content dir-permissions '^755$' 418 | command stat -c '%a' parent-of-0750 > dir-permissions 419 | assert_file_has_content dir-permissions '^750$' 420 | command stat -c '%a' parent-of-0710 > dir-permissions 421 | assert_file_has_content dir-permissions '^750$' 422 | command stat -c '%a' parent-of-0720 > dir-permissions 423 | assert_file_has_content dir-permissions '^750$' 424 | command stat -c '%a' parent-of-0640 > dir-permissions 425 | assert_file_has_content dir-permissions '^750$' 426 | command stat -c '%a' parent-of-0700 > dir-permissions 427 | assert_file_has_content dir-permissions '^700$' 428 | command stat -c '%a' parent-of-0600 > dir-permissions 429 | assert_file_has_content dir-permissions '^700$' 430 | command stat -c '%a' parent-of-0705 > dir-permissions 431 | assert_file_has_content dir-permissions '^705$' 432 | command stat -c '%a' parent-of-0604 > dir-permissions 433 | assert_file_has_content dir-permissions '^705$' 434 | command stat -c '%a' parent-of-0000 > dir-permissions 435 | assert_file_has_content dir-permissions '^700$' 436 | chmod -R 0700 parent* 437 | rm -fr parent* 438 | ok "Directories created as parents have expected permissions" 439 | 440 | $RUN \ 441 | --perms 01777 --tmpfs "$(pwd -P)" \ 442 | cat /proc/self/mountinfo >&2 443 | $RUN \ 444 | --perms 01777 --tmpfs "$(pwd -P)" \ 445 | stat -c '%a' "$(pwd -P)" > dir-permissions 446 | assert_file_has_content dir-permissions '^1777$' 447 | $RUN \ 448 | --tmpfs "$(pwd -P)" \ 449 | stat -c '%a' "$(pwd -P)" > dir-permissions 450 | assert_file_has_content dir-permissions '^755$' 451 | ok "tmpfs has expected permissions" 452 | 453 | # 1048576 = 1 MiB 454 | if test -n "${bwrap_is_suid:-}"; then 455 | if $RUN --size 1048576 --tmpfs "$(pwd -P)" true; then 456 | assert_not_reached "Should not allow --size --tmpfs when setuid" 457 | fi 458 | ok "--size --tmpfs is not allowed when setuid" 459 | elif df --output=size --block-size=1K "$(pwd -P)" >/dev/null 2>/dev/null; then 460 | $RUN \ 461 | --size 1048576 --tmpfs "$(pwd -P)" \ 462 | df --output=size --block-size=1K "$(pwd -P)" > dir-size 463 | assert_file_has_content dir-size '^ *1024$' 464 | $RUN \ 465 | --size 1048576 --perms 01777 --tmpfs "$(pwd -P)" \ 466 | stat -c '%a' "$(pwd -P)" > dir-permissions 467 | assert_file_has_content dir-permissions '^1777$' 468 | $RUN \ 469 | --size 1048576 --perms 01777 --tmpfs "$(pwd -P)" \ 470 | df --output=size --block-size=1K "$(pwd -P)" > dir-size 471 | assert_file_has_content dir-size '^ *1024$' 472 | $RUN \ 473 | --perms 01777 --size 1048576 --tmpfs "$(pwd -P)" \ 474 | stat -c '%a' "$(pwd -P)" > dir-permissions 475 | assert_file_has_content dir-permissions '^1777$' 476 | $RUN \ 477 | --perms 01777 --size 1048576 --tmpfs "$(pwd -P)" \ 478 | df --output=size --block-size=1K "$(pwd -P)" > dir-size 479 | assert_file_has_content dir-size '^ *1024$' 480 | ok "tmpfs has expected size" 481 | else 482 | $RUN --size 1048576 --tmpfs "$(pwd -P)" true 483 | $RUN --perms 01777 --size 1048576 --tmpfs "$(pwd -P)" true 484 | $RUN --size 1048576 --perms 01777 --tmpfs "$(pwd -P)" true 485 | ok_skip "df is too old, cannot test --size --tmpfs fully" 486 | fi 487 | 488 | $RUN \ 489 | --file 0 /tmp/file \ 490 | stat -c '%a' /tmp/file < /dev/null > file-permissions 491 | assert_file_has_content file-permissions '^666$' 492 | $RUN \ 493 | --perms 0640 --file 0 /tmp/file \ 494 | stat -c '%a' /tmp/file < /dev/null > file-permissions 495 | assert_file_has_content file-permissions '^640$' 496 | $RUN \ 497 | --bind-data 0 /tmp/file \ 498 | stat -c '%a' /tmp/file < /dev/null > file-permissions 499 | assert_file_has_content file-permissions '^600$' 500 | $RUN \ 501 | --perms 0640 --bind-data 0 /tmp/file \ 502 | stat -c '%a' /tmp/file < /dev/null > file-permissions 503 | assert_file_has_content file-permissions '^640$' 504 | $RUN \ 505 | --ro-bind-data 0 /tmp/file \ 506 | stat -c '%a' /tmp/file < /dev/null > file-permissions 507 | assert_file_has_content file-permissions '^600$' 508 | $RUN \ 509 | --perms 0640 --ro-bind-data 0 /tmp/file \ 510 | stat -c '%a' /tmp/file < /dev/null > file-permissions 511 | assert_file_has_content file-permissions '^640$' 512 | ok "files have expected permissions" 513 | 514 | if $RUN --size 0 --tmpfs /tmp/a true; then 515 | assert_not_reached Zero tmpfs size allowed 516 | fi 517 | if $RUN --size 123bogus --tmpfs /tmp/a true; then 518 | assert_not_reached Bogus tmpfs size allowed 519 | fi 520 | if $RUN --size '' --tmpfs /tmp/a true; then 521 | assert_not_reached Empty tmpfs size allowed 522 | fi 523 | if $RUN --size -12345678 --tmpfs /tmp/a true; then 524 | assert_not_reached Negative tmpfs size allowed 525 | fi 526 | if $RUN --size ' -12345678' --tmpfs /tmp/a true; then 527 | assert_not_reached Negative tmpfs size with space allowed 528 | fi 529 | # This is 2^64 530 | if $RUN --size 18446744073709551616 --tmpfs /tmp/a true; then 531 | assert_not_reached Overflowing tmpfs size allowed 532 | fi 533 | # This is 2^63 + 1; note that the current max size is SIZE_MAX/2 534 | if $RUN --size 9223372036854775809 --tmpfs /tmp/a true; then 535 | assert_not_reached Too-large tmpfs size allowed 536 | fi 537 | ok "bogus tmpfs size not allowed" 538 | 539 | if $RUN --perms 0640 --perms 0640 --tmpfs /tmp/a true; then 540 | assert_not_reached Multiple perms options allowed 541 | fi 542 | if $RUN --size 1048576 --size 1048576 --tmpfs /tmp/a true; then 543 | assert_not_reached Multiple perms options allowed 544 | fi 545 | ok "--perms and --size only allowed once" 546 | 547 | 548 | FOO= BAR=baz $RUN --setenv FOO bar sh -c 'echo "$FOO$BAR"' > stdout 549 | assert_file_has_content stdout barbaz 550 | FOO=wrong BAR=baz $RUN --setenv FOO bar sh -c 'echo "$FOO$BAR"' > stdout 551 | assert_file_has_content stdout barbaz 552 | FOO=wrong BAR=baz $RUN --unsetenv FOO sh -c 'printf "%s%s" "$FOO" "$BAR"' > stdout 553 | printf baz > reference 554 | assert_files_equal stdout reference 555 | FOO=wrong BAR=wrong $RUN --clearenv /usr/bin/env > stdout 556 | echo "PWD=$(pwd -P)" > reference 557 | assert_files_equal stdout reference 558 | ok "environment manipulation" 559 | 560 | $RUN sh -c 'echo $0' > stdout 561 | assert_file_has_content stdout sh 562 | $RUN --argv0 sh sh -c 'echo $0' > stdout 563 | assert_file_has_content stdout sh 564 | $RUN --argv0 right sh -c 'echo $0' > stdout 565 | assert_file_has_content stdout right 566 | ok "argv0 manipulation" 567 | 568 | echo "foobar" > file-data 569 | $RUN --proc /proc --dev /dev --bind / / --bind-fd 100 /tmp cat /tmp/file-data 100< . > stdout 570 | assert_file_has_content stdout foobar 571 | 572 | ok "bind-fd" 573 | 574 | done_testing 575 | --------------------------------------------------------------------------------