diff --git a/bubblewrap.c b/bubblewrap.c
index be020048..63a2427b 100644
--- a/bubblewrap.c
+++ b/bubblewrap.c
@@ -99,8 +99,10 @@ char *opt_args_data = NULL; /* owned */
int opt_userns_fd = -1;
int opt_userns2_fd = -1;
int opt_pidns_fd = -1;
+int opt_tmp_overlay_count = 0;
int next_perms = -1;
size_t next_size_arg = 0;
+int next_overlay_src_count = 0;
#define CAP_TO_MASK_0(x) (1L << ((x) & 31))
#define CAP_TO_MASK_1(x) CAP_TO_MASK_0(x - 32)
@@ -130,6 +132,10 @@ typedef enum {
SETUP_BIND_MOUNT,
SETUP_RO_BIND_MOUNT,
SETUP_DEV_BIND_MOUNT,
+ SETUP_OVERLAY_MOUNT,
+ SETUP_TMP_OVERLAY_MOUNT,
+ SETUP_RO_OVERLAY_MOUNT,
+ SETUP_OVERLAY_SRC,
SETUP_MOUNT_PROC,
SETUP_MOUNT_DEV,
SETUP_MOUNT_TMPFS,
@@ -175,6 +181,7 @@ struct _LockFile
enum {
PRIV_SEP_OP_DONE,
PRIV_SEP_OP_BIND_MOUNT,
+ PRIV_SEP_OP_OVERLAY_MOUNT,
PRIV_SEP_OP_PROC_MOUNT,
PRIV_SEP_OP_TMPFS_MOUNT,
PRIV_SEP_OP_DEVPTS_MOUNT,
@@ -332,6 +339,11 @@ usage (int ecode, FILE *out)
" --ro-bind SRC DEST Bind mount the host path SRC readonly on DEST\n"
" --ro-bind-try SRC DEST Equal to --ro-bind but ignores non-existent SRC\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 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"
+ " --ro-overlay DEST Mount overlayfs read-only on DEST\n"
" --exec-label LABEL Exec label for the sandbox\n"
" --file-label LABEL File label for temporary sandbox content\n"
" --proc DEST Mount new procfs on DEST\n"
@@ -1145,6 +1157,12 @@ privileged_op (int privileged_op_socket,
die_with_error ("Can't mount mqueue on %s", arg1);
break;
+ case PRIV_SEP_OP_OVERLAY_MOUNT:
+ if (mount ("overlay", arg2, "overlay", MS_MGC_VAL, arg1) != 0)
+ die_with_error ("Can't make overlay mount on %s with options %s",
+ arg2, arg1);
+ break;
+
case PRIV_SEP_OP_SET_HOSTNAME:
/* This is checked at the start, but lets verify it here in case
something manages to send hacked priv-sep operation requests. */
@@ -1168,6 +1186,7 @@ setup_newroot (bool unshare_pid,
int privileged_op_socket)
{
SetupOp *op;
+ int tmp_overlay_idx = 0;
for (op = ops; op != NULL; op = op->next)
{
@@ -1231,6 +1250,45 @@ setup_newroot (bool unshare_pid,
0, 0, source, dest);
break;
+ case SETUP_OVERLAY_MOUNT:
+ case SETUP_RO_OVERLAY_MOUNT:
+ case SETUP_TMP_OVERLAY_MOUNT:
+ {
+ StringBuilder sb = {0};
+ bool multi_src = FALSE;
+
+ if (mkdir (dest, 0755) != 0 && errno != EEXIST)
+ die_with_error ("Can't mkdir %s", op->dest);
+
+ if (op->source != NULL)
+ {
+ strappend (&sb, "upperdir=/oldroot");
+ strappend_escape_for_mount_options (&sb, op->source);
+ strappend (&sb, ",workdir=/oldroot");
+ op = op->next;
+ strappend_escape_for_mount_options (&sb, op->source);
+ strappend (&sb, ",");
+ }
+ else if (op->type == SETUP_TMP_OVERLAY_MOUNT)
+ strappendf (&sb, "upperdir=/tmp-overlay-upper-%1$d,workdir=/tmp-overlay-work-%1$d,",
+ tmp_overlay_idx++);
+
+ strappend (&sb, "lowerdir=/oldroot");
+ while (op->next != NULL && op->next->type == SETUP_OVERLAY_SRC)
+ {
+ op = op->next;
+ if (multi_src)
+ strappend (&sb, ":/oldroot");
+ strappend_escape_for_mount_options (&sb, op->source);
+ multi_src = TRUE;
+ }
+
+ privileged_op (privileged_op_socket,
+ PRIV_SEP_OP_OVERLAY_MOUNT, 0, 0, 0, sb.str, dest);
+ free (sb.str);
+ }
+ break;
+
case SETUP_REMOUNT_RO_NO_RECURSIVE:
privileged_op (privileged_op_socket,
PRIV_SEP_OP_REMOUNT_RO_NO_RECURSIVE, 0, 0, 0, NULL, dest);
@@ -1476,6 +1534,7 @@ setup_newroot (bool unshare_pid,
op->dest, NULL);
break;
+ case SETUP_OVERLAY_SRC: /* handled by SETUP_OVERLAY_MOUNT */
default:
die ("Unexpected type %d", op->type);
}
@@ -1518,6 +1577,8 @@ resolve_symlinks_in_ops (void)
case SETUP_RO_BIND_MOUNT:
case SETUP_DEV_BIND_MOUNT:
case SETUP_BIND_MOUNT:
+ case SETUP_OVERLAY_SRC:
+ case SETUP_OVERLAY_MOUNT:
old_source = op->source;
op->source = realpath (old_source, NULL);
if (op->source == NULL)
@@ -1529,6 +1590,8 @@ resolve_symlinks_in_ops (void)
}
break;
+ case SETUP_RO_OVERLAY_MOUNT:
+ case SETUP_TMP_OVERLAY_MOUNT:
case SETUP_MOUNT_PROC:
case SETUP_MOUNT_DEV:
case SETUP_MOUNT_TMPFS:
@@ -1620,6 +1683,32 @@ warn_only_last_option (const char *name)
warn ("Only the last %s option will take effect", name);
}
+static void
+make_setup_overlay_src_ops (const char *const *const argv)
+{
+ /* 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.
+ * This lets us reuse existing code paths to handle resolving the realpaths
+ * 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 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.
+ */
+ int i;
+ SetupOp *op;
+
+ for (i = 1; i <= next_overlay_src_count; i++)
+ {
+ op = setup_op_new (SETUP_OVERLAY_SRC);
+ op->source = argv[1 - 2 * i];
+ }
+ next_overlay_src_count = 0;
+}
+
static void
parse_args_recurse (int *argcp,
const char ***argvp,
@@ -1845,6 +1934,76 @@ parse_args_recurse (int *argcp,
argv += 2;
argc -= 2;
}
+ else if (strcmp (arg, "--overlay-src") == 0)
+ {
+ if (is_privileged)
+ die ("The --overlay-src option is not permitted in setuid mode");
+
+ next_overlay_src_count++;
+
+ argv += 1;
+ argc -= 1;
+ }
+ else if (strcmp (arg, "--overlay") == 0)
+ {
+ SetupOp *workdir_op;
+
+ if (is_privileged)
+ die ("The --overlay option is not permitted in setuid mode");
+
+ if (argc < 4)
+ die ("--overlay takes three arguments");
+
+ if (next_overlay_src_count < 1)
+ die ("--overlay requires at least one --overlay-src");
+
+ op = setup_op_new (SETUP_OVERLAY_MOUNT);
+ op->source = argv[1];
+ workdir_op = setup_op_new (SETUP_OVERLAY_SRC);
+ workdir_op->source = argv[2];
+ op->dest = argv[3];
+ make_setup_overlay_src_ops (argv);
+
+ argv += 3;
+ argc -= 3;
+ }
+ else if (strcmp (arg, "--tmp-overlay") == 0)
+ {
+ if (is_privileged)
+ die ("The --tmp-overlay option is not permitted in setuid mode");
+
+ if (argc < 2)
+ die ("--tmp-overlay takes an argument");
+
+ if (next_overlay_src_count < 1)
+ die ("--tmp-overlay requires at least one --overlay-src");
+
+ op = setup_op_new (SETUP_TMP_OVERLAY_MOUNT);
+ op->dest = argv[1];
+ make_setup_overlay_src_ops (argv);
+ opt_tmp_overlay_count++;
+
+ argv += 1;
+ argc -= 1;
+ }
+ else if (strcmp (arg, "--ro-overlay") == 0)
+ {
+ if (is_privileged)
+ die ("The --ro-overlay option is not permitted in setuid mode");
+
+ if (argc < 2)
+ die ("--ro-overlay takes an argument");
+
+ if (next_overlay_src_count < 2)
+ die ("--ro-overlay requires at least two --overlay-src");
+
+ op = setup_op_new (SETUP_RO_OVERLAY_MOUNT);
+ op->dest = argv[1];
+ make_setup_overlay_src_ops (argv);
+
+ argv += 1;
+ argc -= 1;
+ }
else if (strcmp (arg, "--proc") == 0)
{
if (argc < 2)
@@ -2514,6 +2673,10 @@ parse_args_recurse (int *argcp,
if (!is_modifier_option(arg) && next_size_arg != 0)
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");
+
argv++;
argc--;
}
@@ -2529,6 +2692,9 @@ parse_args (int *argcp,
int total_parsed_argc = *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");
}
static void
@@ -2633,6 +2799,7 @@ main (int argc,
int res UNUSED;
cleanup_free char *args_data UNUSED = NULL;
int intermediate_pids_sockets[2] = {-1, -1};
+ int i;
/* Handle --version early on before we try to acquire/drop
* any capabilities so it works in a build environment;
@@ -3073,6 +3240,19 @@ main (int argc,
if (mkdir ("oldroot", 0755))
die_with_error ("Creating oldroot failed");
+ for (i = 0; i < opt_tmp_overlay_count; i++)
+ {
+ char *dirname;
+ dirname = xasprintf ("tmp-overlay-upper-%d", i);
+ if (mkdir (dirname, 0755))
+ die_with_error ("Creating --tmp-overlay upperdir failed");
+ free (dirname);
+ dirname = xasprintf ("tmp-overlay-work-%d", i);
+ if (mkdir (dirname, 0755))
+ die_with_error ("Creating --tmp-overlay workdir failed");
+ free (dirname);
+ }
+
if (pivot_root (base_path, "oldroot"))
die_with_error ("pivot_root");
diff --git a/bwrap.xml b/bwrap.xml
index 4fe571ef..046d77fb 100644
--- a/bwrap.xml
+++ b/bwrap.xml
@@ -292,6 +292,71 @@
Remount the path DEST as readonly. It works only on the specified mount point, without changing any other mount point under the specified path
+
+
+
+
+ This option does nothing on its own, and must be followed by one of
+ the other overlay options. It specifies a host
+ path from which files should be read if they aren't present in a
+ higher layer.
+
+
+ This option can be used multiple times to provide multiple sources.
+ The sources are overlaid from bottom to top: if a given path to be
+ read exists in more than one source, the file is read from the last
+ such source specified.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Use overlayfs to mount the host paths specified by
+ RWSRC and all immediately preceding
+ on DEST.
+ DEST will contain the union of all the files
+ in all the layers.
+
+
+ With --overlay all writes will go to
+ RWSRC. Reads will come preferentially from
+ RWSRC, and then from any
+ paths.
+ WORKDIR must be an empty directory on the
+ same filesystem as RWSRC.
+
+
+ With --tmp-overlay all writes will go to
+ the tmpfs that hosts the sandbox root, in a location not accessible
+ from either the host or the child process. Writes will therefore not
+ be persisted across multiple runs.
+
+
+ With --ro-overlay the filesystem will be
+ mounted read-only. This option requires at least two
+ to precede it.
+
+
+ None of these options are available in the setuid version of
+ bubblewrap. Using --ro-overlay or providing
+ more than one requires a Linux kernel
+ version of 4.0 or later.
+
+
+ For more information see the Overlay Filesystem documentation in the
+ Linux kernel at
+ https://www.kernel.org/doc/Documentation/filesystems/overlayfs.txt
+
+
+
Mount procfs on DEST
diff --git a/completions/bash/bwrap b/completions/bash/bwrap
index ca18d896..9f1eb29f 100644
--- a/completions/bash/bwrap
+++ b/completions/bash/bwrap
@@ -50,15 +50,19 @@ _bwrap() {
--hostname
--info-fd
--lock-file
+ --overlay
+ --overlay-src
--perms
--proc
--remount-ro
--ro-bind
+ --ro-overlay
--seccomp
--setenv
--size
--symlink
--sync-fd
+ --tmp-overlay
--uid
--unsetenv
--userns-block-fd
diff --git a/tests/test-run.sh b/tests/test-run.sh
old mode 100755
new mode 100644
index 979480e2..5d851e26
--- a/tests/test-run.sh
+++ b/tests/test-run.sh
@@ -8,7 +8,7 @@ srcd=$(cd $(dirname "$0") && pwd)
bn=$(basename "$0")
-echo "1..58"
+echo "1..63"
# Test help
${BWRAP} --help > help.txt
@@ -523,4 +523,100 @@ echo "PWD=$(pwd -P)" > reference
assert_files_equal stdout reference
echo "ok - environment manipulation"
+if test -n "${bwrap_is_suid:-}"; then
+ # Test --overlay
+ mkdir lower1 lower2 upper work
+ printf 1 > lower1/a
+ printf 2 > lower1/b
+ printf 3 > lower2/b
+ printf 4 > upper/a
+ if $RUN --overlay upper work /tmp true 2>err.txt; then
+ assert_not_reached At least one --overlay-src not required
+ fi
+ assert_file_has_content err.txt "^bwrap: --overlay requires at least one --overlay-src"
+ $RUN --overlay-src lower1 --overlay upper work /tmp/x/y/z cat /tmp/x/y/z/a > stdout
+ assert_file_has_content stdout '^4$'
+ $RUN --overlay-src lower1 --overlay upper work /tmp/x/y/z cat /tmp/x/y/z/b > stdout
+ assert_file_has_content stdout '^2$'
+ $RUN --overlay-src lower1 --overlay-src lower2 --overlay upper work /tmp/x/y/z cat /tmp/x/y/z/a > stdout
+ assert_file_has_content stdout '^4$'
+ $RUN --overlay-src lower1 --overlay-src lower2 --overlay upper work /tmp/x/y/z cat /tmp/x/y/z/b > stdout
+ assert_file_has_content stdout '^3$'
+ $RUN --overlay-src lower1 --overlay-src lower2 --overlay upper work /tmp/x/y/z sh -c 'printf 5 > /tmp/x/y/z/b; cat /tmp/x/y/z/b' > stdout
+ assert_file_has_content stdout '^5$'
+ assert_file_has_content upper/b '^5$'
+ echo "ok - --overlay"
+
+ # Test --overlay path escaping
+ # Coincidentally, :,\ is the face I make contemplating anyone who might
+ # need this functionality, not that that's going to stop me from supporting
+ # it.
+ mkdir 'lower :,\' 'upper :,\' 'work :,\'
+ printf 1 > 'lower :,\'/a
+ $RUN --overlay-src 'lower :,\' --overlay 'upper :,\' 'work :,\' /tmp/x sh -c 'cat /tmp/x/a; printf 2 > /tmp/x/a; cat /tmp/x/a' > stdout
+ assert_file_has_content stdout '^12$'
+ assert_file_has_content 'lower :,\'/a '^1$'
+ assert_file_has_content 'upper :,\'/a '^2$'
+ echo "ok - --overlay path escaping"
+
+ # Test --tmp-overlay
+ printf 1 > lower1/a
+ printf 2 > lower1/b
+ printf 3 > lower2/b
+ if $RUN --tmp-overlay /tmp true 2>err.txt; then
+ assert_not_reached At least one --overlay-src not required
+ fi
+ assert_file_has_content err.txt "^bwrap: --tmp-overlay requires at least one --overlay-src"
+ $RUN --overlay-src lower1 --tmp-overlay /tmp/x/y/z cat /tmp/x/y/z/a > stdout
+ assert_file_has_content stdout '^1$'
+ $RUN --overlay-src lower1 --tmp-overlay /tmp/x/y/z cat /tmp/x/y/z/b > stdout
+ assert_file_has_content stdout '^2$'
+ $RUN --overlay-src lower1 --overlay-src lower2 --tmp-overlay /tmp/x/y/z cat /tmp/x/y/z/a > stdout
+ assert_file_has_content stdout '^1$'
+ $RUN --overlay-src lower1 --overlay-src lower2 --tmp-overlay /tmp/x/y/z cat /tmp/x/y/z/b > stdout
+ assert_file_has_content stdout '^3$'
+ $RUN --overlay-src lower1 --overlay-src lower2 --tmp-overlay /tmp/x/y/z sh -c 'printf 4 > /tmp/x/y/z/b; cat /tmp/x/y/z/b' > stdout
+ assert_file_has_content stdout '^4$'
+ $RUN --overlay-src lower1 --tmp-overlay /tmp/x --overlay-src lower2 --tmp-overlay /tmp/y sh -c 'cat /tmp/x/b; printf 4 > /tmp/x/b; cat /tmp/x/b; cat /tmp/y/b' > stdout
+ assert_file_has_content stdout '^243$'
+ echo "ok - --tmp-overlay"
+
+ # Test --ro-overlay
+ printf 1 > lower1/a
+ printf 2 > lower1/b
+ printf 3 > lower2/b
+ if $RUN --ro-overlay /tmp true 2>err.txt; then
+ assert_not_reached At least two --overlay-src not required
+ fi
+ assert_file_has_content err.txt "^bwrap: --ro-overlay requires at least two --overlay-src"
+ if $RUN --overlay-src lower1 --ro-overlay /tmp true 2>err.txt; then
+ assert_not_reached At least two --overlay-src not required
+ fi
+ assert_file_has_content err.txt "^bwrap: --ro-overlay requires at least two --overlay-src"
+ $RUN --overlay-src lower1 --overlay-src lower2 --ro-overlay /tmp/x/y/z cat /tmp/x/y/z/a > stdout
+ assert_file_has_content stdout '^1$'
+ $RUN --overlay-src lower1 --overlay-src lower2 --ro-overlay /tmp/x/y/z cat /tmp/x/y/z/b > stdout
+ assert_file_has_content stdout '^3$'
+ $RUN --overlay-src lower1 --overlay-src lower2 --ro-overlay /tmp/x/y/z sh -c 'printf 4 > /tmp/x/y/z/b; cat /tmp/x/y/z/b' > stdout
+ assert_file_has_content stdout '^3$'
+ echo "ok - --ro-overlay"
+
+ # Test --overlay-src restrictions
+ if $RUN --overlay-src /tmp true 2>err.txt; then
+ assert_not_reached Trailing --overlay-src allowed
+ fi
+ assert_file_has_content err.txt "^bwrap: --overlay-src must be followed by another --overlay-src or one of --overlay, --tmp-overlay, or --ro-overlay"
+ if $RUN --overlay-src /tmp --chdir / true 2>err.txt; then
+ assert_not_reached --overlay-src allowed to precede non-overlay options
+ fi
+ assert_file_has_content err.txt "^bwrap: --overlay-src must be followed by another --overlay-src or one of --overlay, --tmp-overlay, or --ro-overlay"
+ echo "ok - --overlay-src restrictions"
+else
+ echo "ok - # SKIP no --overlay support"
+ echo "ok - # SKIP no --overlay support"
+ echo "ok - # SKIP no --tmp-overlay support"
+ echo "ok - # SKIP no --ro-overlay support"
+ echo "ok - # SKIP no --overlay-src support"
+fi
+
echo "ok - End of test"
diff --git a/tests/test-utils.c b/tests/test-utils.c
index 41874a15..9f4aad0f 100644
--- a/tests/test-utils.c
+++ b/tests/test-utils.c
@@ -200,6 +200,23 @@ test_has_path_prefix (void)
}
}
+static void
+test_string_builder (void)
+{
+ StringBuilder sb = {0};
+
+ strappend (&sb, "aaa");
+ g_assert_cmpstr (sb.str, ==, "aaa");
+ strappend (&sb, "bbb");
+ g_assert_cmpstr (sb.str, ==, "aaabbb");
+ strappendf (&sb, "c%dc%s", 9, "x");
+ g_assert_cmpstr (sb.str, ==, "aaabbbc9cx");
+ strappend_escape_for_mount_options (&sb, "/path :,\\");
+ g_assert_cmpstr (sb.str, ==, "aaabbbc9cx/path \\:\\,\\\\");
+ strappend (&sb, "zzz");
+ g_assert_cmpstr (sb.str, ==, "aaabbbc9cx/path \\:\\,\\\\zzz");
+}
+
int
main (int argc UNUSED,
char **argv UNUSED)
@@ -210,6 +227,7 @@ main (int argc UNUSED,
test_strconcat3 ();
test_has_prefix ();
test_has_path_prefix ();
+ test_string_builder ();
printf ("1..%u\n", test_number);
return 0;
}
diff --git a/utils.c b/utils.c
index 693273b5..b97d1c48 100644
--- a/utils.c
+++ b/utils.c
@@ -21,6 +21,7 @@
#include "utils.h"
#include
#include
+#include
#ifdef HAVE_SELINUX
#include
#endif
@@ -889,3 +890,82 @@ label_exec (UNUSED const char *exec_label)
#endif
return 0;
}
+
+void
+strappend (StringBuilder *dest, const char *src)
+{
+ size_t len = strlen (src);
+
+ if (dest->offset + len >= dest->size)
+ {
+ dest->size = (dest->size + len + 1) * 2;
+ dest->str = xrealloc (dest->str, dest->size);
+ }
+
+ strncpy (dest->str + dest->offset, src, len + 1);
+ dest->offset += len;
+}
+
+__attribute__((format (printf, 2, 3)))
+void
+strappendf (StringBuilder *dest, const char *fmt, ...)
+{
+ va_list args;
+ int len;
+
+ va_start (args, fmt);
+ len = vsnprintf (dest->str + dest->offset, dest->size - dest->offset, fmt, args);
+ va_end (args);
+ if (len < 0)
+ die_with_error ("vsnprintf");
+ if (dest->offset + len >= dest->size)
+ {
+ dest->size = (dest->size + len + 1) * 2;
+ dest->str = xrealloc (dest->str, dest->size);
+ va_start (args, fmt);
+ len = vsnprintf (dest->str + dest->offset, dest->size - dest->offset, fmt, args);
+ va_end (args);
+ if (len < 0)
+ die_with_error ("vsnprintf");
+ }
+
+ dest->offset += len;
+}
+
+void
+strappend_escape_for_mount_options (StringBuilder *dest, const char *src)
+{
+ bool unescaped = TRUE;
+
+ for (;;)
+ {
+ if (dest->offset == dest->size)
+ {
+ dest->size = MAX (64, dest->size * 2);
+ dest->str = xrealloc (dest->str, dest->size);
+ }
+ switch (*src)
+ {
+ case '\0':
+ dest->str[dest->offset] = '\0';
+ return;
+
+ case '\\':
+ case ',':
+ case ':':
+ if (unescaped)
+ {
+ dest->str[dest->offset++] = '\\';
+ unescaped = FALSE;
+ continue;
+ }
+ /* else fall through */
+
+ default:
+ dest->str[dest->offset++] = *src;
+ unescaped = 1;
+ break;
+ }
+ src++;
+ }
+}
diff --git a/utils.h b/utils.h
index 37d8c7c0..a7ce5c40 100644
--- a/utils.h
+++ b/utils.h
@@ -181,3 +181,20 @@ steal_pointer (void *pp)
/* type safety */
#define steal_pointer(pp) \
(0 ? (*(pp)) : (steal_pointer) (pp))
+
+typedef struct _StringBuilder StringBuilder;
+
+struct _StringBuilder
+{
+ char * str;
+ size_t size;
+ size_t offset;
+};
+
+void strappend (StringBuilder *dest,
+ const char *src);
+void strappendf (StringBuilder *dest,
+ const char *fmt,
+ ...);
+void strappend_escape_for_mount_options (StringBuilder *dest,
+ const char *src);