diff --git a/bubblewrap.c b/bubblewrap.c index 62227014..86e18107 100644 --- a/bubblewrap.c +++ b/bubblewrap.c @@ -94,6 +94,8 @@ static int opt_tmp_overlay_count = 0; static int next_perms = -1; static size_t next_size_arg = 0; static int next_overlay_src_count = 0; +static char **next_overlay_srcs = NULL; +static char *next_overlay_opt = NULL; static bool opt_not_a_security_boundary = false; #define CAP_TO_MASK_0(x) (1L << ((x) & 31)) @@ -165,6 +167,7 @@ struct _SetupOp SetupOpFlag flags; int perms; size_t size; /* number of bytes, zero means unset/default */ + char *overlay_opt; /* extra overlayfs mount options, owned, NULL if unset */ SetupOp *next; }; @@ -329,6 +332,8 @@ usage (int ecode, FILE *out) " --ro-bind-fd FD DEST Bind open directory or path fd read-only on DEST\n" " --remount-ro DEST Remount DEST as readonly; does not recursively remount\n" " --overlay-src SRC Read files from SRC in the following overlay\n" + " --overlay-opt OPT Pass extra overlayfs mount options (e.g. index=off,xino=off) to the\n" + " next --overlay/--tmp-overlay/--ro-overlay\n" " --overlay RWSRC WORKDIR DEST Mount overlayfs on DEST, with RWSRC as the host path for writes and\n" " WORKDIR an empty directory on the same filesystem as RWSRC\n" " --tmp-overlay DEST Mount overlayfs on DEST, with writes going to an invisible tmpfs\n" @@ -1317,6 +1322,8 @@ setup_newroot (bool unshare_pid) bool multi_src = false; cleanup_fdset FdSet fds = {0}; cleanup_free char *dest_path = fd_to_proc_path (dest_fd); + /* Keep the mount op handle; `op` is advanced through the workdir/src ops below. */ + SetupOp *mount_op = op; if (op->source != NULL) { @@ -1350,6 +1357,12 @@ setup_newroot (bool unshare_pid) strappend (&sb, ",userxattr"); + if (mount_op->overlay_opt != NULL) + { + strappend (&sb, ","); + strappend (&sb, mount_op->overlay_opt); + } + if (mount ("overlay", dest_path, "overlay", MS_MGC_VAL | MS_NOSUID | MS_NODEV, sb.str) != 0) { /* The standard message for ELOOP, "Too many levels of symbolic @@ -1357,9 +1370,9 @@ setup_newroot (bool unshare_pid) if (errno == ELOOP) die ("Can't make overlay mount on %s with options %s: " "Overlay directories may not overlap", - op->dest, sb.str); + mount_op->dest, sb.str); die_with_mount_error ("Can't make overlay mount on %s with options %s", - op->dest, sb.str); + mount_op->dest, sb.str); } free (sb.str); @@ -1723,7 +1736,7 @@ path_argument (const char *option, } static void -make_setup_overlay_src_ops (const char *const *const argv) +make_setup_overlay_src_ops (SetupOp *mount_op) { /* SETUP_OVERLAY_SRC is unlike other SETUP_* ops in that it exists to hold * data for SETUP_{,TMP_,RO_}OVERLAY_MOUNT ops, not to be its own operation. @@ -1731,20 +1744,32 @@ make_setup_overlay_src_ops (const char *const *const argv) * of each source, as no other operations involve multiple sources the way * the *_OVERLAY_MOUNT ops do. * - * While the --overlay-src arguments are expected to (directly) precede the + * While the --overlay-src arguments are expected to precede the * --overlay argument, in bottom-to-top order, the SETUP_OVERLAY_SRC ops * follow their corresponding *_OVERLAY_MOUNT op, in top-to-bottom order * (the order in which overlayfs will want them). They are handled specially * in setup_new_root () during the processing of *_OVERLAY_MOUNT. + * + * The source paths are stored in the next_overlay_srcs pending array at + * parse time. They can no longer be re-derived from fixed argv positions, + * because --overlay-opt may appear between the --overlay-src arguments and + * the overlay op. The pending --overlay-opt, if any, is moved onto the + * mount op. */ int i; SetupOp *op; - for (i = 1; i <= next_overlay_src_count; i++) + mount_op->overlay_opt = next_overlay_opt; + next_overlay_opt = NULL; + + for (i = next_overlay_src_count - 1; i >= 0; i--) { op = setup_op_new (SETUP_OVERLAY_SRC); - op->source = path_argument ("--overlay-src", argv[1 - 2 * i]); + op->source = next_overlay_srcs[i]; } + + free (next_overlay_srcs); + next_overlay_srcs = NULL; next_overlay_src_count = 0; } @@ -2015,7 +2040,32 @@ parse_args_recurse (int *argcp, } else if (strcmp (arg, "--overlay-src") == 0) { - next_overlay_src_count++; + next_overlay_srcs = realloc (next_overlay_srcs, + (size_t) (next_overlay_src_count + 1) * sizeof (char *)); + if (next_overlay_srcs == NULL) + die ("Out of memory"); + next_overlay_srcs[next_overlay_src_count++] = + path_argument ("--overlay-src", argv[1]); + + argv += 1; + argc -= 1; + } + else if (strcmp (arg, "--overlay-opt") == 0) + { + const char *opt; + + if (argc < 2) + die ("--overlay-opt takes an argument"); + + opt = argv[1]; + if (next_overlay_opt == NULL) + next_overlay_opt = xstrdup (opt); + else + { + char *joined = xasprintf ("%s,%s", next_overlay_opt, opt); + free (next_overlay_opt); + next_overlay_opt = joined; + } argv += 1; argc -= 1; @@ -2035,7 +2085,7 @@ parse_args_recurse (int *argcp, workdir_op = setup_op_new (SETUP_OVERLAY_SRC); workdir_op->source = path_argument (arg, argv[2]); op->dest = path_argument (arg, argv[3]); - make_setup_overlay_src_ops (argv); + make_setup_overlay_src_ops (op); argv += 3; argc -= 3; @@ -2050,7 +2100,7 @@ parse_args_recurse (int *argcp, op = setup_op_new (SETUP_TMP_OVERLAY_MOUNT); op->dest = path_argument (arg, argv[1]); - make_setup_overlay_src_ops (argv); + make_setup_overlay_src_ops (op); opt_tmp_overlay_count++; argv += 1; @@ -2066,7 +2116,7 @@ parse_args_recurse (int *argcp, op = setup_op_new (SETUP_RO_OVERLAY_MOUNT); op->dest = path_argument (arg, argv[1]); - make_setup_overlay_src_ops (argv); + make_setup_overlay_src_ops (op); argv += 1; argc -= 1; @@ -2752,8 +2802,10 @@ parse_args_recurse (int *argcp, die ("--size must be followed by --tmpfs"); /* Similarly for --overlay-src. */ - if (strcmp (arg, "--overlay-src") != 0 && next_overlay_src_count > 0) - die ("--overlay-src must be followed by another --overlay-src or one of --overlay, --tmp-overlay, or --ro-overlay"); + if (strcmp (arg, "--overlay-src") != 0 && + strcmp (arg, "--overlay-opt") != 0 && + next_overlay_src_count > 0) + die ("--overlay-src must be followed by --overlay, --tmp-overlay, or --ro-overlay"); argv++; argc--; @@ -2772,7 +2824,10 @@ parse_args (int *argcp, parse_args_recurse (argcp, argvp, false, &total_parsed_argc); if (next_overlay_src_count > 0) - die ("--overlay-src must be followed by another --overlay-src or one of --overlay, --tmp-overlay, or --ro-overlay"); + die ("--overlay-src must be followed by --overlay, --tmp-overlay, or --ro-overlay"); + + if (next_overlay_opt != NULL) + die ("--overlay-opt must be followed by --overlay, --tmp-overlay, or --ro-overlay"); } static void diff --git a/bwrap.xml b/bwrap.xml index b48cf5b4..7bba568a 100644 --- a/bwrap.xml +++ b/bwrap.xml @@ -342,6 +342,24 @@ + + + + + This option does nothing on its own, and must be followed by one of + the other overlay options. It passes extra mount + options to the kernel's overlayfs for that + mount, for example index=off,xino=off. + + + This option can be used multiple times, in which case the values are + joined with a comma. It may be given before or after the + arguments, but must precede the + , or + option it applies to. + + + diff --git a/completions/bash/bwrap b/completions/bash/bwrap index e7a523c2..5b737199 100644 --- a/completions/bash/bwrap +++ b/completions/bash/bwrap @@ -52,6 +52,7 @@ _bwrap() { --info-fd --lock-file --overlay + --overlay-opt --overlay-src --perms --proc diff --git a/completions/zsh/_bwrap b/completions/zsh/_bwrap index fbddda43..37dba38e 100644 --- a/completions/zsh/_bwrap +++ b/completions/zsh/_bwrap @@ -54,6 +54,7 @@ _bwrap_args=( '--lock-file[Take a lock on DEST while sandbox is running]:lock file:_files' '--mqueue[Mount new mqueue on DEST]:mount point for mqueue:_files -/' '--new-session[Create a new terminal session]' + '--overlay-opt[Pass extra overlayfs mount options (e.g. index=off,xino=off) to the next overlay option]:overlayfs options:' '--perms[Set permissions for next action argument]: :_guard "[0-7]#" "permissions in octal": :->after_perms' '--pidns[Use this user namespace (as parent namespace if using --unshare-pid)]: :' '--proc[Mount new procfs on DEST]:mount point for procfs:_files -/' diff --git a/tests/test-sandbox.py b/tests/test-sandbox.py index a266b8cd..171eb013 100644 --- a/tests/test-sandbox.py +++ b/tests/test-sandbox.py @@ -531,6 +531,51 @@ def test_ro_overlay(self): self.assertEqual(result.returncode, 0) self.assertEqual(result.stdout, b'child content') + # ------ overlay-opt ------ + + def test_tmp_overlay_opt_after_src(self): + result = run_bwrap('--overlay-src', self.src.dir, + '--overlay-opt', 'index=off,xino=off', + '--tmp-overlay', '/tmp/ov', + 'cat', '/tmp/ov/child') + if result.returncode != 0 and b'overlay' in result.stderr.lower(): + self.skipTest('overlayfs not available') + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, b'child content') + + def test_tmp_overlay_opt_before_src(self): + result = run_bwrap('--overlay-opt', 'index=off,xino=off', + '--overlay-src', self.src.dir, + '--tmp-overlay', '/tmp/ov', + 'cat', '/tmp/ov/child') + if result.returncode != 0 and b'overlay' in result.stderr.lower(): + self.skipTest('overlayfs not available') + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, b'child content') + + def test_tmp_overlay_opt_multiple_comma_joined(self): + result = run_bwrap('--overlay-opt', 'index=off', + '--overlay-opt', 'xino=off', + '--overlay-src', self.src.dir, + '--tmp-overlay', '/tmp/ov', + 'cat', '/tmp/ov/child') + if result.returncode != 0 and b'overlay' in result.stderr.lower(): + self.skipTest('overlayfs not available') + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, b'child content') + + def test_overlay_opt_dangling(self): + result = run_bwrap('--overlay-opt', 'index=off', 'true') + self.assertBwrapFailed(result) + self.assertInStderr(b'--overlay-opt must be followed', result) + + def test_overlay_src_not_consumed(self): + result = run_bwrap('--overlay-src', self.src.dir, + '--overlay-opt', 'index=off', + '--ro-bind', self.src.file, '/tmp/f', 'true') + self.assertBwrapFailed(result) + self.assertInStderr(b'--overlay-src must be followed', result) + # ------ Edge cases: source path with symlinks ------ def test_bind_source_absolute_symlink_in_path(self):