diff options
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) @@ -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)"); @@ -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-', + ], +) @@ -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) @@ -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, |