Hello community, here is the log from the commit of package bubblewrap for openSUSE:Factory checked in at 2017-10-13 14:09:16 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/bubblewrap (Old) and /work/SRC/openSUSE:Factory/.bubblewrap.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "bubblewrap" Fri Oct 13 14:09:16 2017 rev:4 rq:532853 version:0.2.0 Changes: -------- --- /work/SRC/openSUSE:Factory/bubblewrap/bubblewrap.changes 2017-09-21 12:32:47.996533808 +0200 +++ /work/SRC/openSUSE:Factory/.bubblewrap.new/bubblewrap.changes 2017-10-13 14:09:17.578155078 +0200 @@ -1,0 +2,17 @@ +Mon Oct 9 17:53:37 UTC 2017 - [email protected] + +- update to version 0.2.0 + - bwrap now automatically detects the new + user namespace restrictions in Red Hat Enterprise Linux 7.4: + bubblewrap: check for max_user_namespaces == 0. + - The most notable features are new arguments --as-pid1, and + --cap-add/--cap-drop. These were added for running systemd (or in general a + "full" init system) inside bubblewrap. But the capability options are also + useful for unprivileged callers to potentially retain capbilities inside the + sandbox (for example CAP_NET_ADMIN), when user namespaces are enabled. + Conversely, privileged callers (uid 0) can conversely drop capabilities (without + user namespaces). Contributed by Giuseppe Scrivano. + - With --dev, add /dev/fd and /dev/core symlinks + which should improve compatibility with older software. + +------------------------------------------------------------------- Old: ---- v0.1.8.tar.gz New: ---- v0.2.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ bubblewrap.spec ++++++ --- /var/tmp/diff_new_pack.oNiZK5/_old 2017-10-13 14:09:18.454116559 +0200 +++ /var/tmp/diff_new_pack.oNiZK5/_new 2017-10-13 14:09:18.458116383 +0200 @@ -17,7 +17,7 @@ Name: bubblewrap -Version: 0.1.8 +Version: 0.2.0 Release: 0 Summary: Core execution tool for unprivileged containers License: LGPL-2.0+ ++++++ v0.1.8.tar.gz -> v0.2.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bubblewrap-0.1.8/.papr.yml new/bubblewrap-0.2.0/.papr.yml --- old/bubblewrap-0.1.8/.papr.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/bubblewrap-0.2.0/.papr.yml 2017-10-09 16:11:41.000000000 +0200 @@ -0,0 +1,28 @@ +context: centos7 +required: true + +branches: + - master + - auto + - try + +host: + distro: centos/7/atomic + +tests: + - env BWRAP_SUID=true ./ci/papr.sh centos:7 + +timeout: 30m + +--- + +inherit: true + +host: + distro: fedora/26/atomic + +context: f26-sanitizer +required: true + +tests: + - env CFLAGS='-g -Og -fsanitize=undefined -fsanitize=address -O2 -Wp,-D_FORTIFY_SOURCE=2' ./ci/papr.sh registry.fedoraproject.org/fedora:26 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bubblewrap-0.1.8/.redhat-ci.yml new/bubblewrap-0.2.0/.redhat-ci.yml --- old/bubblewrap-0.1.8/.redhat-ci.yml 2017-03-28 16:26:53.000000000 +0200 +++ new/bubblewrap-0.2.0/.redhat-ci.yml 1970-01-01 01:00:00.000000000 +0100 @@ -1,25 +0,0 @@ -context: centos7 -required: true - -branches: - - master - - auto - - try - -host: - distro: centos/7/atomic - -tests: - - env BWRAP_SUID=true ./ci/redhat-ci.sh centos:7 - -timeout: 30m - ---- - -inherit: true - -context: f25-asan-ubsan -required: true - -tests: - - env CFLAGS='-g -Og -fsanitize=undefined -fsanitize=address' ./ci/redhat-ci.sh fedora:25 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bubblewrap-0.1.8/Makefile.am new/bubblewrap-0.2.0/Makefile.am --- old/bubblewrap-0.1.8/Makefile.am 2017-03-28 16:26:53.000000000 +0200 +++ new/bubblewrap-0.2.0/Makefile.am 2017-10-09 16:11:41.000000000 +0200 @@ -29,10 +29,13 @@ include Makefile-docs.am +LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) $(top_srcdir)/build-aux/tap-driver.sh +LOG_COMPILER = TESTS = tests/test-run.sh TESTS_ENVIRONMENT = BWRAP=$(abs_top_builddir)/test-bwrap EXTRA_DIST += $(TESTS) +EXTRA_DIST += tests/libtest-core.sh if ENABLE_BASH_COMPLETION bashcompletiondir = $(BASH_COMPLETION_DIR) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bubblewrap-0.1.8/README.md new/bubblewrap-0.2.0/README.md --- old/bubblewrap-0.1.8/README.md 2017-03-28 16:26:53.000000000 +0200 +++ new/bubblewrap-0.2.0/README.md 2017-10-09 16:11:41.000000000 +0200 @@ -55,6 +55,7 @@ - [Flatpak](http://www.flatpak.org) - [rpm-ostree unprivileged](https://github.com/projectatomic/rpm-ostree/pull/209) + - [bwrap-oci](https://github.com/projectatomic/bwrap-oci) We would also like to see this be available in Kubernetes/OpenShift clusters. Having the ability for unprivileged users to use container @@ -173,7 +174,3 @@ The name bubblewrap was chosen to convey that this tool runs as the parent of the application (so wraps it in some sense) and creates a protective layer (the sandbox) around it. - - - -(Bubblewrap cat by [dancing_stupidity](https://www.flickr.com/photos/27549668@N03/)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bubblewrap-0.1.8/bind-mount.c new/bubblewrap-0.2.0/bind-mount.c --- old/bubblewrap-0.1.8/bind-mount.c 2017-03-28 16:26:53.000000000 +0200 +++ new/bubblewrap-0.2.0/bind-mount.c 2017-10-09 16:11:41.000000000 +0200 @@ -389,7 +389,7 @@ if (src) { - if (mount (src, dest, NULL, MS_MGC_VAL | MS_BIND | (recursive ? MS_REC : 0), NULL) != 0) + if (mount (src, dest, NULL, MS_BIND | (recursive ? MS_REC : 0), NULL) != 0) return 1; } @@ -411,7 +411,7 @@ new_flags = current_flags | (devices ? 0 : MS_NODEV) | MS_NOSUID | (readonly ? MS_RDONLY : 0); if (new_flags != current_flags && mount ("none", resolved_dest, - NULL, MS_MGC_VAL | MS_BIND | MS_REMOUNT | new_flags, NULL) != 0) + NULL, MS_BIND | MS_REMOUNT | new_flags, NULL) != 0) return 3; /* We need to work around the fact that a bind mount does not apply the flags, so we need to manually @@ -426,7 +426,7 @@ new_flags = current_flags | (devices ? 0 : MS_NODEV) | MS_NOSUID | (readonly ? MS_RDONLY : 0); if (new_flags != current_flags && mount ("none", mount_tab[i].mountpoint, - NULL, MS_MGC_VAL | MS_BIND | MS_REMOUNT | new_flags, NULL) != 0) + NULL, MS_BIND | MS_REMOUNT | new_flags, NULL) != 0) { /* 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. */ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bubblewrap-0.1.8/bubblewrap.c new/bubblewrap-0.2.0/bubblewrap.c --- old/bubblewrap-0.1.8/bubblewrap.c 2017-03-28 16:26:53.000000000 +0200 +++ new/bubblewrap-0.2.0/bubblewrap.c 2017-10-09 16:11:41.000000000 +0200 @@ -47,14 +47,15 @@ static gid_t real_gid; static uid_t overflow_uid; static gid_t overflow_gid; -static bool is_privileged; +static bool is_privileged; /* See acquire_privs() */ static const char *argv0; static const char *host_tty_dev; static int proc_fd = -1; -static char *opt_exec_label = NULL; -static char *opt_file_label = NULL; +static const char *opt_exec_label = NULL; +static const char *opt_file_label = NULL; +static bool opt_as_pid_1; -char *opt_chdir_path = NULL; +const char *opt_chdir_path = NULL; bool opt_unshare_user = FALSE; bool opt_unshare_user_try = FALSE; bool opt_unshare_pid = FALSE; @@ -70,9 +71,13 @@ gid_t opt_sandbox_gid = -1; int opt_sync_fd = -1; int opt_block_fd = -1; +int opt_userns_block_fd = -1; int opt_info_fd = -1; int opt_seccomp_fd = -1; -char *opt_sandbox_hostname = NULL; +const char *opt_sandbox_hostname = NULL; + +#define CAP_TO_MASK_0(x) (1L << ((x) & 31)) +#define CAP_TO_MASK_1(x) CAP_TO_MASK_0(x - 32) typedef enum { SETUP_BIND_MOUNT, @@ -112,6 +117,7 @@ struct _LockFile { const char *path; + int fd; LockFile *next; }; @@ -180,7 +186,7 @@ fprintf (out, " --help Print this help\n" " --version Print version\n" - " --args FD Parse nul-separated args from FD\n" + " --args FD Parse NUL-separated args from FD\n" " --unshare-all Unshare every namespace we support by default\n" " --share-net Retain the network namespace (can only combine with --unshare-all)\n" " --unshare-user Create new user namespace (may be automatically implied if not setuid)\n" @@ -192,7 +198,7 @@ " --unshare-cgroup Create new cgroup namespace\n" " --unshare-cgroup-try Create new cgroup namespace if possible else continue by skipping it\n" " --uid UID Custom uid in the sandbox (requires --unshare-user)\n" - " --gid GID Custon gid in the sandbox (requires --unshare-user)\n" + " --gid GID Custom gid in the sandbox (requires --unshare-user)\n" " --hostname NAME Custom hostname in the sandbox (requires --unshare-uts)\n" " --chdir DIR Change directory to DIR\n" " --setenv VAR VALUE Set an environment variable\n" @@ -202,23 +208,27 @@ " --bind SRC DEST Bind mount the host path SRC on DEST\n" " --dev-bind SRC DEST Bind mount the host path SRC on DEST, allowing device access\n" " --ro-bind SRC DEST Bind mount the host path SRC readonly on DEST\n" - " --remount-ro DEST Remount DEST as readonly, it doesn't recursively remount\n" - " --exec-label LABEL Exec Label for the sandbox\n" + " --remount-ro DEST Remount DEST as readonly; does not recursively remount\n" + " --exec-label LABEL Exec label for the sandbox\n" " --file-label LABEL File label for temporary sandbox content\n" - " --proc DEST Mount procfs on DEST\n" + " --proc DEST Mount new procfs on DEST\n" " --dev DEST Mount new dev on DEST\n" " --tmpfs DEST Mount new tmpfs on DEST\n" " --mqueue DEST Mount new mqueue on DEST\n" " --dir DEST Create dir at DEST\n" - " --file FD DEST Copy from FD to dest DEST\n" + " --file FD DEST Copy from FD to destination DEST\n" " --bind-data FD DEST Copy from FD to file which is bind-mounted on DEST\n" " --ro-bind-data FD DEST Copy from FD to file which is readonly bind-mounted on DEST\n" " --symlink SRC DEST Create symlink at DEST with target SRC\n" " --seccomp FD Load and use seccomp rules from FD\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" " --new-session Create a new terminal session\n" " --die-with-parent Kills with SIGKILL child process (COMMAND) when bwrap or bwrap's parent dies.\n" + " --as-pid-1 Do not install a reaper process with PID=1\n" + " --cap-add CAP Add cap CAP when running as privileged user\n" + " --cap-drop CAP Drop cap CAP when running as privileged user\n" ); exit (ecode); } @@ -409,6 +419,7 @@ die_with_error ("Unable to lock file %s", lock->path); /* Keep fd open to hang on to lock */ + lock->fd = fd; } /* Optionally bind our lifecycle to that of the caller */ @@ -445,11 +456,29 @@ } } + /* Close FDs. */ + for (lock = lock_files; lock != NULL; lock = lock->next) + { + if (lock->fd >= 0) + { + close (lock->fd); + lock->fd = -1; + } + } + return initial_exit_status; } +#define CAP_TO_MASK_0(x) (1L << ((x) & 31)) +#define CAP_TO_MASK_1(x) CAP_TO_MASK_0(x - 32) + +/* Set if --cap-add or --cap-drop were used */ +static bool opt_cap_add_or_drop_used; +/* The capability set we'll target, used if above is true */ +static uint32_t requested_caps[2] = {0, 0}; + /* low 32bit caps needed */ -#define REQUIRED_CAPS_0 (CAP_TO_MASK (CAP_SYS_ADMIN) | CAP_TO_MASK (CAP_SYS_CHROOT) | CAP_TO_MASK (CAP_NET_ADMIN) | CAP_TO_MASK (CAP_SETUID) | CAP_TO_MASK (CAP_SETGID)) +#define REQUIRED_CAPS_0 (CAP_TO_MASK_0 (CAP_SYS_ADMIN) | CAP_TO_MASK_0 (CAP_SYS_CHROOT) | CAP_TO_MASK_0 (CAP_NET_ADMIN) | CAP_TO_MASK_0 (CAP_SETUID) | CAP_TO_MASK_0 (CAP_SETGID)) /* high 32bit caps needed */ #define REQUIRED_CAPS_1 0 @@ -471,13 +500,43 @@ } static void -drop_all_caps (void) +drop_all_caps (bool keep_requested_caps) { struct __user_cap_header_struct hdr = { _LINUX_CAPABILITY_VERSION_3, 0 }; struct __user_cap_data_struct data[2] = { { 0 } }; + if (keep_requested_caps) + { + /* Avoid calling capset() unless we need to; currently + * systemd-nspawn at least is known to install a seccomp + * policy denying capset() for dubious reasons. + * <https://github.com/projectatomic/bubblewrap/pull/122> + */ + if (!opt_cap_add_or_drop_used && real_uid == 0) + { + assert (!is_privileged); + return; + } + data[0].effective = requested_caps[0]; + data[0].permitted = requested_caps[0]; + data[0].inheritable = requested_caps[0]; + data[1].effective = requested_caps[1]; + data[1].permitted = requested_caps[1]; + data[1].inheritable = requested_caps[1]; + } + if (capset (&hdr, data) < 0) - die_with_error ("capset failed"); + { + /* While the above logic ensures we don't call capset() for the primary + * process unless configured to do so, we still try to drop privileges for + * the init process unconditionally. Since due to the systemd seccomp + * filter that will fail, let's just ignore it. + */ + if (errno == EPERM && real_uid == 0 && !is_privileged) + return; + else + die_with_error ("capset failed"); + } } static bool @@ -492,8 +551,12 @@ return data[0].permitted != 0 || data[1].permitted != 0; } +/* Most of the code here is used both to add caps to the ambient capabilities + * and drop caps from the bounding set. Handle both cases here and add + * drop_cap_bounding_set/set_ambient_capabilities wrappers to facilitate its usage. + */ static void -drop_cap_bounding_set (void) +prctl_caps (uint32_t *caps, bool do_cap_bounding, bool do_set_ambient) { unsigned long cap; @@ -504,14 +567,56 @@ * https://github.com/projectatomic/bubblewrap/pull/175#issuecomment-278051373 * https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/security/commoncap.c?id=160da84dbb39443fdade7151bc63a88f8e953077 */ - for (cap = 0; cap <= 63; cap++) + for (cap = 0; cap <= CAP_LAST_CAP; cap++) { - int res = prctl (PR_CAPBSET_DROP, cap, 0, 0, 0); - if (res == -1 && !(errno == EINVAL || errno == EPERM)) - die_with_error ("Dropping capability %ld from bounds", cap); + bool keep = FALSE; + if (cap < 32) + { + if (CAP_TO_MASK_0 (cap) & caps[0]) + keep = TRUE; + } + else + { + if (CAP_TO_MASK_1 (cap) & caps[1]) + keep = TRUE; + } + + if (keep && do_set_ambient) + { + int res = prctl (PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, cap, 0, 0); + if (res == -1 && !(errno == EINVAL || errno == EPERM)) + die_with_error ("Adding ambient capability %ld", cap); + } + + if (!keep && do_cap_bounding) + { + int res = prctl (PR_CAPBSET_DROP, cap, 0, 0, 0); + if (res == -1 && !(errno == EINVAL || errno == EPERM)) + die_with_error ("Dropping capability %ld from bounds", cap); + } } } +static void +drop_cap_bounding_set (bool drop_all) +{ + if (!drop_all) + prctl_caps (requested_caps, TRUE, FALSE); + else + { + uint32_t no_caps[2] = {0, 0}; + prctl_caps (no_caps, TRUE, FALSE); + } +} + +static void +set_ambient_capabilities (void) +{ + if (is_privileged) + return; + prctl_caps (requested_caps, FALSE, TRUE); +} + /* This acquires the privileges that the bwrap will need it to work. * If bwrap is not setuid, then this does nothing, and it relies on * unprivileged user namespaces to be used. This case is @@ -536,11 +641,10 @@ /* Are we setuid ? */ if (real_uid != euid) { - if (euid == 0) - is_privileged = TRUE; - else + if (euid != 0) die ("Unexpected setuid user %d, should be 0", euid); + is_privileged = TRUE; /* We want to keep running as euid=0 until at the clone() * operation because doing so will make the user namespace be * owned by root, which makes it not ptrace:able by the user as @@ -560,8 +664,8 @@ if (new_fsuid != real_uid) die ("Unable to set fsuid (was %d)", (int)new_fsuid); - /* We never need capabilies after execve(), so lets drop everything from the bounding set */ - drop_cap_bounding_set (); + /* We never need capabilities after execve(), so lets drop everything from the bounding set */ + drop_cap_bounding_set (TRUE); /* Keep only the required capabilities for setup */ set_required_caps (); @@ -573,6 +677,21 @@ don't support anymore */ die ("Unexpected capabilities but not setuid, old file caps config?"); } + else if (real_uid == 0) + { + /* If our uid is 0, default to inheriting all caps; the caller + * can drop them via --cap-drop. This is used by at least rpm-ostree. + * Note this needs to happen before the argument parsing of --cap-drop. + */ + struct __user_cap_header_struct hdr = { _LINUX_CAPABILITY_VERSION_3, 0 }; + struct __user_cap_data_struct data[2] = { { 0 } }; + + if (capget (&hdr, data) < 0) + die_with_error ("capget (for uid == 0) failed"); + + requested_caps[0] = data[0].effective; + requested_caps[1] = data[1].effective; + } /* Else, we try unprivileged user namespaces */ } @@ -583,7 +702,7 @@ { /* If we're in a new user namespace, we got back the bounding set, clear it again */ if (opt_unshare_user) - drop_cap_bounding_set (); + drop_cap_bounding_set (FALSE); if (!is_privileged) return; @@ -599,17 +718,16 @@ set_required_caps (); } +/* Call setuid() and use capset() to adjust capabilities */ static void -drop_privs (void) +drop_privs (bool keep_requested_caps) { - if (!is_privileged) - return; - + assert (!keep_requested_caps || !is_privileged); /* Drop root uid */ - if (setuid (opt_sandbox_uid) < 0) + if (getuid () == 0 && setuid (opt_sandbox_uid) < 0) die_with_error ("unable to drop root uid"); - drop_all_caps (); + drop_all_caps (keep_requested_caps); } static char * @@ -777,20 +895,20 @@ break; case PRIV_SEP_OP_PROC_MOUNT: - if (mount ("proc", arg1, "proc", MS_MGC_VAL | MS_NOSUID | MS_NOEXEC | MS_NODEV, NULL) != 0) + if (mount ("proc", arg1, "proc", MS_NOSUID | MS_NOEXEC | MS_NODEV, NULL) != 0) die_with_error ("Can't mount proc on %s", arg1); break; case PRIV_SEP_OP_TMPFS_MOUNT: { cleanup_free char *opt = label_mount ("mode=0755", opt_file_label); - if (mount ("tmpfs", arg1, "tmpfs", MS_MGC_VAL | MS_NOSUID | MS_NODEV, opt) != 0) + if (mount ("tmpfs", arg1, "tmpfs", MS_NOSUID | MS_NODEV, opt) != 0) die_with_error ("Can't mount tmpfs on %s", arg1); break; } case PRIV_SEP_OP_DEVPTS_MOUNT: - if (mount ("devpts", arg1, "devpts", MS_MGC_VAL | MS_NOSUID | MS_NOEXEC, + if (mount ("devpts", arg1, "devpts", MS_NOSUID | MS_NOEXEC, "newinstance,ptmxmode=0666,mode=620") != 0) die_with_error ("Can't mount devpts on %s", arg1); break; @@ -939,6 +1057,16 @@ die_with_error ("Can't create symlink %s/%s", op->dest, stdionodes[i]); } + /* /dev/fd and /dev/core - legacy, but both nspawn and docker do these */ + { cleanup_free char *dev_fd = strconcat (dest, "/fd"); + if (symlink ("/proc/self/fd", dev_fd) < 0) + die_with_error ("Can't create symlink %s", dev_fd); + } + { cleanup_free char *dev_core = strconcat (dest, "/core"); + if (symlink ("/proc/kcore", dev_core) < 0) + die_with_error ("Can't create symlink %s", dev_core); + } + { cleanup_free char *pts = strconcat (dest, "/pts"); cleanup_free char *ptmx = strconcat (dest, "/ptmx"); @@ -1030,6 +1158,8 @@ close (op->fd); + assert (dest != NULL); + if (ensure_file (dest, 0666) != 0) die_with_error ("Can't create file at %s", op->dest); @@ -1046,11 +1176,13 @@ break; case SETUP_MAKE_SYMLINK: + assert (op->source != NULL); /* guaranteed by the constructor */ if (symlink (op->source, dest) != 0) die_with_error ("Can't make symlink at %s", op->dest); break; case SETUP_SET_HOSTNAME: + assert (op->dest != NULL); /* guaranteed by the constructor */ privileged_op (privileged_op_socket, PRIV_SEP_OP_SET_HOSTNAME, 0, op->dest, NULL); @@ -1150,14 +1282,14 @@ } static void -parse_args_recurse (int *argcp, - char ***argvp, - bool in_file, - int *total_parsed_argc_p) +parse_args_recurse (int *argcp, + const char ***argvp, + bool in_file, + int *total_parsed_argc_p) { SetupOp *op; int argc = *argcp; - char **argv = *argvp; + const char **argv = *argvp; /* I can't imagine a case where someone wants more than this. * If you do...you should be able to pass multiple files * via a single tmpfs and linking them there, etc. @@ -1189,11 +1321,11 @@ { int the_fd; char *endptr; - char *data, *p; - char *data_end; + char *data = NULL; + const char *p, *data_end; size_t data_len; - cleanup_free char **data_argv = NULL; - char **data_argv_copy; + cleanup_free const char **data_argv = NULL; + const char **data_argv_copy; int data_argc; int i; @@ -1210,6 +1342,7 @@ data = load_file_data (the_fd, &data_len); if (data == NULL) die_with_error ("Can't read --args data"); + (void) close (the_fd); data_end = data + data_len; data_argc = 0; @@ -1542,6 +1675,23 @@ argv += 1; argc -= 1; } + else if (strcmp (arg, "--userns-block-fd") == 0) + { + int the_fd; + char *endptr; + + if (argc < 2) + die ("--userns-block-fd takes an argument"); + + the_fd = strtol (argv[1], &endptr, 10); + if (argv[1][0] == 0 || endptr[0] != 0 || the_fd < 0) + die ("Invalid fd: %s", argv[1]); + + opt_userns_block_fd = the_fd; + + argv += 1; + argc -= 1; + } else if (strcmp (arg, "--info-fd") == 0) { int the_fd; @@ -1652,6 +1802,62 @@ { opt_die_with_parent = TRUE; } + else if (strcmp (arg, "--as-pid-1") == 0) + { + opt_as_pid_1 = TRUE; + } + else if (strcmp (arg, "--cap-add") == 0) + { + cap_value_t cap; + if (argc < 2) + die ("--cap-add takes an argument"); + + opt_cap_add_or_drop_used = TRUE; + + if (strcasecmp (argv[1], "ALL") == 0) + { + requested_caps[0] = requested_caps[1] = 0xFFFFFFFF; + } + else + { + if (cap_from_name (argv[1], &cap) < 0) + die ("unknown cap: %s", argv[1]); + + if (cap < 32) + requested_caps[0] |= CAP_TO_MASK_0 (cap); + else + requested_caps[1] |= CAP_TO_MASK_1 (cap - 32); + } + + argv += 1; + argc -= 1; + } + else if (strcmp (arg, "--cap-drop") == 0) + { + cap_value_t cap; + if (argc < 2) + die ("--cap-drop takes an argument"); + + opt_cap_add_or_drop_used = TRUE; + + if (strcasecmp (argv[1], "ALL") == 0) + { + requested_caps[0] = requested_caps[1] = 0; + } + else + { + if (cap_from_name (argv[1], &cap) < 0) + die ("unknown cap: %s", argv[1]); + + if (cap < 32) + requested_caps[0] &= ~CAP_TO_MASK_0 (cap); + else + requested_caps[1] &= ~CAP_TO_MASK_1 (cap - 32); + } + + argv += 1; + argc -= 1; + } else if (*arg == '-') { die ("Unknown option %s", arg); @@ -1670,8 +1876,8 @@ } static void -parse_args (int *argcp, - char ***argvp) +parse_args (int *argcp, + const char ***argvp) { int total_parsed_argc = *argcp; @@ -1756,7 +1962,16 @@ if (argc == 0) usage (EXIT_FAILURE, stderr); - parse_args (&argc, &argv); + parse_args (&argc, (const char ***) &argv); + + if ((requested_caps[0] || requested_caps[1]) && is_privileged) + die ("--cap-add in setuid mode can be used only by root"); + + if (opt_userns_block_fd != -1 && !opt_unshare_user) + die ("--userns-block-fd requires --unshare-user"); + + if (opt_userns_block_fd != -1 && opt_info_fd == -1) + die ("--userns-block-fd requires --info-fd"); /* We have to do this if we weren't installed setuid (and we're not * root), so let's just DWIM */ @@ -1783,6 +1998,15 @@ disabled = TRUE; } + /* Check for max_user_namespaces */ + if (stat ("/proc/sys/user/max_user_namespaces", &sbuf) == 0) + { + cleanup_free char *max_user_ns = NULL; + max_user_ns = load_file_at (AT_FDCWD, "/proc/sys/user/max_user_namespaces"); + if (max_user_ns != NULL && strcmp(max_user_ns, "0\n") == 0) + disabled = TRUE; + } + /* Debian lets you disable *unprivileged* user namespaces. However this is not a problem if we're privileged, and if we're not opt_unshare_user is TRUE already, and there is not much we can do, its just a non-working setup. */ @@ -1810,6 +2034,12 @@ if (!opt_unshare_uts && opt_sandbox_hostname != NULL) die ("Specifying --hostname requires --unshare-uts"); + if (opt_as_pid_1 && !opt_unshare_pid) + die ("Specifying --as-pid-1 requires --unshare-pid"); + + if (opt_as_pid_1 && lock_files != NULL) + die ("Specifying --as-pid-1 and --lock-file is not permitted"); + /* We need to read stuff from proc during the pivot_root dance, etc. Lets keep a fd to it open */ proc_fd = open ("/proc", O_RDONLY | O_PATH); @@ -1829,7 +2059,7 @@ __debug__ (("creating new namespace\n")); - if (opt_unshare_pid) + if (opt_unshare_pid && !opt_as_pid_1) { event_fd = eventfd (0, EFD_CLOEXEC | EFD_NONBLOCK); if (event_fd == -1) @@ -1890,7 +2120,7 @@ { /* Parent, outside sandbox, privileged (initially) */ - if (is_privileged && opt_unshare_user) + if (is_privileged && opt_unshare_user && opt_userns_block_fd == -1) { /* We're running as euid 0, but the uid we want to map is * not 0. This means we're not allowed to write this from @@ -1908,17 +2138,11 @@ /* Initial launched process, wait for exec:ed command to exit */ /* We don't need any privileges in the launcher, drop them immediately. */ - drop_privs (); + drop_privs (FALSE); /* Optionally bind our lifecycle to that of the parent */ handle_die_with_parent (); - /* Let child run now that the uid maps are set up */ - val = 1; - res = write (child_wait_fd, &val, 8); - /* Ignore res, if e.g. the child died and closed child_wait_fd we don't want to error out here */ - close (child_wait_fd); - if (opt_info_fd != -1) { cleanup_free char *output = xasprintf ("{\n \"child-pid\": %i\n}\n", pid); @@ -1928,6 +2152,19 @@ close (opt_info_fd); } + if (opt_userns_block_fd != -1) + { + char b[1]; + (void) TEMP_FAILURE_RETRY (read (opt_userns_block_fd, b, 1)); + close (opt_userns_block_fd); + } + + /* Let child run now that the uid maps are set up */ + val = 1; + res = write (child_wait_fd, &val, 8); + /* Ignore res, if e.g. the child died and closed child_wait_fd we don't want to error out here */ + close (child_wait_fd); + monitor_child (event_fd, pid); exit (0); /* Should not be reached, but better safe... */ } @@ -1964,7 +2201,7 @@ ns_uid = opt_sandbox_uid; ns_gid = opt_sandbox_gid; - if (!is_privileged && opt_unshare_user) + if (!is_privileged && opt_unshare_user && opt_userns_block_fd == -1) { /* In the unprivileged case we have to write the uid/gid maps in * the child, because we have no caps in the parent */ @@ -2038,7 +2275,7 @@ if (child == 0) { /* Unprivileged setup process */ - drop_privs (); + drop_privs (FALSE); close (privsep_sockets[0]); setup_newroot (opt_unshare_pid, privsep_sockets[1]); exit (0); @@ -2081,7 +2318,8 @@ die_with_error ("unmount old root"); if (opt_unshare_user && - (ns_uid != opt_sandbox_uid || ns_gid != opt_sandbox_gid)) + (ns_uid != opt_sandbox_uid || ns_gid != opt_sandbox_gid) && + opt_userns_block_fd == -1) { /* Now that devpts is mounted and we've no need for mount permissions we can create a new userspace and map our uid @@ -2103,13 +2341,13 @@ if (chdir ("/") != 0) die_with_error ("chdir /"); - /* All privileged ops are done now, so drop it */ - drop_privs (); + /* All privileged ops are done now, so drop caps we don't need */ + drop_privs (!is_privileged); if (opt_block_fd != -1) { char b[1]; - read (opt_block_fd, b, 1); + (void) TEMP_FAILURE_RETRY (read (opt_block_fd, b, 1)); close (opt_block_fd); } @@ -2162,7 +2400,7 @@ __debug__ (("forking for child\n")); - if (opt_unshare_pid || lock_files != NULL || opt_sync_fd != -1) + if (!opt_as_pid_1 && (opt_unshare_pid || lock_files != NULL || opt_sync_fd != -1)) { /* We have to have a pid 1 in the pid namespace, because * otherwise we'll get a bunch of zombies as nothing reaps @@ -2176,6 +2414,8 @@ if (pid != 0) { + drop_all_caps (FALSE); + /* Close fds in pid 1, except stdio and optionally event_fd (for syncing pid 2 lifetime with monitor_child) and opt_sync_fd (for syncing sandbox lifetime with outside @@ -2201,8 +2441,13 @@ if (proc_fd != -1) close (proc_fd); - if (opt_sync_fd != -1) - close (opt_sync_fd); + /* If we are using --as-pid-1 leak the sync fd into the sandbox. + --sync-fd will still work unless the container process doesn't close this file. */ + if (!opt_as_pid_1) + { + if (opt_sync_fd != -1) + close (opt_sync_fd); + } /* We want sigchild in the child */ unblock_sigchild (); @@ -2210,6 +2455,9 @@ /* Optionally bind our lifecycle */ handle_die_with_parent (); + if (!is_privileged) + set_ambient_capabilities (); + /* Should be the last thing before execve() so that filters don't * need to handle anything above */ if (seccomp_data != NULL && Binary files old/bubblewrap-0.1.8/bubblewrap.jpg and new/bubblewrap-0.2.0/bubblewrap.jpg differ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bubblewrap-0.1.8/bwrap.xml new/bubblewrap-0.2.0/bwrap.xml --- old/bubblewrap-0.1.8/bwrap.xml 2017-03-28 16:26:53.000000000 +0200 +++ new/bubblewrap-0.2.0/bwrap.xml 2017-10-09 16:11:41.000000000 +0200 @@ -263,6 +263,14 @@ </para></listitem> </varlistentry> <varlistentry> + <term><option>--userns-block-fd <arg choice="plain">FD</arg></option></term> + <listitem><para> + Do not initialize the user namespace but wait on FD until it is ready. This allow + external processes (like newuidmap/newgidmap) to setup the user namespace before it + is used by the sandbox process. + </para></listitem> + </varlistentry> + <varlistentry> <term><option>--info-fd <arg choice="plain">FD</arg></option></term> <listitem><para> Write information in JSON format about the sandbox to FD. @@ -289,6 +297,31 @@ See prctl, PR_SET_PDEATHSIG. </para></listitem> </varlistentry> + <varlistentry> + <term><option>--as-pid-1</option></term> + <listitem><para> + Do not create a process with PID=1 in the sandbox to reap child processes. + </para></listitem> + </varlistentry> + <varlistentry> + <term><option>--cap-add <arg choice="plain">CAP</arg></option></term> + <listitem><para> + Add the specified capability when running as privileged user. It accepts + the special value ALL to add all the permitted caps. + </para></listitem> + </varlistentry> + <varlistentry> + <term><option>--cap-drop <arg choice="plain">CAP</arg></option></term> + <listitem><para> + Drop the specified capability when running as privileged user. It accepts + the special value ALL to drop all the caps. + + By default no caps are left in the sandboxed process. The + <option>--cap-add</option> and <option>--cap-drop</option> + options are processed in the order they are specified on the + command line. Please be careful to the order they are specified. + </para></listitem> + </varlistentry> </variablelist> </refsect1> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bubblewrap-0.1.8/ci/papr.sh new/bubblewrap-0.2.0/ci/papr.sh --- old/bubblewrap-0.1.8/ci/papr.sh 1970-01-01 01:00:00.000000000 +0100 +++ new/bubblewrap-0.2.0/ci/papr.sh 2017-10-09 16:11:41.000000000 +0200 @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +set -xeuo pipefail + +distro=$1 + +runcontainer() { + docker run --rm --env=container=true --env=BWRAP_SUID=${BWRAP_SUID:-} --env CFLAGS="${CFLAGS:-}" --net=host --privileged -v /usr:/host/usr -v $(pwd):/srv/code -w /srv/code $distro ./ci/papr.sh $distro +} + +buildinstall_to_host() { + + yum -y install git autoconf automake libtool make gcc redhat-rpm-config \ + libcap-devel 'pkgconfig(libselinux)' 'libxslt' 'docbook-style-xsl' \ + lib{a,ub,t}san /usr/bin/eu-readelf rsync + + echo testing: $(git describe --tags --always --abbrev=42) + + env NOCONFIGURE=1 ./autogen.sh + ./configure --prefix=/usr --libdir=/usr/lib64 + make -j 8 + tmpd=$(mktemp -d) + make install DESTDIR=${tmpd} + for san in a t ub; do + if eu-readelf -d ${tmpd}/usr/bin/bwrap | grep -q "NEEDED.*lib${san}san"; then + for x in /usr/lib64/lib${san}san*.so.*; do + install -D $x ${tmpd}${x} + done + fi + done + rsync -rlv ${tmpd}/usr/ /host/usr/ + if ${BWRAP_SUID}; then + chmod u+s /host/usr/bin/bwrap + fi + rm ${tmpd} -rf +} + +if test -z "${container:-}"; then + ostree admin unlock + useradd bwrap-tester + runcontainer + # Unprivileged invocation + runuser -u bwrap-tester env ASAN_OPTIONS=detect_leaks=false ./tests/test-run.sh + # Privileged invocation + env ASAN_OPTIONS=detect_leaks=false ./tests/test-run.sh +else + buildinstall_to_host +fi diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bubblewrap-0.1.8/ci/redhat-ci.sh new/bubblewrap-0.2.0/ci/redhat-ci.sh --- old/bubblewrap-0.1.8/ci/redhat-ci.sh 2017-03-28 16:26:53.000000000 +0200 +++ new/bubblewrap-0.2.0/ci/redhat-ci.sh 1970-01-01 01:00:00.000000000 +0100 @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -xeuo pipefail - -distro=$1 - -runcontainer() { - docker run --rm --env=container=true --env=BWRAP_SUID=${BWRAP_SUID:-} --env CFLAGS="${CFLAGS:-}" --net=host --privileged -v /usr:/host/usr -v $(pwd):/srv/code -w /srv/code $distro ./ci/redhat-ci.sh $distro -} - -buildinstall_to_host() { - - yum -y install git autoconf automake libtool make gcc redhat-rpm-config \ - libcap-devel 'pkgconfig(libselinux)' 'libxslt' 'docbook-style-xsl' \ - lib{a,ub,t}san /usr/bin/eu-readelf - - echo testing: $(git describe --tags --always --abbrev=42) - - env NOCONFIGURE=1 ./autogen.sh - ./configure --prefix=/usr --libdir=/usr/lib64 - make -j 8 - tmpd=$(mktemp -d) - make install DESTDIR=${tmpd} - for san in a t ub; do - if eu-readelf -d ${tmpd}/usr/bin/bwrap | grep -q "NEEDED.*lib${san}san"; then - for x in /usr/lib64/lib${san}san*.so.*; do - install -D $x ${tmpd}${x} - done - fi - done - rsync -rlv ${tmpd}/usr/ /host/usr/ - if ${BWRAP_SUID}; then - chmod u+s /host/usr/bin/bwrap - fi - rm ${tmpd} -rf -} - -if test -z "${container:-}"; then - ostree admin unlock - # Hack until the host tree is updated in rhci - rpm -Uvh https://kojipkgs.fedoraproject.org//packages/glibc/2.24/4.fc25/x86_64/{libcrypt-nss,glibc,glibc-common,glibc-all-langpacks}-2.24-4.fc25.x86_64.rpm - useradd bwrap-tester - runcontainer - runuser -u bwrap-tester env ASAN_OPTIONS=detect_leaks=false ./tests/test-run.sh -else - buildinstall_to_host -fi diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bubblewrap-0.1.8/completions/bash/bwrap new/bubblewrap-0.2.0/completions/bash/bwrap --- old/bubblewrap-0.1.8/completions/bash/bwrap 2017-03-28 16:26:53.000000000 +0200 +++ new/bubblewrap-0.2.0/completions/bash/bwrap 2017-10-09 16:11:41.000000000 +0200 @@ -8,11 +8,14 @@ _init_completion || return local boolean_options=" + --as-pid-1 --help + --new-session --unshare-cgroup --unshare-cgroup-try --unshare-user --unshare-user-try + --unshare-all --unshare-ipc --unshare-net --unshare-pid @@ -26,9 +29,12 @@ --bind --bind-data --block-fd + --cap-add + --cap-drop --chdir --dev --dev-bind + --die-with-parent --dir --exec-label --file @@ -38,17 +44,15 @@ --info-fd --lock-file --proc - --ro-bind --remount-ro + --ro-bind --seccomp --setenv --symlink --sync-fd --uid --unsetenv - --seccomp - --symlink - --die-with-parent + --userns-block-fd " if [[ "$cur" == -* ]]; then diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bubblewrap-0.1.8/configure.ac new/bubblewrap-0.2.0/configure.ac --- old/bubblewrap-0.1.8/configure.ac 2017-03-28 16:26:53.000000000 +0200 +++ new/bubblewrap-0.2.0/configure.ac 2017-10-09 16:11:41.000000000 +0200 @@ -1,5 +1,5 @@ AC_PREREQ([2.63]) -AC_INIT([bubblewrap], [0.1.8], [[email protected]]) +AC_INIT([bubblewrap], [0.2.0], [[email protected]]) AC_CONFIG_HEADER([config.h]) AC_CONFIG_MACRO_DIR([m4]) AC_CONFIG_AUX_DIR([build-aux]) @@ -87,6 +87,12 @@ ]) AC_SUBST(WARN_CFLAGS) +AC_CHECK_LIB(cap, cap_from_text) + +if test "$ac_cv_lib_cap_cap_from_text" != "yes"; then + AC_MSG_ERROR([*** libcap requested but not found]) +fi + AC_ARG_WITH(priv-mode, AS_HELP_STRING([--with-priv-mode=setuid/none], [How to set privilege-raising during make install]), @@ -110,6 +116,9 @@ AC_DEFINE(ENABLE_REQUIRE_USERNS, 1, [Define if userns should be used by default in suid mode]) ]) +AC_PROG_AWK +AC_REQUIRE_AUX_FILE([tap-driver.sh]) + AC_CONFIG_FILES([ Makefile ]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bubblewrap-0.1.8/demos/bubblewrap-shell.sh new/bubblewrap-0.2.0/demos/bubblewrap-shell.sh --- old/bubblewrap-0.1.8/demos/bubblewrap-shell.sh 2017-03-28 16:26:53.000000000 +0200 +++ new/bubblewrap-0.2.0/demos/bubblewrap-shell.sh 2017-10-09 16:11:41.000000000 +0200 @@ -23,6 +23,7 @@ --chdir / \ --unshare-all \ --share-net \ + --die-with-parent \ --dir /run/user/$(id -u) \ --setenv XDG_RUNTIME_DIR "/run/user/`id -u`" \ --setenv PS1 "bwrap-demo$ " \ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bubblewrap-0.1.8/demos/userns-block-fd.py new/bubblewrap-0.2.0/demos/userns-block-fd.py --- old/bubblewrap-0.1.8/demos/userns-block-fd.py 1970-01-01 01:00:00.000000000 +0100 +++ new/bubblewrap-0.2.0/demos/userns-block-fd.py 2017-10-09 16:11:41.000000000 +0200 @@ -0,0 +1,36 @@ +#!/bin/python + +import os, select, subprocess, json + +pipe_info = os.pipe() +userns_block = os.pipe() + +pid = os.fork() + +if pid != 0: + os.close(pipe_info[1]) + os.close(userns_block[0]) + + select.select([pipe_info[0]], [], []) + + data = json.load(os.fdopen(pipe_info[0])) + child_pid = str(data['child-pid']) + + subprocess.call(["newuidmap", child_pid, "0", str(os.getuid()), "1"]) + subprocess.call(["newgidmap", child_pid, "0", str(os.getgid()), "1"]) + + os.write(userns_block[1], '1') +else: + os.close(pipe_info[0]) + os.close(userns_block[1]) + + args = ["bwrap", + "bwrap", + "--unshare-all", + "--unshare-user", + "--userns-block-fd", "%i" % userns_block[0], + "--info-fd", "%i" % pipe_info[1], + "--bind", "/", "/", + "cat", "/proc/self/uid_map"] + + os.execl(*args) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bubblewrap-0.1.8/tests/libtest-core.sh new/bubblewrap-0.2.0/tests/libtest-core.sh --- old/bubblewrap-0.1.8/tests/libtest-core.sh 1970-01-01 01:00:00.000000000 +0100 +++ new/bubblewrap-0.2.0/tests/libtest-core.sh 2017-10-09 16:11:41.000000000 +0200 @@ -0,0 +1,142 @@ +# Core source library for shell script tests; the +# canonical version lives in: +# +# https://github.com/ostreedev/ostree +# +# Known copies are in the following repos: +# +# - https://github.com/projectatomic/rpm-ostree +# +# Copyright (C) 2017 Colin Walters <[email protected]> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +fatal() { + echo $@ 1>&2; exit 1 +} +# fatal() is shorter to type, but retain this alias +assert_not_reached () { + fatal "$@" +} + +# Some tests look for specific English strings. Use a UTF-8 version +# of the C (POSIX) locale if we have one, or fall back to POSIX +# (https://sourceware.org/glibc/wiki/Proposals/C.UTF-8) +if locale -a | grep C.UTF-8 >/dev/null; then + export LC_ALL=C.UTF-8 +else + export LC_ALL=C +fi + +# This should really be the default IMO +export G_DEBUG=fatal-warnings + +assert_streq () { + test "$1" = "$2" || fatal "$1 != $2" +} + +assert_str_match () { + if ! echo "$1" | grep -E -q "$2"; then + fatal "$1 does not match regexp $2" + fi +} + +assert_not_streq () { + (! test "$1" = "$2") || fatal "$1 == $2" +} + +assert_has_file () { + test -f "$1" || fatal "Couldn't find '$1'" +} + +assert_has_dir () { + test -d "$1" || fatal "Couldn't find '$1'" +} + +# Dump ls -al + file contents to stderr, then fatal() +_fatal_print_file() { + file="$1" + shift + ls -al "$file" >&2 + sed -e 's/^/# /' < "$file" >&2 + fatal "$@" +} + +assert_not_has_file () { + if test -f "$1"; then + _fatal_print_file "$1" "File '$1' exists" + fi +} + +assert_not_file_has_content () { + fpath=$1 + shift + for re in "$@"; do + if grep -q -e "$re" "$fpath"; then + _fatal_print_file "$fpath" "File '$fpath' matches regexp '$re'" + fi + done +} + +assert_not_has_dir () { + if test -d "$1"; then + fatal "Directory '$1' exists" + fi +} + +assert_file_has_content () { + fpath=$1 + shift + for re in "$@"; do + if ! grep -q -e "$re" "$fpath"; then + _fatal_print_file "$fpath" "File '$fpath' doesn't match regexp '$re'" + fi + done +} + +assert_file_has_content_literal () { + if ! grep -q -F -e "$2" "$1"; then + _fatal_print_file "$1" "File '$1' doesn't match fixed string list '$2'" + fi +} + +assert_file_has_mode () { + mode=$(stat -c '%a' $1) + if [ "$mode" != "$2" ]; then + fatal "File '$1' has wrong mode: expected $2, but got $mode" + fi +} + +assert_symlink_has_content () { + if ! test -L "$1"; then + fatal "File '$1' is not a symbolic link" + fi + if ! readlink "$1" | grep -q -e "$2"; then + _fatal_print_file "$1" "Symbolic link '$1' doesn't match regexp '$2'" + fi +} + +assert_file_empty() { + if test -s "$1"; then + _fatal_print_file "$1" "File '$1' is not empty" + fi +} + +# Use to skip all of these tests +skip() { + echo "1..0 # SKIP" "$@" + exit 0 +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bubblewrap-0.1.8/tests/test-run.sh new/bubblewrap-0.2.0/tests/test-run.sh --- old/bubblewrap-0.1.8/tests/test-run.sh 2017-03-28 16:26:53.000000000 +0200 +++ new/bubblewrap-0.2.0/tests/test-run.sh 2017-10-09 16:11:41.000000000 +0200 @@ -2,7 +2,13 @@ set -xeuo pipefail +# Make sure /sbin/getpcaps etc. are in our PATH even if non-root +PATH="$PATH:/usr/sbin:/sbin" + srcd=$(cd $(dirname $0) && pwd) + +. ${srcd}/libtest-core.sh + bn=$(basename $0) tempdir=$(mktemp -d /var/tmp/tap-test.XXXXXX) touch ${tempdir}/.testtmp @@ -19,20 +25,6 @@ : "${BWRAP:=bwrap}" -skip () { - echo $@ 1>&2; exit 77 -} - -assert_not_reached () { - echo $@ 1>&2; exit 1 -} - -assert_file_has_content () { - if ! grep -q -e "$2" "$1"; then - echo 1>&2 "File '$1' doesn't match regexp '$2'"; exit 1 - fi -} - FUSE_DIR= for mp in $(cat /proc/self/mounts | grep " fuse[. ]" | grep user_id=$(id -u) | awk '{print $2}'); do if test -d $mp; then @@ -42,9 +34,15 @@ fi done +if test "$(id -u)" = "0"; then + is_uidzero=true +else + is_uidzero=false +fi + # This is supposed to be an otherwise readable file in an unreadable (by the user) dir UNREADABLE=/root/.bashrc -if test -x `dirname $UNREADABLE`; then +if ${is_uidzero} || test -x `dirname $UNREADABLE`; then UNREADABLE= fi @@ -55,36 +53,101 @@ skip Seems like bwrap is not working at all. Maybe setuid is not working fi +echo "1..32" + # Test help ${BWRAP} --help > help.txt assert_file_has_content help.txt "usage: ${BWRAP}" +echo "ok - Help works" for ALT in "" "--unshare-user-try" "--unshare-pid" "--unshare-user-try --unshare-pid"; do # Test fuse fs as bind source if [ x$FUSE_DIR != x ]; then $RUN $ALT --proc /proc --dev /dev --bind $FUSE_DIR /tmp/foo true + echo "ok - can bind-mount a FUSE directory with $ALT" + else + echo "ok # SKIP no FUSE support" fi # no --dev => no devpts => no map_root workaround $RUN $ALT --proc /proc true + echo "ok - can mount /proc with $ALT" # No network $RUN $ALT --unshare-net --proc /proc --dev /dev true + echo "ok - can unshare network, create new /dev with $ALT" # Unreadable file - echo -n "expect EPERM: " - if $RUN $ALT --unshare-net --proc /proc --bind /etc/shadow /tmp/foo cat /etc/shadow; then + echo -n "expect EPERM: " >&2 + + # Test caps when bwrap is not setuid + if ! test -u ${BWRAP}; then + CAP="--cap-add ALL" + else + CAP="" + fi + + if ! ${is_uidzero} && $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 - echo -n "expect EPERM: " + echo -n "expect EPERM: " >&2 if $RUN $ALT --unshare-net --proc /proc --dev /dev --bind $UNREADABLE /tmp/foo cat /tmp/foo ; then assert_not_reached Could read $UNREADABLE fi + echo "ok - cannot read $UNREADABLE with $ALT" + else + echo "ok # SKIP not sure what unreadable file to use" fi # bind dest in symlink (https://github.com/projectatomic/bubblewrap/pull/119) $RUN $ALT --dir /tmp/dir --symlink dir /tmp/link --bind /etc /tmp/link true + echo "ok - can bind a destination over a symlink" done +# Test devices +$RUN --unshare-pid --dev /dev ls -al /dev/{stdin,stdout,stderr,null,random,urandom,fd,core} >/dev/null +echo "ok - all expected devices were created" + +# Test --as-pid-1 +$RUN --unshare-pid --as-pid-1 --bind / / bash -c 'echo $$' > as_pid_1.txt +assert_file_has_content as_pid_1.txt "1" +echo "ok - can run as pid 1" + +# Test error prefixing +if $RUN --unshare-pid --bind /source-enoent /dest true 2>err.txt; then + assert_not_reached "bound nonexistent source" +fi +assert_file_has_content err.txt "^bwrap: Can't find source path.*source-enoent" +echo "ok error prefxing" + +if ! ${is_uidzero}; then + # When invoked as non-root, check that by default we have no caps left + for OPT in "" "--unshare-user-try --as-pid-1" "--unshare-user-try" "--as-pid-1"; do + e=0 + $RUN $OPT --unshare-pid getpcaps 1 2> caps.test || e=$? + sed -e 's/^/# /' < caps.test >&2 + test "$e" = 0 + assert_not_file_has_content caps.test ': =.*cap' + done + echo "ok - we have no caps as uid != 0" +else + capsh --print > caps.orig + for OPT in "" "--as-pid-1"; do + $RUN $OPT --unshare-pid capsh --print >caps.test + diff -u caps.orig caps.test + done + # And test that we can drop all, as well as specific caps + $RUN $OPT --cap-drop ALL --unshare-pid capsh --print >caps.test + assert_file_has_content caps.test 'Current: =$' + # Check for dropping kill/fowner (we assume all uid 0 callers have this) + $RUN $OPT --cap-drop CAP_KILL --cap-drop CAP_FOWNER --unshare-pid capsh --print >caps.test + assert_not_file_has_content caps.test '^Current: =.*cap_kill' + assert_not_file_has_content caps.test '^Current: =.*cap_fowner' + # But we should still have net_bind_service for example + assert_file_has_content caps.test '^Current: =.*cap_net_bind_service' + echo "ok - we have the expected caps as uid 0" +fi + # Test --die-with-parent cat >lockf-n.py <<EOF @@ -107,7 +170,10 @@ touch lock for die_with_parent_argv in "--die-with-parent" "--die-with-parent --unshare-pid"; do - /bin/bash -c "$RUN ${die_with_parent_argv} --lock-file $(pwd)/lock sleep 1h && true" & + # We have to loop here, because bwrap doesn't wait for the lock if + # another process is holding it. If we're unlucky, lockf-n.py will + # be holding it. + /bin/bash -c "while true; do $RUN ${die_with_parent_argv} --lock-file $(pwd)/lock sleep 1h; done" & childshellpid=$! # Wait for lock to be taken (yes hacky) @@ -129,4 +195,8 @@ echo "ok die with parent ${die_with_parent_argv}" done -echo OK +printf '%s--dir\0/tmp/hello/world\0' '' > test.args +$RUN --args 3 test -d /tmp/hello/world 3<test.args +echo "ok - we can parse arguments from a fd" + +echo "ok - End of test" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bubblewrap-0.1.8/utils.c new/bubblewrap-0.2.0/utils.c --- old/bubblewrap-0.1.8/utils.c 2017-03-28 16:26:53.000000000 +0200 +++ new/bubblewrap-0.2.0/utils.c 2017-10-09 16:11:41.000000000 +0200 @@ -29,6 +29,8 @@ va_list args; int errsv; + fprintf (stderr, "bwrap: "); + errsv = errno; va_start (args, format); @@ -45,6 +47,8 @@ { va_list args; + fprintf (stderr, "bwrap: "); + va_start (args, format); vfprintf (stderr, format, args); va_end (args);
