summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/check.yml72
-rw-r--r--CODE-OF-CONDUCT.md2
-rw-r--r--Makefile.am10
-rw-r--r--README.md28
-rw-r--r--SECURITY.md2
-rw-r--r--bind-mount.c65
-rw-r--r--bind-mount.h9
-rw-r--r--bubblewrap.c163
-rw-r--r--bwrap.xml52
-rwxr-xr-xci/builddeps.sh2
-rw-r--r--completions/bash/bwrap2
-rw-r--r--completions/bash/meson.build36
-rw-r--r--completions/meson.build7
-rw-r--r--[-rwxr-xr-x]completions/zsh/_bwrap22
-rw-r--r--completions/zsh/meson.build7
-rw-r--r--configure.ac2
-rw-r--r--meson.build176
-rw-r--r--meson_options.txt73
-rw-r--r--release-checklist.md18
-rw-r--r--tests/meson.build72
-rwxr-xr-xtests/test-run.sh70
-rwxr-xr-xtests/test-specifying-pidns.sh1
-rw-r--r--tests/test-utils.c6
-rw-r--r--tests/try-syscall.c10
-rw-r--r--tests/use-as-subproject/.gitignore2
-rw-r--r--tests/use-as-subproject/README3
-rwxr-xr-xtests/use-as-subproject/assert-correct-rpath.py26
-rw-r--r--tests/use-as-subproject/config.h1
-rw-r--r--tests/use-as-subproject/dummy-config.h.in1
-rw-r--r--tests/use-as-subproject/meson.build20
-rw-r--r--utils.c8
-rw-r--r--utils.h4
32 files changed, 882 insertions, 90 deletions
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index ce561e2..d55d158 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -3,14 +3,14 @@ name: CI checks
on:
push:
branches:
- - master
+ - main
pull_request:
branches:
- - master
+ - main
jobs:
check:
- name: Build with gcc and test
+ name: Build with Autotools and gcc, and test
runs-on: ubuntu-latest
steps:
- name: Check out
@@ -69,6 +69,72 @@ jobs:
run: |
make -C _build -j $(getconf _NPROCESSORS_ONLN) distcheck VERBOSE=1 BWRAP_MUST_WORK=1
+ meson:
+ name: Build with Meson and gcc, and test
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out
+ uses: actions/checkout@v1
+ - name: Install build-dependencies
+ run: sudo ./ci/builddeps.sh
+ - name: Create logs dir
+ run: mkdir test-logs
+ - name: setup
+ run: |
+ meson _build
+ env:
+ CFLAGS: >-
+ -O2
+ -Wp,-D_FORTIFY_SOURCE=2
+ -fsanitize=address
+ -fsanitize=undefined
+ - name: compile
+ run: ninja -C _build -v
+ - name: smoke-test
+ run: |
+ set -x
+ ./_build/bwrap --bind / / --tmpfs /tmp true
+ env:
+ ASAN_OPTIONS: detect_leaks=0
+ - name: test
+ run: |
+ BWRAP_MUST_WORK=1 meson test -C _build
+ env:
+ ASAN_OPTIONS: detect_leaks=0
+ - name: Collect overall test logs on failure
+ if: failure()
+ run: mv _build/meson-logs/testlog.txt test-logs/ || true
+ - name: install
+ run: |
+ DESTDIR="$(pwd)/DESTDIR" meson install -C _build
+ ( cd DESTDIR && find -ls )
+ - name: dist
+ run: |
+ BWRAP_MUST_WORK=1 meson dist -C _build
+ - name: Collect dist test logs on failure
+ if: failure()
+ run: mv _build/meson-private/dist-build/meson-logs/testlog.txt test-logs/disttestlog.txt || true
+ - name: use as subproject
+ run: |
+ mkdir tests/use-as-subproject/subprojects
+ tar -C tests/use-as-subproject/subprojects -xf _build/meson-dist/bubblewrap-*.tar.xz
+ mv tests/use-as-subproject/subprojects/bubblewrap-* tests/use-as-subproject/subprojects/bubblewrap
+ ( cd tests/use-as-subproject && meson _build )
+ ninja -C tests/use-as-subproject/_build -v
+ meson test -C tests/use-as-subproject/_build
+ DESTDIR="$(pwd)/DESTDIR-as-subproject" meson install -C tests/use-as-subproject/_build
+ ( cd DESTDIR-as-subproject && find -ls )
+ test -x DESTDIR-as-subproject/usr/local/libexec/not-flatpak-bwrap
+ test ! -e DESTDIR-as-subproject/usr/local/bin/bwrap
+ test ! -e DESTDIR-as-subproject/usr/local/libexec/bwrap
+ tests/use-as-subproject/assert-correct-rpath.py DESTDIR-as-subproject/usr/local/libexec/not-flatpak-bwrap
+ - name: Upload test logs
+ uses: actions/upload-artifact@v1
+ if: failure() || cancelled()
+ with:
+ name: test logs
+ path: test-logs
+
clang:
name: Build with clang and analyze
runs-on: ubuntu-latest
diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md
index 6260919..8c417a6 100644
--- a/CODE-OF-CONDUCT.md
+++ b/CODE-OF-CONDUCT.md
@@ -1,3 +1,3 @@
## The bubblewrap Project Community Code of Conduct
-The bubblewrap project follows the [Containers Community Code of Conduct](https://github.com/containers/common/blob/master/CODE-OF-CONDUCT.md).
+The bubblewrap project follows the [Containers Community Code of Conduct](https://github.com/containers/common/blob/HEAD/CODE-OF-CONDUCT.md).
diff --git a/Makefile.am b/Makefile.am
index 9c8145e..94ef77f 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -5,11 +5,21 @@ EXTRA_DIST = \
.editorconfig \
README.md \
autogen.sh \
+ completions/bash/meson.build \
+ completions/meson.build \
+ completions/zsh/meson.build \
demos/bubblewrap-shell.sh \
demos/flatpak-run.sh \
demos/flatpak.bpf \
demos/userns-block-fd.py \
+ meson.build \
+ meson_options.txt \
packaging/bubblewrap.spec \
+ tests/meson.build \
+ tests/use-as-subproject/README \
+ tests/use-as-subproject/config.h \
+ tests/use-as-subproject/dummy-config.h.in \
+ tests/use-as-subproject/meson.build \
uncrustify.cfg \
uncrustify.sh \
$(NULL)
diff --git a/README.md b/README.md
index 4d50b5f..d251fbd 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ user namespaces. Emphasis on subset - specifically relevant to the
above CVE, bubblewrap does not allow control over iptables.
The original bubblewrap code existed before user namespaces - it inherits code from
-[xdg-app helper](https://cgit.freedesktop.org/xdg-app/xdg-app/tree/common/xdg-app-helper.c)
+[xdg-app helper](https://cgit.freedesktop.org/xdg-app/xdg-app/tree/common/xdg-app-helper.c?id=4c3bf179e2e4a2a298cd1db1d045adaf3f564532)
which in turn distantly derives from
[linux-user-chroot](https://git.gnome.org/browse/linux-user-chroot).
@@ -62,6 +62,30 @@ clusters. Having the ability for unprivileged users to use container
features would make it significantly easier to do interactive
debugging scenarios and the like.
+Installation
+------------
+
+bubblewrap is available in the package repositories of the most Linux distributions
+and can be installed from there.
+
+If you need to build bubblewrap from source, you can do this with meson or autotools.
+
+meson:
+
+```
+meson _builddir
+meson compile -C _builddir
+meson install -C _builddir
+```
+
+autotools:
+
+```
+./autogen.sh
+make
+sudo make install
+```
+
Usage
-----
@@ -117,7 +141,7 @@ Seccomp filters: You can pass in seccomp filters that limit which syscalls can b
Related project comparison: Firejail
------------------------------------
-[Firejail](https://github.com/netblue30/firejail/tree/master/src/firejail)
+[Firejail](https://github.com/netblue30/firejail/tree/HEAD/src/firejail)
is similar to Flatpak before bubblewrap was split out in that it combines
a setuid tool with a lot of desktop-specific sandboxing features. For
example, Firejail knows about Pulseaudio, whereas bubblewrap does not.
diff --git a/SECURITY.md b/SECURITY.md
index 455bf0f..d914d4a 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,3 +1,3 @@
## Security and Disclosure Information Policy for the bubblewrap Project
-The bubblewrap Project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/master/SECURITY.md) for the Containers Projects.
+The bubblewrap Project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/HEAD/SECURITY.md) for the Containers Projects.
diff --git a/bind-mount.c b/bind-mount.c
index 877b095..488d85a 100644
--- a/bind-mount.c
+++ b/bind-mount.c
@@ -378,7 +378,8 @@ bind_mount_result
bind_mount (int proc_fd,
const char *src,
const char *dest,
- bind_option_t options)
+ bind_option_t options,
+ char **failing_path)
{
bool readonly = (options & BIND_READONLY) != 0;
bool devices = (options & BIND_DEVICES) != 0;
@@ -406,7 +407,12 @@ bind_mount (int proc_fd,
dest_fd = open (resolved_dest, O_PATH | O_CLOEXEC);
if (dest_fd < 0)
- return BIND_MOUNT_ERROR_REOPEN_DEST;
+ {
+ if (failing_path != NULL)
+ *failing_path = steal_pointer (&resolved_dest);
+
+ return BIND_MOUNT_ERROR_REOPEN_DEST;
+ }
/* If we are in a case-insensitive filesystem, mountinfo might contain a
* different case combination of the path we requested to mount.
@@ -422,11 +428,19 @@ bind_mount (int proc_fd,
oldroot_dest_proc = get_oldroot_path (dest_proc);
kernel_case_combination = readlink_malloc (oldroot_dest_proc);
if (kernel_case_combination == NULL)
- return BIND_MOUNT_ERROR_READLINK_DEST_PROC_FD;
+ {
+ if (failing_path != NULL)
+ *failing_path = steal_pointer (&resolved_dest);
+
+ return BIND_MOUNT_ERROR_READLINK_DEST_PROC_FD;
+ }
mount_tab = parse_mountinfo (proc_fd, kernel_case_combination);
if (mount_tab[0].mountpoint == NULL)
{
+ if (failing_path != NULL)
+ *failing_path = steal_pointer (&kernel_case_combination);
+
errno = EINVAL;
return BIND_MOUNT_ERROR_FIND_DEST_MOUNT;
}
@@ -437,7 +451,12 @@ bind_mount (int proc_fd,
if (new_flags != current_flags &&
mount ("none", resolved_dest,
NULL, MS_SILENT | MS_BIND | MS_REMOUNT | new_flags, NULL) != 0)
- return BIND_MOUNT_ERROR_REMOUNT_DEST;
+ {
+ if (failing_path != NULL)
+ *failing_path = steal_pointer (&resolved_dest);
+
+ return BIND_MOUNT_ERROR_REMOUNT_DEST;
+ }
/* We need to work around the fact that a bind mount does not apply the flags, so we need to manually
* apply the flags to all submounts in the recursive case.
@@ -456,7 +475,12 @@ bind_mount (int proc_fd,
/* If we can't read the mountpoint we can't remount it, but that should
be safe to ignore because its not something the user can access. */
if (errno != EACCES)
- return BIND_MOUNT_ERROR_REMOUNT_SUBMOUNT;
+ {
+ if (failing_path != NULL)
+ *failing_path = xstrdup (mount_tab[i].mountpoint);
+
+ return BIND_MOUNT_ERROR_REMOUNT_SUBMOUNT;
+ }
}
}
}
@@ -469,50 +493,53 @@ bind_mount (int proc_fd,
* If want_errno_p is non-NULL, *want_errno_p is used to indicate whether
* it would make sense to print strerror(saved_errno).
*/
-const char *
+static char *
bind_mount_result_to_string (bind_mount_result res,
+ const char *failing_path,
bool *want_errno_p)
{
- const char *string;
+ char *string = NULL;
bool want_errno = TRUE;
switch (res)
{
case BIND_MOUNT_ERROR_MOUNT:
- string = "Unable to mount source on destination";
+ string = xstrdup ("Unable to mount source on destination");
break;
case BIND_MOUNT_ERROR_REALPATH_DEST:
- string = "realpath(destination)";
+ string = xstrdup ("realpath(destination)");
break;
case BIND_MOUNT_ERROR_REOPEN_DEST:
- string = "open(destination, O_PATH)";
+ string = xasprintf ("open(\"%s\", O_PATH)", failing_path);
break;
case BIND_MOUNT_ERROR_READLINK_DEST_PROC_FD:
- string = "readlink(/proc/self/fd/<destination>)";
+ string = xasprintf ("readlink(/proc/self/fd/N) for \"%s\"", failing_path);
break;
case BIND_MOUNT_ERROR_FIND_DEST_MOUNT:
- string = "Unable to find destination in mount table";
+ string = xasprintf ("Unable to find \"%s\" in mount table", failing_path);
want_errno = FALSE;
break;
case BIND_MOUNT_ERROR_REMOUNT_DEST:
- string = "Unable to remount destination with correct flags";
+ string = xasprintf ("Unable to remount destination \"%s\" with correct flags",
+ failing_path);
break;
case BIND_MOUNT_ERROR_REMOUNT_SUBMOUNT:
- string = "Unable to remount recursively with correct flags";
+ string = xasprintf ("Unable to apply mount flags: remount \"%s\"",
+ failing_path);
break;
case BIND_MOUNT_SUCCESS:
- string = "Success";
+ string = xstrdup ("Success");
break;
default:
- string = "(unknown/invalid bind_mount_result)";
+ string = xstrdup ("(unknown/invalid bind_mount_result)");
break;
}
@@ -525,11 +552,13 @@ bind_mount_result_to_string (bind_mount_result res,
void
die_with_bind_result (bind_mount_result res,
int saved_errno,
+ const char *failing_path,
const char *format,
...)
{
va_list args;
bool want_errno = TRUE;
+ char *message;
fprintf (stderr, "bwrap: ");
@@ -537,7 +566,9 @@ die_with_bind_result (bind_mount_result res,
vfprintf (stderr, format, args);
va_end (args);
- fprintf (stderr, ": %s", bind_mount_result_to_string (res, &want_errno));
+ message = bind_mount_result_to_string (res, failing_path, &want_errno);
+ fprintf (stderr, ": %s", message);
+ /* message is leaked, but we're exiting unsuccessfully anyway, so ignore */
if (want_errno)
fprintf (stderr, ": %s", strerror (saved_errno));
diff --git a/bind-mount.h b/bind-mount.h
index 1fac3e5..8a361fb 100644
--- a/bind-mount.h
+++ b/bind-mount.h
@@ -42,14 +42,13 @@ typedef enum
bind_mount_result bind_mount (int proc_fd,
const char *src,
const char *dest,
- bind_option_t options);
-
-const char *bind_mount_result_to_string (bind_mount_result res,
- bool *want_errno);
+ bind_option_t options,
+ char **failing_path);
void die_with_bind_result (bind_mount_result res,
int saved_errno,
+ const char *failing_path,
const char *format,
...)
__attribute__((__noreturn__))
- __attribute__((format (printf, 3, 4)));
+ __attribute__((format (printf, 4, 5)));
diff --git a/bubblewrap.c b/bubblewrap.c
index c043158..f2e4a12 100644
--- a/bubblewrap.c
+++ b/bubblewrap.c
@@ -23,6 +23,7 @@
#include <sched.h>
#include <pwd.h>
#include <grp.h>
+#include <ctype.h>
#include <sys/mount.h>
#include <sys/socket.h>
#include <sys/wait.h>
@@ -52,6 +53,12 @@
__result; }))
#endif
+/* We limit the size of a tmpfs to half the architecture's address space,
+ * to avoid hitting arbitrary limits in the kernel.
+ * For example, on at least one x86_64 machine, the actual limit seems to be
+ * 2^64 - 2^12. */
+#define MAX_TMPFS_BYTES ((size_t) (SIZE_MAX >> 1))
+
/* Globals to avoid having to use getuid(), since the uid/gid changes during runtime */
static uid_t real_uid;
static gid_t real_gid;
@@ -91,6 +98,7 @@ int opt_userns_fd = -1;
int opt_userns2_fd = -1;
int opt_pidns_fd = -1;
int next_perms = -1;
+size_t next_size_arg = 0;
#define CAP_TO_MASK_0(x) (1L << ((x) & 31))
#define CAP_TO_MASK_1(x) CAP_TO_MASK_0(x - 32)
@@ -149,6 +157,7 @@ struct _SetupOp
int fd;
SetupOpFlag flags;
int perms;
+ size_t size; /* number of bytes, zero means unset/default */
SetupOp *next;
};
@@ -177,6 +186,7 @@ typedef struct
uint32_t op;
uint32_t flags;
uint32_t perms;
+ size_t size_arg;
uint32_t arg1_offset;
uint32_t arg2_offset;
} PrivSepOp;
@@ -330,7 +340,7 @@ usage (int ecode, FILE *out)
" --ro-bind-data FD DEST Copy from FD to file which is readonly bind-mounted on DEST\n"
" --symlink SRC DEST Create symlink at DEST with target SRC\n"
" --seccomp FD Load and use seccomp rules from FD (not repeatable)\n"
- " --add-seccomp FD Load and use seccomp rules from FD (repeatable)\n"
+ " --add-seccomp-fd FD Load and use seccomp rules from FD (repeatable)\n"
" --block-fd FD Block on FD until some data to read is available\n"
" --userns-block-fd FD Block on FD until the user namespace is ready\n"
" --info-fd FD Write information about the running container to FD\n"
@@ -341,6 +351,7 @@ usage (int ecode, FILE *out)
" --cap-add CAP Add cap CAP when running as privileged user\n"
" --cap-drop CAP Drop cap CAP when running as privileged user\n"
" --perms OCTAL Set permissions of next argument (--bind-data, --file, etc.)\n"
+ " --size BYTES Set size of next argument (only for --tmpfs)\n"
" --chmod OCTAL PATH Change permissions of PATH (must already exist)\n"
);
exit (ecode);
@@ -1001,10 +1012,12 @@ privileged_op (int privileged_op_socket,
uint32_t op,
uint32_t flags,
uint32_t perms,
+ size_t size_arg,
const char *arg1,
const char *arg2)
{
bind_mount_result bind_result;
+ char *failing_path = NULL;
if (privileged_op_socket != -1)
{
@@ -1032,6 +1045,7 @@ privileged_op (int privileged_op_socket,
op_buffer->op = op;
op_buffer->flags = flags;
op_buffer->perms = perms;
+ op_buffer->size_arg = size_arg;
op_buffer->arg1_offset = arg1_offset;
op_buffer->arg2_offset = arg2_offset;
if (arg1 != NULL)
@@ -1070,23 +1084,25 @@ privileged_op (int privileged_op_socket,
break;
case PRIV_SEP_OP_REMOUNT_RO_NO_RECURSIVE:
- bind_result = bind_mount (proc_fd, NULL, arg2, BIND_READONLY);
+ bind_result = bind_mount (proc_fd, NULL, arg2, BIND_READONLY, &failing_path);
if (bind_result != BIND_MOUNT_SUCCESS)
- die_with_bind_result (bind_result, errno,
+ die_with_bind_result (bind_result, errno, failing_path,
"Can't remount readonly on %s", arg2);
+ assert (failing_path == NULL); /* otherwise we would have died */
break;
case PRIV_SEP_OP_BIND_MOUNT:
/* We always bind directories recursively, otherwise this would let us
access files that are otherwise covered on the host */
- bind_result = bind_mount (proc_fd, arg1, arg2, BIND_RECURSIVE | flags);
+ bind_result = bind_mount (proc_fd, arg1, arg2, BIND_RECURSIVE | flags, &failing_path);
if (bind_result != BIND_MOUNT_SUCCESS)
- die_with_bind_result (bind_result, errno,
+ die_with_bind_result (bind_result, errno, failing_path,
"Can't bind mount %s on %s", arg1, arg2);
+ assert (failing_path == NULL); /* otherwise we would have died */
break;
case PRIV_SEP_OP_PROC_MOUNT:
@@ -1096,7 +1112,18 @@ privileged_op (int privileged_op_socket,
case PRIV_SEP_OP_TMPFS_MOUNT:
{
- cleanup_free char *mode = xasprintf ("mode=%#o", perms);
+ cleanup_free char *mode = NULL;
+
+ /* This check should be unnecessary since we checked this when parsing
+ * the --size option as well. However, better be safe than sorry. */
+ if (size_arg > MAX_TMPFS_BYTES)
+ die_with_error ("Specified tmpfs size too large (%zu > %zu)", size_arg, MAX_TMPFS_BYTES);
+
+ if (size_arg != 0)
+ mode = xasprintf ("mode=%#o,size=%zu", perms, size_arg);
+ else
+ mode = xasprintf ("mode=%#o", perms);
+
cleanup_free char *opt = label_mount (mode, opt_file_label);
if (mount ("tmpfs", arg1, "tmpfs", MS_NOSUID | MS_NODEV, opt) != 0)
die_with_error ("Can't mount tmpfs on %s", arg1);
@@ -1197,12 +1224,12 @@ setup_newroot (bool unshare_pid,
PRIV_SEP_OP_BIND_MOUNT,
(op->type == SETUP_RO_BIND_MOUNT ? BIND_READONLY : 0) |
(op->type == SETUP_DEV_BIND_MOUNT ? BIND_DEVICES : 0),
- 0, source, dest);
+ 0, 0, source, dest);
break;
case SETUP_REMOUNT_RO_NO_RECURSIVE:
privileged_op (privileged_op_socket,
- PRIV_SEP_OP_REMOUNT_RO_NO_RECURSIVE, 0, 0, NULL, dest);
+ PRIV_SEP_OP_REMOUNT_RO_NO_RECURSIVE, 0, 0, 0, NULL, dest);
break;
case SETUP_MOUNT_PROC:
@@ -1213,14 +1240,14 @@ setup_newroot (bool unshare_pid,
{
/* Our own procfs */
privileged_op (privileged_op_socket,
- PRIV_SEP_OP_PROC_MOUNT, 0, 0,
+ PRIV_SEP_OP_PROC_MOUNT, 0, 0, 0,
dest, NULL);
}
else
{
/* Use system procfs, as we share pid namespace anyway */
privileged_op (privileged_op_socket,
- PRIV_SEP_OP_BIND_MOUNT, 0, 0,
+ PRIV_SEP_OP_BIND_MOUNT, 0, 0, 0,
"oldroot/proc", dest);
}
@@ -1242,7 +1269,7 @@ setup_newroot (bool unshare_pid,
}
privileged_op (privileged_op_socket,
- PRIV_SEP_OP_BIND_MOUNT, BIND_READONLY, 0,
+ PRIV_SEP_OP_BIND_MOUNT, BIND_READONLY, 0, 0,
subdir, subdir);
}
@@ -1253,7 +1280,7 @@ setup_newroot (bool unshare_pid,
die_with_error ("Can't mkdir %s", op->dest);
privileged_op (privileged_op_socket,
- PRIV_SEP_OP_TMPFS_MOUNT, 0, 0755,
+ PRIV_SEP_OP_TMPFS_MOUNT, 0, 0755, 0,
dest, NULL);
static const char *const devnodes[] = { "null", "zero", "full", "random", "urandom", "tty" };
@@ -1264,7 +1291,7 @@ setup_newroot (bool unshare_pid,
if (create_file (node_dest, 0444, NULL) != 0)
die_with_error ("Can't create file %s/%s", op->dest, devnodes[i]);
privileged_op (privileged_op_socket,
- PRIV_SEP_OP_BIND_MOUNT, BIND_DEVICES, 0,
+ PRIV_SEP_OP_BIND_MOUNT, BIND_DEVICES, 0, 0,
node_src, node_dest);
}
@@ -1298,7 +1325,7 @@ setup_newroot (bool unshare_pid,
if (mkdir (pts, 0755) == -1)
die_with_error ("Can't create %s/devpts", op->dest);
privileged_op (privileged_op_socket,
- PRIV_SEP_OP_DEVPTS_MOUNT, 0, 0, pts, NULL);
+ PRIV_SEP_OP_DEVPTS_MOUNT, 0, 0, 0, pts, NULL);
if (symlink ("pts/ptmx", ptmx) != 0)
die_with_error ("Can't make symlink at %s/ptmx", op->dest);
@@ -1318,7 +1345,7 @@ setup_newroot (bool unshare_pid,
die_with_error ("creating %s/console", op->dest);
privileged_op (privileged_op_socket,
- PRIV_SEP_OP_BIND_MOUNT, BIND_DEVICES, 0,
+ PRIV_SEP_OP_BIND_MOUNT, BIND_DEVICES, 0, 0,
src_tty_dev, dest_console);
}
@@ -1333,7 +1360,7 @@ setup_newroot (bool unshare_pid,
die_with_error ("Can't mkdir %s", op->dest);
privileged_op (privileged_op_socket,
- PRIV_SEP_OP_TMPFS_MOUNT, 0, op->perms,
+ PRIV_SEP_OP_TMPFS_MOUNT, 0, op->perms, op->size,
dest, NULL);
break;
@@ -1342,7 +1369,7 @@ setup_newroot (bool unshare_pid,
die_with_error ("Can't mkdir %s", op->dest);
privileged_op (privileged_op_socket,
- PRIV_SEP_OP_MQUEUE_MOUNT, 0, 0,
+ PRIV_SEP_OP_MQUEUE_MOUNT, 0, 0, 0,
dest, NULL);
break;
@@ -1423,7 +1450,7 @@ setup_newroot (bool unshare_pid,
privileged_op (privileged_op_socket,
PRIV_SEP_OP_BIND_MOUNT,
(op->type == SETUP_MAKE_RO_BIND_FILE ? BIND_READONLY : 0),
- 0, tempfile, dest);
+ 0, 0, tempfile, dest);
/* Remove the file so we're sure the app can't get to it in any other way.
Its outside the container chroot, so it shouldn't be possible, but lets
@@ -1441,7 +1468,7 @@ setup_newroot (bool unshare_pid,
case SETUP_SET_HOSTNAME:
assert (op->dest != NULL); /* guaranteed by the constructor */
privileged_op (privileged_op_socket,
- PRIV_SEP_OP_SET_HOSTNAME, 0, 0,
+ PRIV_SEP_OP_SET_HOSTNAME, 0, 0, 0,
op->dest, NULL);
break;
@@ -1450,7 +1477,7 @@ setup_newroot (bool unshare_pid,
}
}
privileged_op (privileged_op_socket,
- PRIV_SEP_OP_DONE, 0, 0, NULL, NULL);
+ PRIV_SEP_OP_DONE, 0, 0, 0, NULL, NULL);
}
/* Do not leak file descriptors already used by setup_newroot () */
@@ -1537,6 +1564,7 @@ read_priv_sec_op (int read_socket,
size_t buffer_size,
uint32_t *flags,
uint32_t *perms,
+ size_t *size_arg,
const char **arg1,
const char **arg2)
{
@@ -1561,6 +1589,7 @@ read_priv_sec_op (int read_socket,
*flags = op->flags;
*perms = op->perms;
+ *size_arg = op->size_arg;
*arg1 = resolve_string_offset (buffer, rec_len, op->arg1_offset);
*arg2 = resolve_string_offset (buffer, rec_len, op->arg2_offset);
@@ -1575,25 +1604,10 @@ print_version_and_exit (void)
}
static int
-takes_perms (const char *next_option)
+is_modifier_option (const char *option)
{
- static const char *const options_that_take_perms[] =
- {
- "--bind-data",
- "--dir",
- "--file",
- "--ro-bind-data",
- "--tmpfs",
- };
- size_t i;
-
- for (i = 0; i < N_ELEMENTS (options_that_take_perms); i++)
- {
- if (strcmp (options_that_take_perms[i], next_option) == 0)
- return 1;
- }
-
- return 0;
+ return strcmp (option, "--perms") == 0
+ || strcmp(option, "--size") == 0;
}
static void
@@ -1630,9 +1644,6 @@ parse_args_recurse (int *argcp,
{
const char *arg = argv[0];
- if (next_perms >= 0 && !takes_perms (arg))
- die ("--perms must be followed by an option that creates a file");
-
if (strcmp (arg, "--help") == 0)
{
usage (EXIT_SUCCESS, stdout);
@@ -1890,6 +1901,13 @@ parse_args_recurse (int *argcp,
op->perms = 0755;
next_perms = -1;
+
+ /* If the option is unset, next_size_arg is zero, which results in
+ * the default tmpfs size. This is exactly what we want. */
+ op->size = next_size_arg;
+
+ next_size_arg = 0;
+
argv += 1;
argc -= 1;
}
@@ -2383,6 +2401,9 @@ parse_args_recurse (int *argcp,
if (argc < 2)
die ("--perms takes an argument");
+ if (next_perms != -1)
+ die ("--perms given twice for the same action");
+
perms = strtoul (argv[1], &endptr, 8);
if (argv[1][0] == '\0'
@@ -2396,6 +2417,42 @@ parse_args_recurse (int *argcp,
argv += 1;
argc -= 1;
}
+ else if (strcmp (arg, "--size") == 0)
+ {
+ unsigned long long size;
+ char *endptr = NULL;
+
+ if (is_privileged)
+ die ("The --size option is not permitted in setuid mode");
+
+ if (argc < 2)
+ die ("--size takes an argument");
+
+ if (next_size_arg != 0)
+ die ("--size given twice for the same action");
+
+ errno = 0; /* reset errno so we can detect ERANGE from strtoull */
+
+ size = strtoull (argv[1], &endptr, 0);
+
+ /* isdigit: Not only check that the first digit is not '\0', but
+ * simultaneously guard against negative numbers or preceding
+ * spaces. */
+ if (errno != 0 /* from strtoull */
+ || !isdigit(argv[1][0])
+ || endptr == NULL
+ || *endptr != '\0'
+ || size == 0)
+ die ("--size takes a non-zero number of bytes");
+
+ if (size > MAX_TMPFS_BYTES)
+ die ("--size (for tmpfs) is limited to %zu", MAX_TMPFS_BYTES);
+
+ next_size_arg = (size_t) size;
+
+ argv += 1;
+ argc -= 1;
+ }
else if (strcmp (arg, "--chmod") == 0)
{
unsigned long perms;
@@ -2435,6 +2492,16 @@ parse_args_recurse (int *argcp,
break;
}
+ /* If --perms was set for the current action but the current action
+ * didn't consume the setting, apparently --perms wasn't suitable for
+ * this action. */
+ if (!is_modifier_option(arg) && next_perms >= 0)
+ die ("--perms must be followed by an option that creates a file");
+
+ /* Similarly for --size. */
+ if (!is_modifier_option(arg) && next_size_arg != 0)
+ die ("--size must be followed by --tmpfs");
+
argv++;
argc--;
}
@@ -2552,7 +2619,7 @@ main (int argc,
struct stat sbuf;
uint64_t val;
int res UNUSED;
- cleanup_free char *args_data = NULL;
+ cleanup_free char *args_data UNUSED = NULL;
int intermediate_pids_sockets[2] = {-1, -1};
/* Handle --version early on before we try to acquire/drop
@@ -2790,6 +2857,9 @@ main (int argc,
die ("No permissions to creating new namespace, likely because the kernel does not allow non-privileged user namespaces. On e.g. debian this can be enabled with 'sysctl kernel.unprivileged_userns_clone=1'.");
}
+ if (errno == ENOSPC)
+ die ("Creating new namespace failed: nesting depth or /proc/sys/user/max_*_namespaces exceeded (ENOSPC)");
+
die_with_error ("Creating new namespace failed");
}
@@ -3016,6 +3086,7 @@ main (int argc,
int status;
uint32_t buffer[2048]; /* 8k, but is int32 to guarantee nice alignment */
uint32_t op, flags, perms;
+ size_t size_arg;
const char *arg1, *arg2;
cleanup_fd int unpriv_socket = -1;
@@ -3025,8 +3096,8 @@ main (int argc,
do
{
op = read_priv_sec_op (unpriv_socket, buffer, sizeof (buffer),
- &flags, &perms, &arg1, &arg2);
- privileged_op (-1, op, flags, perms, arg1, arg2);
+ &flags, &perms, &size_arg, &arg1, &arg2);
+ privileged_op (-1, op, flags, perms, size_arg, arg1, arg2);
if (write (unpriv_socket, buffer, 1) != 1)
die ("Can't write to op_socket");
}
@@ -3068,8 +3139,8 @@ main (int argc,
* Both runc and LXC are using this "alternative" method for
* setting up the root of the container:
*
- * https://github.com/opencontainers/runc/blob/master/libcontainer/rootfs_linux.go#L671
- * https://github.com/lxc/lxc/blob/master/src/lxc/conf.c#L1121
+ * https://github.com/opencontainers/runc/blob/HEAD/libcontainer/rootfs_linux.go#L671
+ * https://github.com/lxc/lxc/blob/HEAD/src/lxc/conf.c#L1121
*/
if (pivot_root (".", ".") != 0)
die_with_error ("pivot_root(/newroot)");
diff --git a/bwrap.xml b/bwrap.xml
index 4768175..46e2478 100644
--- a/bwrap.xml
+++ b/bwrap.xml
@@ -131,6 +131,10 @@
<listitem><para>Unshare all possible namespaces. Currently equivalent with: <option>--unshare-user-try</option> <option>--unshare-ipc</option> <option>--unshare-pid</option> <option>--unshare-net</option> <option>--unshare-uts</option> <option>--unshare-cgroup-try</option></para></listitem>
</varlistentry>
<varlistentry>
+ <term><option>--share-net</option></term>
+ <listitem><para>Retain the network namespace, overriding an earlier <option>--unshare-all</option> or <option>--unshare-net</option></para></listitem>
+ </varlistentry>
+ <varlistentry>
<term><option>--userns <arg choice="plain">FD</arg></option></term>
<listitem><para>Use an existing user namespace instead of creating a new one. The namespace must fulfil the permission requirements for setns(), which generally means that it must be a descendant of the currently active user namespace, owned by the same user. </para>
<para>This is incompatible with --unshare-user, and doesn't work in the setuid version of bubblewrap.</para></listitem>
@@ -203,6 +207,9 @@
(rwxr-xr-x). However, if a <option>--perms</option> option is in effect, and
it sets the permissions for group or other to zero, then newly-created
parent directories will also have their corresponding permission set to zero.
+ <option>--size</option> modifies the size of the created mount when preceding a
+ <option>--tmpfs</option> action; <option>--perms</option> and <option>--size</option>
+ can be combined.
</para>
<variablelist>
<varlistentry>
@@ -213,7 +220,24 @@
Subsequent operations are not affected: for example,
<literal>--perms 0700 --tmpfs /a --tmpfs /b</literal> will mount
<filename>/a</filename> with permissions 0700, then return to
- the default permissions for <filename>/b</filename>.</para></listitem>
+ the default permissions for <filename>/b</filename>.
+ Note that <option>--perms</option> and <option>--size</option> can be
+ combined: <literal>--perms 0700 --size 10485760 --tmpfs /s</literal> will apply
+ permissions as well as a maximum size to the created tmpfs.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--size <arg choice="plain">BYTES</arg></option></term>
+ <listitem><para>This option does nothing on its own, and must be followed
+ by <literal>--tmpfs</literal>. It sets the size in bytes for the next tmpfs.
+ For example, <literal>--size 10485760 --tmpfs /tmp</literal> will create a tmpfs
+ at <filename>/tmp</filename> of size 10MiB. Subsequent operations are not
+ affected: for example,
+ <literal>--size 10485760 --tmpfs /a --tmpfs /b</literal> will mount
+ <filename>/a</filename> with size 10MiB, then return to the default size for
+ <filename>/b</filename>.
+ Note that <option>--perms</option> and <option>--size</option> can be
+ combined: <literal>--size 10485760 --perms 0700 --tmpfs /s</literal> will apply
+ permissions as well as a maximum size to the created tmpfs.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--bind <arg choice="plain">SRC</arg> <arg choice="plain">DEST</arg></option></term>
@@ -256,7 +280,9 @@
<listitem>
<para>Mount new tmpfs on <arg choice="plain">DEST</arg>.
If the previous option was <option>--perms</option>, it sets the
- mode of the tmpfs. Otherwise, the tmpfs has mode 0755.</para>
+ mode of the tmpfs. Otherwise, the tmpfs has mode 0755.
+ If the previous option was <option>--size</option>, it sets the
+ size in bytes of the tmpfs. Otherwise, the tmpfs has the default size.</para>
</listitem>
</varlistentry>
<varlistentry>
@@ -382,6 +408,28 @@
</para></listitem>
</varlistentry>
<varlistentry>
+ <term><option>--json-status-fd <arg choice="plain">FD</arg></option></term>
+ <listitem><para>
+ Multiple JSON documents are written to <arg choice="plain">FD</arg>,
+ one per line (<ulink url="https://jsonlines.org/">"JSON lines" format</ulink>).
+ Each line is a single JSON object.
+ After <command>bwrap</command> has started the child process inside the sandbox,
+ it writes an object with a <literal>child-pid</literal> member to the
+ <option>--json-status-fd</option> (this duplicates the older <option>--info-fd</option>).
+ The corresponding value is the process ID of the child process in the pid namespace from
+ which <command>bwrap</command> was run.
+ If available, the namespace IDs are also included in the object with the <literal>child-pid</literal>;
+ again, this duplicates the older <option>--info-fd</option>.
+ When the child process inside the sandbox exits, <command>bwrap</command> writes an object
+ with an exit-code member, and then closes the <option>--json-status-fd</option>. The value
+ corresponding to <literal>exit-code</literal> is the exit status of the child, in the usual
+ shell encoding (n if it exited normally with status n, or 128+n if it was killed by signal n).
+ Other members may be added to those objects in future versions of <command>bwrap</command>,
+ and other JSON objects may be added before or after the current objects, so readers must
+ ignore members and objects that they do not understand.
+ </para></listitem>
+ </varlistentry>
+ <varlistentry>
<term><option>--new-session</option></term>
<listitem><para>
Create a new terminal session for the sandbox (calls setsid()). This
diff --git a/ci/builddeps.sh b/ci/builddeps.sh
index 65fa8b4..4accd2e 100755
--- a/ci/builddeps.sh
+++ b/ci/builddeps.sh
@@ -64,6 +64,7 @@ if dpkg-vendor --derives-from Debian; then
libcap-dev \
libselinux1-dev \
libtool \
+ meson \
pkg-config \
python3 \
xsltproc \
@@ -92,6 +93,7 @@ if command -v yum; then
libubsan \
libxslt \
make \
+ meson \
redhat-rpm-config \
rsync \
${NULL+}
diff --git a/completions/bash/bwrap b/completions/bash/bwrap
index 59928a8..e796be3 100644
--- a/completions/bash/bwrap
+++ b/completions/bash/bwrap
@@ -28,6 +28,7 @@ _bwrap() {
# Please keep sorted in LC_ALL=C order
local options_with_args="
$boolean_optons
+ --add-seccomp-fd
--args
--bind
--bind-data
@@ -53,6 +54,7 @@ _bwrap() {
--ro-bind
--seccomp
--setenv
+ --size
--symlink
--sync-fd
--uid
diff --git a/completions/bash/meson.build b/completions/bash/meson.build
new file mode 100644
index 0000000..1dd946f
--- /dev/null
+++ b/completions/bash/meson.build
@@ -0,0 +1,36 @@
+bash_completion_dir = get_option('bash_completion_dir')
+
+if bash_completion_dir == ''
+ bash_completion = dependency(
+ 'bash-completion',
+ version : '>=2.0',
+ required : false,
+ )
+
+ if bash_completion.found()
+ if meson.version().version_compare('>=0.51.0')
+ bash_completion_dir = bash_completion.get_variable(
+ default_value: '',
+ pkgconfig: 'completionsdir',
+ pkgconfig_define: [
+ 'prefix', get_option('prefix'),
+ 'datadir', get_option('prefix') / get_option('datadir'),
+ ],
+ )
+ else
+ bash_completion_dir = bash_completion.get_pkgconfig_variable(
+ 'completionsdir',
+ default: '',
+ define_variable: [
+ 'datadir', get_option('prefix') / get_option('datadir'),
+ ],
+ )
+ endif
+ endif
+endif
+
+if bash_completion_dir == ''
+ bash_completion_dir = get_option('datadir') / 'bash-completion' / 'completions'
+endif
+
+install_data('bwrap', install_dir : bash_completion_dir)
diff --git a/completions/meson.build b/completions/meson.build
new file mode 100644
index 0000000..958c90a
--- /dev/null
+++ b/completions/meson.build
@@ -0,0 +1,7 @@
+if get_option('bash_completion').enabled()
+ subdir('bash')
+endif
+
+if get_option('zsh_completion').enabled()
+ subdir('zsh')
+endif
diff --git a/completions/zsh/_bwrap b/completions/zsh/_bwrap
index 5a9d2fd..f81ffaf 100755..100644
--- a/completions/zsh/_bwrap
+++ b/completions/zsh/_bwrap
@@ -1,11 +1,23 @@
#compdef bwrap
+_bwrap_args_after_perms_size=(
+ # Please sort alphabetically (in LC_ALL=C order) by option name
+ '--tmpfs[Mount new tmpfs on DEST]:mount point for tmpfs:_files -/'
+)
+
_bwrap_args_after_perms=(
# Please sort alphabetically (in LC_ALL=C order) by option name
'--bind-data[Copy from FD to file which is bind-mounted on DEST]: :_guard "[0-9]#" "file descriptor to read content":destination:_files'
'--dir[Create dir at DEST]:directory to create:_files -/'
'--file[Copy from FD to destination DEST]: :_guard "[0-9]#" "file descriptor to read content from":destination:_files'
'--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'
+ '--size[Set size in bytes for next action argument]: :->after_perms_size'
+ '--tmpfs[Mount new tmpfs on DEST]:mount point for tmpfs:_files -/'
+)
+
+_bwrap_args_after_size=(
+ # Please sort alphabetically (in LC_ALL=C order) by option name
+ '--perms[Set permissions for next action argument]: :_guard "[0-7]#" "permissions in octal": :->after_perms_size'
'--tmpfs[Mount new tmpfs on DEST]:mount point for tmpfs:_files -/'
)
@@ -14,6 +26,7 @@ _bwrap_args=(
$_bwrap_args_after_perms
# Please sort alphabetically (in LC_ALL=C order) by option name
+ '--add-seccomp-fd[Load and use seccomp rules from FD]: :_guard "[0-9]#" "file descriptor to read seccomp rules from"'
'--args[Parse NUL-separated args from FD]: :_guard "[0-9]#" "file descriptor with NUL-separated arguments"'
'--as-pid-1[Do not install a reaper process with PID=1]'
'--bind-try[Equal to --bind but ignores non-existent SRC]:source:_files:destination:_files'
@@ -46,6 +59,7 @@ _bwrap_args=(
'--ro-bind[Bind mount the host path SRC readonly on DEST]:source:_files:destination:_files'
'--seccomp[Load and use seccomp rules from FD]: :_guard "[0-9]#" "file descriptor to read seccomp rules from"'
'--setenv[Set an environment variable]:variable to set:_parameters -g "*export*":value of variable: :'
+ '--size[Set size in bytes for next action argument]: :->after_size'
'--symlink[Create symlink at DEST with target SRC]:symlink target:_files:symlink to create:_files:'
'--sync-fd[Keep this fd open while sandbox is running]: :_guard "[0-9]#" "file descriptor to keep open"'
'--uid[Custom uid in the sandbox (requires --unshare-user or --userns)]: :_guard "[0-9]#" "numeric group ID"'
@@ -72,6 +86,14 @@ _bwrap() {
_values -S ' ' 'option' $_bwrap_args_after_perms
;;
+ after_size)
+ _values -S ' ' 'option' $_bwrap_args_after_size
+ ;;
+
+ after_perms_size)
+ _values -S ' ' 'option' $_bwrap_args_after_perms_size
+ ;;
+
caps)
# $ grep -E '#define\sCAP_\w+\s+[0-9]+' /usr/include/linux/capability.h | awk '{print $2}' | xargs echo
local all_caps=(
diff --git a/completions/zsh/meson.build b/completions/zsh/meson.build
new file mode 100644
index 0000000..7bda727
--- /dev/null
+++ b/completions/zsh/meson.build
@@ -0,0 +1,7 @@
+zsh_completion_dir = get_option('zsh_completion_dir')
+
+if zsh_completion_dir == ''
+ zsh_completion_dir = get_option('datadir') / 'zsh' / 'site-functions'
+endif
+
+install_data('_bwrap', install_dir : zsh_completion_dir)
diff --git a/configure.ac b/configure.ac
index 33601e5..ead51fc 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1,5 +1,5 @@
AC_PREREQ([2.63])
-AC_INIT([bubblewrap], [0.5.0], [atomic-devel@projectatomic.io])
+AC_INIT([bubblewrap], [0.6.2], [atomic-devel@projectatomic.io])
AC_CONFIG_HEADER([config.h])
AC_CONFIG_MACRO_DIR([m4])
AC_CONFIG_AUX_DIR([build-aux])
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..0e1f110
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,176 @@
+project(
+ 'bubblewrap',
+ 'c',
+ version : '0.6.2',
+ meson_version : '>=0.49.0',
+ default_options : [
+ 'warning_level=2',
+ ],
+)
+
+cc = meson.get_compiler('c')
+add_project_arguments('-D_GNU_SOURCE', language : 'c')
+common_include_directories = include_directories('.')
+
+# Keep this in sync with ostree, except remove -Wall (part of Meson
+# warning_level 2) and -Werror=declaration-after-statement
+add_project_arguments(
+ cc.get_supported_arguments([
+ '-Werror=shadow',
+ '-Werror=empty-body',
+ '-Werror=strict-prototypes',
+ '-Werror=missing-prototypes',
+ '-Werror=implicit-function-declaration',
+ '-Werror=pointer-arith',
+ '-Werror=init-self',
+ '-Werror=missing-declarations',
+ '-Werror=return-type',
+ '-Werror=overflow',
+ '-Werror=int-conversion',
+ '-Werror=parenthesis',
+ '-Werror=incompatible-pointer-types',
+ '-Werror=misleading-indentation',
+ '-Werror=missing-include-dirs',
+ '-Werror=aggregate-return',
+
+ # Extra warnings specific to bubblewrap
+ '-Werror=switch-default',
+ '-Wswitch-enum',
+
+ # Meson warning_level=2 would do this, but we are not fully
+ # signedness-safe yet
+ '-Wno-sign-compare',
+ '-Wno-error=sign-compare',
+
+ # Deliberately not warning about these, ability to zero-initialize
+ # a struct is a feature
+ '-Wno-missing-field-initializers',
+ '-Wno-error=missing-field-initializers',
+ ]),
+ language : 'c',
+)
+
+if (
+ cc.has_argument('-Werror=format=2')
+ and cc.has_argument('-Werror=format-security')
+ and cc.has_argument('-Werror=format-nonliteral')
+)
+ add_project_arguments([
+ '-Werror=format=2',
+ '-Werror=format-security',
+ '-Werror=format-nonliteral',
+ ], language : 'c')
+endif
+
+bash = find_program('bash', required : false)
+
+if get_option('python') == ''
+ python = find_program('python3')
+else
+ python = find_program(get_option('python'))
+endif
+
+libcap_dep = dependency('libcap', required : true)
+
+selinux_dep = dependency(
+ 'libselinux',
+ version : '>=2.1.9',
+ # if disabled, Meson will behave as though libselinux was not found
+ required : get_option('selinux'),
+)
+
+cdata = configuration_data()
+cdata.set_quoted(
+ 'PACKAGE_STRING',
+ '@0@ @1@'.format(meson.project_name(), meson.project_version()),
+)
+
+if selinux_dep.found()
+ cdata.set('HAVE_SELINUX', 1)
+ if selinux_dep.version().version_compare('>=2.3')
+ cdata.set('HAVE_SELINUX_2_3', 1)
+ endif
+endif
+
+if get_option('require_userns')
+ cdata.set('ENABLE_REQUIRE_USERNS', 1)
+endif
+
+configure_file(
+ output : 'config.h',
+ configuration : cdata,
+)
+
+if meson.is_subproject() and get_option('program_prefix') == ''
+ error('program_prefix option must be set when bwrap is a subproject')
+endif
+
+if get_option('bwrapdir') != ''
+ bwrapdir = get_option('bwrapdir')
+elif meson.is_subproject()
+ bwrapdir = get_option('libexecdir')
+else
+ bwrapdir = get_option('bindir')
+endif
+
+bwrap = executable(
+ get_option('program_prefix') + 'bwrap',
+ [
+ 'bubblewrap.c',
+ 'bind-mount.c',
+ 'network.c',
+ 'utils.c',
+ ],
+ build_rpath : get_option('build_rpath'),
+ install : true,
+ install_dir : bwrapdir,
+ install_rpath : get_option('install_rpath'),
+ dependencies : [selinux_dep, libcap_dep],
+)
+
+manpages_xsl = 'http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl'
+xsltproc = find_program('xsltproc', required : get_option('man'))
+build_man_page = false
+
+if xsltproc.found() and not meson.is_subproject()
+ if run_command([
+ xsltproc, '--nonet', manpages_xsl,
+ ], check : false).returncode() == 0
+ message('Docbook XSL found, man page enabled')
+ build_man_page = true
+ elif get_option('man').enabled()
+ error('Man page requested, but Docbook XSL stylesheets not found')
+ else
+ message('Docbook XSL not found, man page disabled automatically')
+ endif
+endif
+
+if build_man_page
+ custom_target(
+ 'bwrap.1',
+ output : 'bwrap.1',
+ input : 'bwrap.xml',
+ command : [
+ xsltproc,
+ '--nonet',
+ '--stringparam', 'man.output.quietly', '1',
+ '--stringparam', 'funcsynopsis.style', 'ansi',
+ '--stringparam', 'man.th.extra1.suppress', '1',
+ '--stringparam', 'man.authors.section.enabled', '0',
+ '--stringparam', 'man.copyright.section.enabled', '0',
+ '-o', '@OUTPUT@',
+ manpages_xsl,
+ '@INPUT@',
+ ],
+ install : true,
+ install_dir : get_option('mandir') / 'man1',
+ )
+endif
+
+if not meson.is_subproject()
+ subdir('completions')
+endif
+
+if get_option('tests')
+ subdir('tests')
+endif
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 0000000..10a0a20
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1,73 @@
+option(
+ 'bash_completion',
+ type : 'feature',
+ description : 'install bash completion script',
+ value : 'enabled',
+)
+option(
+ 'bash_completion_dir',
+ type : 'string',
+ description : 'install bash completion script in this directory',
+ value : '',
+)
+option(
+ 'bwrapdir',
+ type : 'string',
+ description : 'install bwrap in this directory [default: bindir, or libexecdir in subprojects]',
+)
+option(
+ 'build_rpath',
+ type : 'string',
+ description : 'set a RUNPATH or RPATH on the bwrap executable',
+)
+option(
+ 'install_rpath',
+ type : 'string',
+ description : 'set a RUNPATH or RPATH on the bwrap executable',
+)
+option(
+ 'man',
+ type : 'feature',
+ description : 'generate man pages',
+ value : 'auto',
+)
+option(
+ 'program_prefix',
+ type : 'string',
+ description : 'Prepend string to bwrap executable name, for use with subprojects',
+)
+option(
+ 'python',
+ type : 'string',
+ description : 'Path to Python 3, or empty to use python3',
+)
+option(
+ 'require_userns',
+ type : 'boolean',
+ description : 'require user namespaces by default when installed setuid',
+ value : 'false',
+)
+option(
+ 'selinux',
+ type : 'feature',
+ description : 'enable optional SELINUX support',
+ value : 'auto',
+)
+option(
+ 'tests',
+ type : 'boolean',
+ description : 'build tests',
+ value : 'true',
+)
+option(
+ 'zsh_completion',
+ type : 'feature',
+ description : 'install zsh completion script',
+ value : 'enabled',
+)
+option(
+ 'zsh_completion_dir',
+ type : 'string',
+ description : 'install zsh completion script in this directory',
+ value : '',
+)
diff --git a/release-checklist.md b/release-checklist.md
new file mode 100644
index 0000000..1ab9710
--- /dev/null
+++ b/release-checklist.md
@@ -0,0 +1,18 @@
+bubblewrap release checklist
+============================
+
+* Collect release notes
+* Update version number in `configure.ac` **and** `meson.build`
+* Commit the changes
+* `make distcheck`
+* Do any final smoke-testing, e.g. update a package, install and test it
+* `git evtag sign v$VERSION`
+ * Include the release notes in the tag message
+* `git push --atomic origin main v$VERSION`
+* https://github.com/containers/bubblewrap/releases/new
+ * Fill in the new version's tag in the "Tag version" box
+ * Title: `$VERSION`
+ * Copy the release notes into the description
+ * Upload the tarball that you built with `make distcheck`
+ * Get the `sha256sum` of the tarball and append it to the description
+ * `Publish release`
diff --git a/tests/meson.build b/tests/meson.build
new file mode 100644
index 0000000..87bf709
--- /dev/null
+++ b/tests/meson.build
@@ -0,0 +1,72 @@
+test_programs = [
+ ['test-utils', executable(
+ 'test-utils',
+ 'test-utils.c',
+ '../utils.c',
+ '../utils.h',
+ dependencies : [selinux_dep],
+ include_directories : common_include_directories,
+ )],
+]
+
+executable(
+ 'try-syscall',
+ 'try-syscall.c',
+ override_options: ['b_sanitize=none'],
+)
+
+test_scripts = [
+ 'test-run.sh',
+ 'test-seccomp.py',
+ 'test-specifying-pidns.sh',
+ 'test-specifying-userns.sh',
+]
+
+test_env = environment()
+test_env.set('BWRAP', bwrap.full_path())
+test_env.set('G_TEST_BUILDDIR', meson.current_build_dir() / '..')
+test_env.set('G_TEST_SRCDIR', meson.current_source_dir() / '..')
+
+foreach pair : test_programs
+ name = pair[0]
+ test_program = pair[1]
+ if meson.version().version_compare('>=0.50.0')
+ test(
+ name,
+ test_program,
+ env : test_env,
+ protocol : 'tap',
+ )
+ else
+ test(
+ name,
+ test_program,
+ env : test_env,
+ )
+ endif
+endforeach
+
+foreach test_script : test_scripts
+ if test_script.endswith('.py')
+ interpreter = python
+ else
+ interpreter = bash
+ endif
+
+ if meson.version().version_compare('>=0.50.0')
+ test(
+ test_script,
+ interpreter,
+ args : [files(test_script)],
+ env : test_env,
+ protocol : 'tap',
+ )
+ else
+ test(
+ test_script,
+ interpreter,
+ args : [files(test_script)],
+ env : test_env,
+ )
+ endif
+endforeach
diff --git a/tests/test-run.sh b/tests/test-run.sh
index f25a9bc..3e5e9e6 100755
--- a/tests/test-run.sh
+++ b/tests/test-run.sh
@@ -8,7 +8,7 @@ srcd=$(cd $(dirname "$0") && pwd)
bn=$(basename "$0")
-echo "1..54"
+echo "1..57"
# Test help
${BWRAP} --help > help.txt
@@ -39,9 +39,16 @@ for ALT in "" "--unshare-user-try" "--unshare-pid" "--unshare-user-try --unshare
CAP=""
fi
- if ! ${is_uidzero} && $RUN $CAP $ALT --unshare-net --proc /proc --bind /etc/shadow /tmp/foo cat /etc/shadow; then
+ if ! cat /etc/shadow >/dev/null &&
+ $RUN $CAP $ALT --unshare-net --proc /proc --bind /etc/shadow /tmp/foo cat /tmp/foo; then
+ assert_not_reached Could read /etc/shadow via /tmp/foo bind-mount
+ fi
+
+ if ! cat /etc/shadow >/dev/null &&
+ $RUN $CAP $ALT --unshare-net --proc /proc --bind /etc/shadow /tmp/foo cat /etc/shadow; then
assert_not_reached Could read /etc/shadow
fi
+
echo "ok - cannot read /etc/shadow with $ALT"
# Unreadable dir
if [ "x$UNREADABLE" != "x" ]; then
@@ -88,7 +95,7 @@ done
echo "ok namespace id info in info and json-status fd"
-if ! which strace >/dev/null 2>/dev/null || ! strace -h | grep -v -e default | grep -e fault >/dev/null; then
+if ! command -v strace >/dev/null || ! strace -h | grep -v -e default | grep -e fault >/dev/null; then
echo "ok - # SKIP no strace fault injection"
else
! strace -o /dev/null -f -e trace=prctl -e fault=prctl:when=39 $RUN --die-with-parent --json-status-fd 42 true 42>json-status.json
@@ -398,6 +405,29 @@ $RUN \
assert_file_has_content dir-permissions '^755$'
echo "ok - tmpfs has expected permissions"
+# 1048576 = 1 MiB
+$RUN \
+ --size 1048576 --tmpfs "$(pwd -P)" \
+ df --output=size --block-size=1K "$(pwd -P)" > dir-size
+assert_file_has_content dir-size '^ *1024$'
+$RUN \
+ --size 1048576 --perms 01777 --tmpfs "$(pwd -P)" \
+ stat -c '%a' "$(pwd -P)" > dir-permissions
+assert_file_has_content dir-permissions '^1777$'
+$RUN \
+ --size 1048576 --perms 01777 --tmpfs "$(pwd -P)" \
+ df --output=size --block-size=1K "$(pwd -P)" > dir-size
+assert_file_has_content dir-size '^ *1024$'
+$RUN \
+ --perms 01777 --size 1048576 --tmpfs "$(pwd -P)" \
+ stat -c '%a' "$(pwd -P)" > dir-permissions
+assert_file_has_content dir-permissions '^1777$'
+$RUN \
+ --perms 01777 --size 1048576 --tmpfs "$(pwd -P)" \
+ df --output=size --block-size=1K "$(pwd -P)" > dir-size
+assert_file_has_content dir-size '^ *1024$'
+echo "ok - tmpfs has expected size"
+
$RUN \
--file 0 /tmp/file \
stat -c '%a' /tmp/file < /dev/null > file-permissions
@@ -424,6 +454,40 @@ $RUN \
assert_file_has_content file-permissions '^640$'
echo "ok - files have expected permissions"
+if $RUN --size 0 --tmpfs /tmp/a true; then
+ assert_not_reached Zero tmpfs size allowed
+fi
+if $RUN --size 123bogus --tmpfs /tmp/a true; then
+ assert_not_reached Bogus tmpfs size allowed
+fi
+if $RUN --size '' --tmpfs /tmp/a true; then
+ assert_not_reached Empty tmpfs size allowed
+fi
+if $RUN --size -12345678 --tmpfs /tmp/a true; then
+ assert_not_reached Negative tmpfs size allowed
+fi
+if $RUN --size ' -12345678' --tmpfs /tmp/a true; then
+ assert_not_reached Negative tmpfs size with space allowed
+fi
+# This is 2^64
+if $RUN --size 18446744073709551616 --tmpfs /tmp/a true; then
+ assert_not_reached Overflowing tmpfs size allowed
+fi
+# This is 2^63 + 1; note that the current max size is SIZE_MAX/2
+if $RUN --size 9223372036854775809 --tmpfs /tmp/a true; then
+ assert_not_reached Too-large tmpfs size allowed
+fi
+echo "ok - bogus tmpfs size not allowed"
+
+if $RUN --perms 0640 --perms 0640 --tmpfs /tmp/a true; then
+ assert_not_reached Multiple perms options allowed
+fi
+if $RUN --size 1048576 --size 1048576 --tmpfs /tmp/a true; then
+ assert_not_reached Multiple perms options allowed
+fi
+echo "ok - --perms and --size only allowed once"
+
+
FOO= BAR=baz $RUN --setenv FOO bar sh -c 'echo "$FOO$BAR"' > stdout
assert_file_has_content stdout barbaz
FOO=wrong BAR=baz $RUN --setenv FOO bar sh -c 'echo "$FOO$BAR"' > stdout
diff --git a/tests/test-specifying-pidns.sh b/tests/test-specifying-pidns.sh
index b0db6d0..de38b97 100755
--- a/tests/test-specifying-pidns.sh
+++ b/tests/test-specifying-pidns.sh
@@ -16,6 +16,7 @@ else
while ! test -f sandbox-pidns; do sleep 1; done
SANDBOX1PID=$(extract_child_pid info.json)
+ ASAN_OPTIONS=detect_leaks=0 LSAN_OPTIONS=detect_leaks=0 \
$RUN --userns 11 --pidns 12 readlink /proc/self/ns/pid > sandbox2-pidns 11< /proc/$SANDBOX1PID/ns/user 12< /proc/$SANDBOX1PID/ns/pid
echo foo > donepipe
diff --git a/tests/test-utils.c b/tests/test-utils.c
index 23d6bcd..41874a1 100644
--- a/tests/test-utils.c
+++ b/tests/test-utils.c
@@ -25,6 +25,8 @@
/* A small implementation of TAP */
static unsigned int test_number = 0;
+
+__attribute__((format(printf, 1, 2)))
static void
ok (const char *format, ...)
{
@@ -199,8 +201,8 @@ test_has_path_prefix (void)
}
int
-main (int argc,
- char **argv)
+main (int argc UNUSED,
+ char **argv UNUSED)
{
setvbuf (stdout, NULL, _IONBF, 0);
test_n_elements ();
diff --git a/tests/try-syscall.c b/tests/try-syscall.c
index df35054..6f2f112 100644
--- a/tests/try-syscall.c
+++ b/tests/try-syscall.c
@@ -24,11 +24,11 @@
#include <sys/types.h>
#if defined(_MIPS_SIM)
-# if _MIPS_SIM == _MIPS_SIM_ABI32
+# if _MIPS_SIM == _ABIO32
# define MISSING_SYSCALL_BASE 4000
-# elif _MIPS_SIM == _MIPS_SIM_ABI64
+# elif _MIPS_SIM == _ABI64
# define MISSING_SYSCALL_BASE 5000
-# elif _MIPS_SIM == _MIPS_SIM_NABI32
+# elif _MIPS_SIM == _ABIN32
# define MISSING_SYSCALL_BASE 6000
# else
# error "Unknown MIPS ABI"
@@ -71,6 +71,10 @@
*/
#define WRONG_POINTER ((char *) 1)
+#ifndef PR_GET_CHILD_SUBREAPER
+#define PR_GET_CHILD_SUBREAPER 37
+#endif
+
int
main (int argc, char **argv)
{
diff --git a/tests/use-as-subproject/.gitignore b/tests/use-as-subproject/.gitignore
new file mode 100644
index 0000000..371a7d9
--- /dev/null
+++ b/tests/use-as-subproject/.gitignore
@@ -0,0 +1,2 @@
+/_build/
+/subprojects/
diff --git a/tests/use-as-subproject/README b/tests/use-as-subproject/README
new file mode 100644
index 0000000..97d2e88
--- /dev/null
+++ b/tests/use-as-subproject/README
@@ -0,0 +1,3 @@
+This is a simple example of a project that uses bubblewrap as a
+subproject. The intention is that if this project can successfully build
+bubblewrap as a subproject, then so could Flatpak.
diff --git a/tests/use-as-subproject/assert-correct-rpath.py b/tests/use-as-subproject/assert-correct-rpath.py
new file mode 100755
index 0000000..10b0947
--- /dev/null
+++ b/tests/use-as-subproject/assert-correct-rpath.py
@@ -0,0 +1,26 @@
+#!/usr/bin/python3
+# Copyright 2022 Collabora Ltd.
+# SPDX-License-Identifier: LGPL-2.0-or-later
+
+import subprocess
+import sys
+
+if __name__ == '__main__':
+ completed = subprocess.run(
+ ['objdump', '-T', '-x', sys.argv[1]],
+ stdout=subprocess.PIPE,
+ )
+ stdout = completed.stdout
+ assert stdout is not None
+ seen_rpath = False
+
+ for line in stdout.splitlines():
+ words = line.strip().split()
+
+ if words and words[0] in (b'RPATH', b'RUNPATH'):
+ print(line.decode(errors='backslashreplace'))
+ assert len(words) == 2, words
+ assert words[1] == b'${ORIGIN}/../lib', words
+ seen_rpath = True
+
+ assert seen_rpath
diff --git a/tests/use-as-subproject/config.h b/tests/use-as-subproject/config.h
new file mode 100644
index 0000000..4a99af4
--- /dev/null
+++ b/tests/use-as-subproject/config.h
@@ -0,0 +1 @@
+#error Should not use superproject config.h to compile bubblewrap
diff --git a/tests/use-as-subproject/dummy-config.h.in b/tests/use-as-subproject/dummy-config.h.in
new file mode 100644
index 0000000..1d1e56a
--- /dev/null
+++ b/tests/use-as-subproject/dummy-config.h.in
@@ -0,0 +1 @@
+#error Should not use superproject generated config.h to compile bubblewrap
diff --git a/tests/use-as-subproject/meson.build b/tests/use-as-subproject/meson.build
new file mode 100644
index 0000000..bc4781c
--- /dev/null
+++ b/tests/use-as-subproject/meson.build
@@ -0,0 +1,20 @@
+project(
+ 'use-bubblewrap-as-subproject',
+ 'c',
+ version : '0',
+ meson_version : '>=0.49.0',
+)
+
+configure_file(
+ output : 'config.h',
+ input : 'dummy-config.h.in',
+ configuration : configuration_data(),
+)
+
+subproject(
+ 'bubblewrap',
+ default_options : [
+ 'install_rpath=${ORIGIN}/../lib',
+ 'program_prefix=not-flatpak-',
+ ],
+)
diff --git a/utils.c b/utils.c
index 117da31..693273b 100644
--- a/utils.c
+++ b/utils.c
@@ -82,7 +82,7 @@ die (const char *format, ...)
}
void
-die_unless_label_valid (const char *label)
+die_unless_label_valid (UNUSED const char *label)
{
#ifdef HAVE_SELINUX
if (is_selinux_enabled () == 1)
@@ -854,7 +854,7 @@ pivot_root (const char * new_root, const char * put_old)
}
char *
-label_mount (const char *opt, const char *mount_label)
+label_mount (const char *opt, UNUSED const char *mount_label)
{
#ifdef HAVE_SELINUX
if (mount_label)
@@ -871,7 +871,7 @@ label_mount (const char *opt, const char *mount_label)
}
int
-label_create_file (const char *file_label)
+label_create_file (UNUSED const char *file_label)
{
#ifdef HAVE_SELINUX
if (is_selinux_enabled () > 0 && file_label)
@@ -881,7 +881,7 @@ label_create_file (const char *file_label)
}
int
-label_exec (const char *exec_label)
+label_exec (UNUSED const char *exec_label)
{
#ifdef HAVE_SELINUX
if (is_selinux_enabled () > 0 && exec_label)
diff --git a/utils.h b/utils.h
index 55a9585..37d8c7c 100644
--- a/utils.h
+++ b/utils.h
@@ -48,6 +48,10 @@ typedef int bool;
#define PIPE_READ_END 0
#define PIPE_WRITE_END 1
+#ifndef PR_SET_CHILD_SUBREAPER
+#define PR_SET_CHILD_SUBREAPER 36
+#endif
+
void warn (const char *format,
...) __attribute__((format (printf, 1, 2)));
void die_with_error (const char *format,