diff --git a/bubblewrap.c b/bubblewrap.c
index 405a604ec40619434260b7bb296dbe96102c586f..f8728c7e127838201061318c32ee42fe4533eeae 100644
--- a/bubblewrap.c
+++ b/bubblewrap.c
@@ -91,8 +91,10 @@ static char *opt_args_data = NULL;  /* owned */
 static int opt_userns_fd = -1;
 static int opt_userns2_fd = -1;
 static int opt_pidns_fd = -1;
+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;
 
 #define CAP_TO_MASK_0(x) (1L << ((x) & 31))
 #define CAP_TO_MASK_1(x) CAP_TO_MASK_0(x - 32)
@@ -122,6 +124,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,
@@ -167,6 +173,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,
@@ -336,6 +343,11 @@ usage (int ecode, FILE *out)
            "    --bind-fd FD DEST            Bind open directory or path fd on DEST\n"
            "    --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 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"
@@ -1141,6 +1153,20 @@ privileged_op (int         privileged_op_socket,
         die_with_mount_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)
+        {
+          /* The standard message for ELOOP, "Too many levels of symbolic
+           * links", is not helpful here. */
+          if (errno == ELOOP)
+            die ("Can't make overlay mount on %s with options %s: "
+                "Overlay directories may not overlap",
+                arg2, arg1);
+          die_with_mount_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. */
@@ -1164,6 +1190,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)
     {
@@ -1251,6 +1278,47 @@ setup_newroot (bool unshare_pid,
 
           break;
 
+        case SETUP_OVERLAY_MOUNT:
+        case SETUP_RO_OVERLAY_MOUNT:
+        case SETUP_TMP_OVERLAY_MOUNT:
+          {
+            StringBuilder sb = {0};
+            bool multi_src = false;
+
+            if (ensure_dir (dest, 0755) != 0)
+              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;
+              }
+
+            strappend (&sb, ",userxattr");
+
+            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);
@@ -1514,6 +1582,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);
         }
@@ -1556,6 +1625,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)
@@ -1567,6 +1638,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:
@@ -1658,6 +1731,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 (directly) 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,
@@ -1923,6 +2022,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)
@@ -2592,6 +2761,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--;
     }
@@ -2607,6 +2780,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
@@ -2712,6 +2888,7 @@ main (int    argc,
   cleanup_free char *args_data UNUSED = NULL;
   int intermediate_pids_sockets[2] = {-1, -1};
   const char *exec_path = NULL;
+  int i;
 
   /* Handle --version early on before we try to acquire/drop
    * any capabilities so it works in a build environment;
@@ -3152,6 +3329,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 2bfca00c295919fa58a499828c159d77cc4802a4..f379f0fa46a98ddb648efef6ca9e49e29b7e8c4f 100644
--- a/bwrap.xml
+++ b/bwrap.xml
@@ -317,6 +317,86 @@
       <term><option>--remount-ro <arg choice="plain">DEST</arg></option></term>
       <listitem><para>Remount the path <arg choice="plain">DEST</arg> as readonly.  It works only on the specified mount point, without changing any other mount point under the specified path</para></listitem>
     </varlistentry>
+    <varlistentry>
+      <term><option>--overlay-src <arg choice="plain">SRC</arg></option></term>
+      <listitem>
+        <para>
+          This option does nothing on its own, and must be followed by one of
+          the other <literal>overlay</literal> options. It specifies a host
+          path from which files should be read if they aren't present in a
+          higher layer.
+        </para>
+        <para>
+          This option can be used multiple times to provide multiple sources.
+          The sources are overlaid in the order given, with the first source on
+          the command line at the bottom of the stack: if a given path to be
+          read exists in more than one source, the file is read from the last
+          such source specified.
+        </para>
+        <para>
+          (For readers familiar with overlayfs, note that this is the
+          reverse of the order used by the kernel's <literal>lowerdir</literal>
+          mount option.)
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term><option>--overlay <arg choice="plain">RWSRC</arg> <arg choice="plain">WORKDIR</arg> <arg choice="plain">DEST</arg></option></term>
+    </varlistentry>
+    <varlistentry>
+      <term><option>--tmp-overlay <arg choice="plain">DEST</arg></option></term>
+    </varlistentry>
+    <varlistentry>
+      <term><option>--ro-overlay <arg choice="plain">DEST</arg></option></term>
+      <listitem>
+        <para>
+          Use overlayfs to mount the host paths specified by
+          <arg choice="plain">RWSRC</arg> and all immediately preceding
+          <option>--overlay-src</option> on <arg choice="plain">DEST</arg>.
+          <arg choice="plain">DEST</arg> will contain the union of all the files
+          in all the layers.
+        </para>
+        <para>
+          With <arg choice="plain">--overlay</arg> all writes will go to
+          <arg choice="plain">RWSRC</arg>. Reads will come preferentially from
+          <arg choice="plain">RWSRC</arg>, and then from any
+          <option>--overlay-src</option> paths.
+          <arg choice="plain">WORKDIR</arg> must be an empty directory on the
+          same filesystem as <arg choice="plain">RWSRC</arg>, and is used
+          internally by the kernel.
+        </para>
+        <para>
+          With <arg choice="plain">--tmp-overlay</arg> 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.
+        </para>
+        <para>
+          With <arg choice="plain">--ro-overlay</arg> the filesystem will be
+          mounted read-only. This option requires at least two
+          <option>--overlay-src</option> to precede it.
+        </para>
+        <para>
+          None of these options are available in the setuid version of
+          bubblewrap. Using <arg choice="plain">--ro-overlay</arg> or providing
+          more than one <option>--overlay-src</option> requires a Linux kernel
+          version of 4.0 or later.
+        </para>
+        <para>
+          Due to limitations of overlayfs, no host directory given via
+          <arg choice="plain">--overlay-src</arg> or
+          <arg choice="plain">--overlay</arg> may be an ancestor of another,
+          after resolving symlinks. Depending on version, the Linux kernel may
+          or may not enforce this, but if not then overlayfs's behavior is
+          undefined.
+        </para>
+        <para>
+          For more information see the Overlay Filesystem documentation in the
+          Linux kernel at
+          https://www.kernel.org/doc/Documentation/filesystems/overlayfs.txt
+        </para>
+      </listitem>
+    </varlistentry>
     <varlistentry>
       <term><option>--proc <arg choice="plain">DEST</arg></option></term>
       <listitem><para>Mount procfs on <arg choice="plain">DEST</arg></para></listitem>
diff --git a/completions/bash/bwrap b/completions/bash/bwrap
index e6d28462b58df64833896d59cb13abfd4fee60ae..e7a523c2850036b4e882e795e7b7aab5603087e8 100644
--- a/completions/bash/bwrap
+++ b/completions/bash/bwrap
@@ -51,15 +51,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
index 74541381e869e0b67fb16a9d33809bbdd5487717..1003e8c4b1d593dae19cae1d55239085ae112780 100755
--- a/tests/test-run.sh
+++ b/tests/test-run.sh
@@ -579,4 +579,114 @@ $RUN --level-prefix --chdir / --chdir / true > stdout 2>&1
 assert_file_has_content stdout '^<4>bwrap: Only the last --chdir option will take effect$'
 ok "--level-prefix"
 
+if test -n "${bwrap_is_suid:-}"; then
+    ok_skip "no --overlay support"
+    ok_skip "no --overlay support"
+    ok_skip "no --tmp-overlay support"
+    ok_skip "no --ro-overlay support"
+    ok_skip "no --overlay-src support"
+else
+    mkdir lower1 lower2 upper work
+    printf 1 > lower1/a
+    printf 2 > lower1/b
+    printf 3 > lower2/b
+    printf 4 > upper/a
+
+    # Check if unprivileged overlayfs is available
+    if ! unshare -rm mount -t overlay -o lowerdir=lower1,upperdir=upper,workdir=work,userxattr overlay lower2; then
+        ok_skip "no kernel support for unprivileged overlayfs"
+        ok_skip "no kernel support for unprivileged overlayfs"
+        ok_skip "no kernel support for unprivileged overlayfs"
+        ok_skip "no kernel support for unprivileged overlayfs"
+        ok_skip "no kernel support for unprivileged overlayfs"
+    else
+
+        # Test --overlay
+        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$'
+        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$'
+        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$'
+        assert_file_has_content lower1/b '^2$'
+        assert_file_has_content lower2/b '^3$'
+        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$'
+        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"
+        ok "--overlay-src restrictions"
+
+    fi
+fi
+
 done_testing
diff --git a/tests/test-utils.c b/tests/test-utils.c
index feba8b7c367eaa7fd7d0b1bfa88bc8e23f3a010c..64f42ff18c0dba3877e9bf2f3eb92b02a48f1a39 100644
--- a/tests/test-utils.c
+++ b/tests/test-utils.c
@@ -200,6 +200,37 @@ 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");
+
+  free (sb.str);
+  sb = (StringBuilder){0};
+
+  strappend_escape_for_mount_options (&sb, "aaa");
+  g_assert_cmpstr (sb.str, ==, "aaa");
+
+  free (sb.str);
+  sb = (StringBuilder){0};
+
+  strappend_escape_for_mount_options (&sb, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+  g_assert_cmpstr (sb.str, ==, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+
+  free (sb.str);
+}
+
 int
 main (int argc UNUSED,
       char **argv UNUSED)
@@ -210,6 +241,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 c45e36dbfba1dfc4750b31d006aeb39815945743..029bb8ad2ea79939c672e33993e35cbfc6924814 100644
--- a/utils.c
+++ b/utils.c
@@ -23,6 +23,7 @@
 #include <stdint.h>
 #include <sys/syscall.h>
 #include <sys/socket.h>
+#include <sys/param.h>
 #ifdef HAVE_SELINUX
 #include <selinux/selinux.h>
 #endif
@@ -950,3 +951,106 @@ mount_strerror (int errsv)
         return strerror (errsv);
     }
 }
+
+static size_t
+xadd (size_t a, size_t b)
+{
+  size_t result;
+  if (__builtin_add_overflow (a, b, &result))
+    die_oom ();
+  return result;
+}
+
+static size_t
+xmul (size_t a, size_t b)
+{
+  size_t result;
+  if (__builtin_mul_overflow (a, b, &result))
+    die_oom ();
+  return result;
+}
+
+void
+strappend (StringBuilder *dest, const char *src)
+{
+  size_t len = strlen (src);
+  size_t new_offset = xadd (dest->offset, len);
+
+  if (new_offset >= dest->size)
+    {
+      dest->size = xmul (xadd (new_offset, 1), 2);
+      dest->str = xrealloc (dest->str, dest->size);
+    }
+
+  /* Preserves the invariant that dest->str is always null-terminated, even
+   * though the offset is positioned at the null byte for the next write.
+   */
+  strncpy (dest->str + dest->offset, src, len + 1);
+  dest->offset = new_offset;
+}
+
+__attribute__((format (printf, 2, 3)))
+void
+strappendf (StringBuilder *dest, const char *fmt, ...)
+{
+  va_list args;
+  int len;
+  size_t new_offset;
+
+  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");
+  new_offset = xadd (dest->offset, len);
+  if (new_offset >= dest->size)
+    {
+      dest->size = xmul (xadd (new_offset, 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 = new_offset;
+}
+
+void
+strappend_escape_for_mount_options (StringBuilder *dest, const char *src)
+{
+  bool unescaped = true;
+
+  for (;;)
+    {
+      if (dest->offset == dest->size)
+        {
+          dest->size = MAX (64, xmul (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 = true;
+          break;
+        }
+      src++;
+    }
+}
diff --git a/utils.h b/utils.h
index 841254dbb9a6cf6754e535e04849ab9a851fed47..079fe7c9362e04b952544b1daab0331c2dbe16f1 100644
--- a/utils.h
+++ b/utils.h
@@ -198,3 +198,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);