diff --git a/README.md b/README.md
index 5ad22fcadd01cdfd4cf9250fdf194a9ccaae00a9..14670054f8b55c1f6dd42304c819f2a07dc6c1dd 100644
--- a/README.md
+++ b/README.md
@@ -446,6 +446,32 @@ configured.
     The interactive shell's current working directory matches the game's.
     As with the xterm, run `"$@"` in the interactive shell to run the game.
 
+* To get a modifiable copy of the runtime, use something like:
+
+        mkdir -p $HOME/.steam/steam/steamapps/common/SteamLinuxRuntime/var
+        /opt/pressure-vessel/bin/pressure-vessel-unruntime \
+            --runtime=$HOME/.steam/steam/steamapps/common/SteamLinuxRuntime/scout/files \
+            --copy-runtime-into=$HOME/.steam/steam/steamapps/common/SteamLinuxRuntime/var \
+            -- %command%
+
+    You will find a temporary copy of the runtime in the directory you
+    chose, in this example something like
+    `.../SteamLinuxRuntime/var/tmp-1234567`. You can modify it in-place.
+    For example, if you want to replace `libdbus-1.so.3` with an
+    instrumented version, you can do something like this:
+
+        cd .../SteamLinuxRuntime/var/tmp-1234567
+        rm -f overrides/lib/x86_64-linux-gnu/libdbus-1.so.3*
+        rm -f usr/lib/x86_64-linux-gnu/libdbus-1.so.3*
+        cp --dereference ~/dbus/dbus/.libs/libdbus-1.so.3 \
+            overrides/lib/x86_64-linux-gnu/libdbus-1.so.3
+
+    To avoid temporary copies building up forever, they will be deleted
+    *next* time you run a game in a container (assuming there is no longer
+    anything running in the previous container), unless you create a file
+    like `.../var/tmp-1234567/keep` to flag this root directory to be kept
+    for future reference.
+
 ### More options
 
 Use `pressure-vessel-unruntime` or `pressure-vessel-unruntime-test-ui`
diff --git a/src/locking.md b/src/locking.md
new file mode 100644
index 0000000000000000000000000000000000000000..adbd4caa8f3d9b608d4571b01aa65fe439a345a8
--- /dev/null
+++ b/src/locking.md
@@ -0,0 +1,55 @@
+pressure-vessel file locking model
+==================================
+
+pressure-vessel uses fcntl locks to guard concurrent access to runtimes.
+They are `F_OFD_SETLK` locks if possible, falling back to POSIX
+process-oriented `F_SETLK` locks on older kernels. See `src/bwrap-lock.c`
+for full details.
+
+Do not use `flock(2)`, `flock(1)` or `lockf(3)` to interact with
+pressure-vessel. `bwrap --lock-file` or `pressure-vessel-with-lock`
+can be used.
+
+Runtimes
+--------
+
+When pressure-vessel-wrap is told to use a runtime, for example
+`scout/files`, it takes out a shared (read) lock on the file `.ref`
+at top level, for example `scout/files/.ref`.
+
+If told to make a mutable copy of the runtime, this lock is only held
+for as long as it takes to carry out setup. There is a separate lock on
+the mutable copy (see below).
+
+If the runtime is used directly, the lock file may appear as either
+`/usr/.ref` or `/.ref` inside the container. If the runtime is a merged
+/usr rather than a complete root filesystem, the lock file will be
+`/usr/.ref` with a symbolic link `/.ref -> usr/.ref`. If the runtime is
+a complete root filesystem, the lock file will be `/.ref`. Processes
+inside the container wishing to take out the lock must use `/.ref`.
+
+Processes wishing to clean up obsolete runtimes should take out an
+exclusive (write) lock on the runtime's `/.ref`, for example
+`scout/files/.ref`, to prevent deletion of a runtime that is in use.
+
+Mutable copies
+--------------
+
+pressure-vessel-wrap can be told to make a mutable copy of the runtime
+with `--runtime-mutable-parent=${parent}`.
+
+While setting this up, it takes out a shared (read) lock on
+`${parent}/.ref`. During setup, it additionally takes out a shared (read)
+lock on `${parent}/tmp-${random}/.ref`, which continues to be held after
+setup has finished and the lock on the parent directory has been released.
+(Implementation detail: that's actually a symbolic link to `usr/.ref`.)
+
+Processes wishing to clean up the mutable copies must take an
+exclusive (write) lock on `${parent}/.ref`, to prevent
+pressure-vessel-wrap from creating new mutable copies during cleanup.
+
+A process that is holding the lock on the parent may detect whether each
+mutable runtime `${parent}/tmp-${random}/` is in use by attempting to
+take out an exclusive (write) lock on `${parent}/tmp-${random}/.ref`
+(to avoid deadlocks, this is best done in non-blocking mode). If it
+successfully takes the lock, it may delete the runtime.
diff --git a/src/runtime.c b/src/runtime.c
index 71a62fc7b4f8c210bb1006f7f3184f8c3241a0a6..cd61d80e0a835067de4f2718aad83384f08eaaa2 100644
--- a/src/runtime.c
+++ b/src/runtime.c
@@ -32,8 +32,10 @@
 
 #include "bwrap.h"
 #include "bwrap-lock.h"
+#include "elf-utils.h"
 #include "enumtypes.h"
 #include "flatpak-run-private.h"
+#include "resolve-in-sysroot.h"
 #include "utils.h"
 
 /*
@@ -51,14 +53,20 @@ struct _PvRuntime
   gchar *tools_dir;
   PvBwrapLock *runtime_lock;
 
+  gchar *mutable_parent;
+  gchar *mutable_sysroot;
   gchar *tmpdir;
   gchar *overrides;
-  gchar *overrides_bin;
+  const gchar *overrides_in_container;
   gchar *container_access;
   FlatpakBwrap *container_access_adverb;
-  gchar *runtime_usr;           /* either source_files or that + "/usr" */
+  const gchar *runtime_files;   /* either source_files or mutable_sysroot */
+  gchar *runtime_usr;           /* either runtime_files or that + "/usr" */
+  const gchar *with_lock_in_container;
 
   PvRuntimeFlags flags;
+  int mutable_parent_fd;
+  int mutable_sysroot_fd;
   gboolean any_libc_from_host;
   gboolean all_libc_from_host;
   gboolean runtime_is_just_usr;
@@ -73,6 +81,7 @@ enum {
   PROP_0,
   PROP_BUBBLEWRAP,
   PROP_FLAGS,
+  PROP_MUTABLE_PARENT,
   PROP_SOURCE_FILES,
   PROP_TOOLS_DIRECTORY,
   N_PROPERTIES
@@ -87,6 +96,38 @@ G_DEFINE_TYPE_WITH_CODE (PvRuntime, pv_runtime, G_TYPE_OBJECT,
                          G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE,
                                                 pv_runtime_initable_iface_init))
 
+/*
+ * Return whether @path is likely to be visible in /run/host.
+ * This needs to be kept approximately in sync with pv_bwrap_bind_usr()
+ * and Flatpak's --filesystem=host-os special keyword.
+ *
+ * This doesn't currently handle /etc: we make the pessimistic assumption
+ * that /etc/ld.so.cache, etc., are not shared.
+ */
+static gboolean
+path_visible_in_run_host (const char *path)
+{
+  while (path[0] == '/')
+    path++;
+
+  if (g_str_has_prefix (path, "usr") &&
+      (path[3] == '\0' || path[3] == '/'))
+    return TRUE;
+
+  if (g_str_has_prefix (path, "lib"))
+    return TRUE;
+
+  if (g_str_has_prefix (path, "bin") &&
+      (path[3] == '\0' || path[3] == '/'))
+    return TRUE;
+
+  if (g_str_has_prefix (path, "sbin") ||
+      (path[4] == '\0' || path[4] == '/'))
+    return TRUE;
+
+  return FALSE;
+}
+
 /*
  * Supported Debian-style multiarch tuples
  */
@@ -143,8 +184,8 @@ runtime_architecture_init (RuntimeArchitecture *self,
                                                  NULL);
   self->libdir_on_host = g_build_filename (runtime->overrides, "lib",
                                            self->tuple, NULL);
-  self->libdir_in_container = g_build_filename ("/overrides", "lib",
-                                                self->tuple, NULL);
+  self->libdir_in_container = g_build_filename (runtime->overrides_in_container,
+                                                "lib", self->tuple, NULL);
 
   /* This has the side-effect of testing whether we can run binaries
    * for this architecture on the host system. */
@@ -201,6 +242,8 @@ pv_runtime_init (PvRuntime *self)
 {
   self->any_libc_from_host = FALSE;
   self->all_libc_from_host = FALSE;
+  self->mutable_parent_fd = -1;
+  self->mutable_sysroot_fd = -1;
 }
 
 static void
@@ -221,6 +264,10 @@ pv_runtime_get_property (GObject *object,
         g_value_set_flags (value, self->flags);
         break;
 
+      case PROP_MUTABLE_PARENT:
+        g_value_set_string (value, self->mutable_parent);
+        break;
+
       case PROP_SOURCE_FILES:
         g_value_set_string (value, self->source_files);
         break;
@@ -241,6 +288,7 @@ pv_runtime_set_property (GObject *object,
                          GParamSpec *pspec)
 {
   PvRuntime *self = PV_RUNTIME (object);
+  const char *path;
 
   switch (prop_id)
     {
@@ -254,10 +302,42 @@ pv_runtime_set_property (GObject *object,
         self->flags = g_value_get_flags (value);
         break;
 
+      case PROP_MUTABLE_PARENT:
+        /* Construct-only */
+        g_return_if_fail (self->mutable_parent == NULL);
+        path = g_value_get_string (value);
+
+        if (path != NULL)
+          {
+            self->mutable_parent = realpath (path, NULL);
+
+            if (self->mutable_parent == NULL)
+              {
+                /* It doesn't exist. Keep the non-canonical path so we
+                 * can warn about it later */
+                self->mutable_parent = g_strdup (path);
+              }
+          }
+
+        break;
+
       case PROP_SOURCE_FILES:
         /* Construct-only */
         g_return_if_fail (self->source_files == NULL);
-        self->source_files = g_value_dup_string (value);
+        path = g_value_get_string (value);
+
+        if (path != NULL)
+          {
+            self->source_files = realpath (path, NULL);
+
+            if (self->source_files == NULL)
+              {
+                /* It doesn't exist. Keep the non-canonical path so we
+                 * can warn about it later */
+                self->source_files = g_strdup (path);
+              }
+          }
+
         break;
 
       case PROP_TOOLS_DIRECTORY:
@@ -283,6 +363,292 @@ pv_runtime_constructed (GObject *object)
   g_return_if_fail (self->tools_dir != NULL);
 }
 
+static gboolean
+pv_runtime_garbage_collect (PvRuntime *self,
+                            PvBwrapLock *mutable_parent_lock,
+                            GError **error)
+{
+  g_auto(GLnxDirFdIterator) iter = { FALSE };
+
+  g_return_val_if_fail (PV_IS_RUNTIME (self), FALSE);
+  g_return_val_if_fail (self->mutable_parent != NULL, FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+  /* We don't actually *use* this: it just acts as an assertion that
+   * we are holding the lock on the parent directory. */
+  g_return_val_if_fail (mutable_parent_lock != NULL, FALSE);
+
+  if (!glnx_dirfd_iterator_init_at (AT_FDCWD, self->mutable_parent,
+                                    TRUE, &iter, error))
+    return FALSE;
+
+  while (TRUE)
+    {
+      g_autoptr(GError) local_error = NULL;
+      g_autoptr(PvBwrapLock) temp_lock = NULL;
+      g_autofree gchar *keep = NULL;
+      g_autofree gchar *ref = NULL;
+      struct dirent *dent;
+      struct stat ignore;
+
+      if (!glnx_dirfd_iterator_next_dent_ensure_dtype (&iter, &dent,
+                                                       NULL, error))
+        return FALSE;
+
+      if (dent == NULL)
+        break;
+
+      switch (dent->d_type)
+        {
+          case DT_DIR:
+            break;
+
+          case DT_BLK:
+          case DT_CHR:
+          case DT_FIFO:
+          case DT_LNK:
+          case DT_REG:
+          case DT_SOCK:
+          case DT_UNKNOWN:
+          default:
+            g_debug ("Ignoring %s/%s: not a directory",
+                     self->mutable_parent, dent->d_name);
+            continue;
+        }
+
+      if (!g_str_has_prefix (dent->d_name, "tmp-"))
+        {
+          g_debug ("Ignoring %s/%s: not tmp-*",
+                   self->mutable_parent, dent->d_name);
+          continue;
+        }
+
+      g_debug ("Found temporary runtime %s/%s, considering whether to "
+               "delete it...",
+               self->mutable_parent, dent->d_name);
+
+      keep = g_build_filename (dent->d_name, "keep", NULL);
+
+      if (glnx_fstatat (self->mutable_parent_fd, keep, &ignore,
+                        AT_SYMLINK_NOFOLLOW, &local_error))
+        {
+          g_debug ("Not deleting \"%s/%s\": ./keep exists",
+                   self->mutable_parent, dent->d_name);
+          continue;
+        }
+      else if (!g_error_matches (local_error, G_IO_ERROR,
+                                 G_IO_ERROR_NOT_FOUND))
+        {
+          /* EACCES or something? Give it the benefit of the doubt */
+          g_warning ("Not deleting \"%s/%s\": unable to stat ./keep: %s",
+                   self->mutable_parent, dent->d_name, local_error->message);
+          g_clear_error (&local_error);
+          continue;
+        }
+
+      g_clear_error (&local_error);
+
+      ref = g_build_filename (dent->d_name, ".ref", NULL);
+      temp_lock = pv_bwrap_lock_new (self->mutable_parent_fd, ref,
+                                     (PV_BWRAP_LOCK_FLAGS_CREATE |
+                                      PV_BWRAP_LOCK_FLAGS_WRITE),
+                                     &local_error);
+
+      if (temp_lock == NULL)
+        {
+          g_debug ("Ignoring \"%s/%s\": unable to get lock: %s",
+                   self->mutable_parent, dent->d_name,
+                   local_error->message);
+          g_clear_error (&local_error);
+          continue;
+        }
+
+      g_debug ("Deleting \"%s/%s\"...", self->mutable_parent, dent->d_name);
+
+      /* We have the lock, which would not have happened if someone was
+       * still using the runtime, so we can safely delete it. */
+      if (!glnx_shutil_rm_rf_at (self->mutable_parent_fd, dent->d_name,
+                                 NULL, &local_error))
+        {
+          g_debug ("Unable to delete %s/%s: %s",
+                   self->mutable_parent, dent->d_name, local_error->message);
+          g_clear_error (&local_error);
+          continue;
+        }
+    }
+
+  return TRUE;
+}
+
+static gboolean
+pv_runtime_init_mutable (PvRuntime *self,
+                         GError **error)
+{
+  g_autofree gchar *dest_usr = NULL;
+  g_autofree gchar *source_usr_subdir = NULL;
+  g_autofree gchar *temp_dir = NULL;
+  g_autoptr(GDir) dir = NULL;
+  g_autoptr(PvBwrapLock) copy_lock = NULL;
+  g_autoptr(PvBwrapLock) mutable_lock = NULL;
+  g_autoptr(PvBwrapLock) source_lock = NULL;
+  const char *member;
+  const char *source_usr;
+  glnx_autofd int temp_dir_fd = -1;
+  gboolean is_just_usr;
+
+  /* Nothing to do in this case */
+  if (self->mutable_parent == NULL)
+    return TRUE;
+
+  if (g_mkdir_with_parents (self->mutable_parent, 0700) != 0)
+    return glnx_throw_errno_prefix (error, "Unable to create %s",
+                                    self->mutable_parent);
+
+  if (!glnx_opendirat (AT_FDCWD, self->mutable_parent, TRUE,
+                       &self->mutable_parent_fd, error))
+    return FALSE;
+
+  /* Lock the parent directory. Anything that directly manipulates the
+   * temporary runtimes is expected to do the same, so that
+   * it cannot be deleting temporary runtimes at the same time we're
+   * creating them.
+   *
+   * This is a read-mode lock: it's OK to create more than one temporary
+   * runtime in parallel, as long as nothing is deleting them
+   * concurrently. */
+  mutable_lock = pv_bwrap_lock_new (self->mutable_parent_fd, ".ref",
+                                    PV_BWRAP_LOCK_FLAGS_CREATE,
+                                    error);
+
+  if (mutable_lock == NULL)
+    return glnx_prefix_error (error, "Unable to lock \"%s/%s\"",
+                              self->mutable_parent, ".ref");
+
+  /* GC old runtimes (if they have become unused) before we create a
+   * new one. This means we should only ever have one temporary runtime
+   * copy per game that is run concurrently. */
+  if ((self->flags & PV_RUNTIME_FLAGS_GC_RUNTIMES) != 0 &&
+      !pv_runtime_garbage_collect (self, mutable_lock, error))
+    return FALSE;
+
+  temp_dir = g_build_filename (self->mutable_parent, "tmp-XXXXXX", NULL);
+
+  if (g_mkdtemp (temp_dir) == NULL)
+    return glnx_throw_errno_prefix (error,
+                                    "Cannot create temporary directory \"%s\"",
+                                    temp_dir);
+
+  source_usr_subdir = g_build_filename (self->source_files, "usr", NULL);
+  dest_usr = g_build_filename (temp_dir, "usr", NULL);
+
+  is_just_usr = !g_file_test (source_usr_subdir, G_FILE_TEST_IS_DIR);
+
+  if (is_just_usr)
+    {
+      /* ${source_files}/usr does not exist, so assume it's a merged /usr,
+       * for example ./scout/files. Copy ${source_files}/bin to
+       * ${temp_dir}/usr/bin, etc. */
+      source_usr = self->source_files;
+
+      if (!pv_cheap_tree_copy (self->source_files, dest_usr, error))
+        return FALSE;
+    }
+  else
+    {
+      /* ${source_files}/usr exists, so assume it's a complete sysroot.
+       * Copy ${source_files}/bin to ${temp_dir}/bin, etc. */
+      source_usr = source_usr_subdir;
+
+      if (!pv_cheap_tree_copy (self->source_files, temp_dir, error))
+        return FALSE;
+    }
+
+  if (!glnx_opendirat (-1, temp_dir, FALSE, &temp_dir_fd, error))
+    return FALSE;
+
+  /* Create the copy in a pre-locked state. After the lock on the parent
+   * directory is released, the copy continues to have a read lock,
+   * preventing it from being modified or deleted while in use (even if
+   * a cleanup process successfully obtains a write lock on the parent).
+   *
+   * Because we control the structure of the runtime in this case, we
+   * actually lock /usr/.ref instead of /.ref, and ensure that /.ref
+   * is a symlink to it. This might become important if we pass the
+   * runtime's /usr to Flatpak, which normally takes out a lock on
+   * /usr/.ref (obviously this will only work if the runtime happens
+   * to be merged-/usr). */
+  copy_lock = pv_bwrap_lock_new (temp_dir_fd, "usr/.ref",
+                                 PV_BWRAP_LOCK_FLAGS_CREATE,
+                                 error);
+
+  if (copy_lock == NULL)
+    return glnx_prefix_error (error,
+                              "Unable to lock \"%s/.ref\" in temporary runtime",
+                              dest_usr);
+
+  if (is_just_usr)
+    {
+      g_autofree gchar *in_root = g_build_filename (temp_dir, ".ref", NULL);
+
+      if (unlink (in_root) != 0 && errno != ENOENT)
+        return glnx_throw_errno_prefix (error,
+                                        "Cannot remove \"%s\"", in_root);
+
+      if (symlink ("usr/.ref", in_root) != 0)
+        return glnx_throw_errno_prefix (error,
+                                        "Cannot create symlink \"%s\" -> usr/.ref",
+                                        in_root);
+    }
+
+  dir = g_dir_open (source_usr, 0, error);
+
+  if (dir == NULL)
+    return FALSE;
+
+  for (member = g_dir_read_name (dir);
+       member != NULL;
+       member = g_dir_read_name (dir))
+    {
+      /* Create symlinks ${temp_dir}/bin -> usr/bin, etc. if missing.
+       *
+       * Also make ${temp_dir}/etc, ${temp_dir}/var symlinks to etc
+       * and var, for the benefit of tools like capsule-capture-libs
+       * accessing /etc/ld.so.cache in the incomplete container (for the
+       * final container command-line they get merged by bind_runtime()
+       * instead). */
+      if (g_str_equal (member, "bin") ||
+          g_str_equal (member, "etc") ||
+          (g_str_has_prefix (member, "lib") &&
+           !g_str_equal (member, "libexec")) ||
+          g_str_equal (member, "sbin") ||
+          g_str_equal (member, "var"))
+        {
+          g_autofree gchar *dest = g_build_filename (temp_dir, member, NULL);
+          g_autofree gchar *target = g_build_filename ("usr", member, NULL);
+
+          if (symlink (target, dest) != 0)
+            {
+              /* Ignore EEXIST in the case where it was not just /usr:
+               * it's fine if the runtime we copied from source_files
+               * already had either directories or symlinks in its root
+               * directory */
+              if (is_just_usr || errno != EEXIST)
+                return glnx_throw_errno_prefix (error,
+                                                "Cannot create symlink \"%s\" -> %s",
+                                                dest, target);
+            }
+        }
+    }
+
+  /* Hand over from holding a lock on the source to just holding a lock
+   * on the copy. We'll release source_lock when we leave this scope */
+  source_lock = g_steal_pointer (&self->runtime_lock);
+  self->runtime_lock = g_steal_pointer (&copy_lock);
+  self->mutable_sysroot = g_steal_pointer (&temp_dir);
+  self->mutable_sysroot_fd = glnx_steal_fd (&temp_dir_fd);
+
+  return TRUE;
+}
+
 static gboolean
 pv_runtime_initable_init (GInitable *initable,
                           GCancellable *cancellable G_GNUC_UNUSED,
@@ -300,6 +666,13 @@ pv_runtime_initable_init (GInitable *initable,
                          self->bubblewrap);
     }
 
+  if (self->mutable_parent != NULL
+      && !g_file_test (self->mutable_parent, G_FILE_TEST_IS_DIR))
+    {
+      return glnx_throw (error, "\"%s\" is not a directory",
+                         self->mutable_parent);
+    }
+
   if (!g_file_test (self->source_files, G_FILE_TEST_IS_DIR))
     {
       return glnx_throw (error, "\"%s\" is not a directory",
@@ -313,7 +686,12 @@ pv_runtime_initable_init (GInitable *initable,
     }
 
   /* Take a lock on the runtime until we're finished with setup,
-   * to make sure it doesn't get deleted. */
+   * to make sure it doesn't get deleted.
+   *
+   * If the runtime is mounted read-only in the container, it will
+   * continue to be locked until all processes in the container exit.
+   * If we make a temporary mutable copy, we only hold this lock until
+   * setup has finished. */
   files_ref = g_build_filename (self->source_files, ".ref", NULL);
   self->runtime_lock = pv_bwrap_lock_new (AT_FDCWD, files_ref,
                                           PV_BWRAP_LOCK_FLAGS_CREATE,
@@ -323,20 +701,33 @@ pv_runtime_initable_init (GInitable *initable,
   if (self->runtime_lock == NULL)
     return FALSE;
 
-  g_debug ("Creating temporary directories...");
+  if (!pv_runtime_init_mutable (self, error))
+    return FALSE;
 
-  /* Using a runtime requires a temporary directory */
-  self->tmpdir = g_dir_make_tmp ("pressure-vessel-wrap.XXXXXX", error);
+  if (self->mutable_sysroot != NULL)
+    {
+      self->overrides_in_container = "/usr/lib/pressure-vessel/overrides";
+      self->overrides = g_build_filename (self->mutable_sysroot,
+                                          self->overrides_in_container, NULL);
+      self->runtime_files = self->mutable_sysroot;
+    }
+  else
+    {
+      /* We currently only need a temporary directory if we don't have
+       * a mutable sysroot to work with. */
+      self->tmpdir = g_dir_make_tmp ("pressure-vessel-wrap.XXXXXX", error);
 
-  if (self->tmpdir == NULL)
-    return FALSE;
+      if (self->tmpdir == NULL)
+        return FALSE;
+
+      self->overrides = g_build_filename (self->tmpdir, "overrides", NULL);
+      self->overrides_in_container = "/overrides";
+      self->runtime_files = self->source_files;
+    }
 
-  self->overrides = g_build_filename (self->tmpdir, "overrides", NULL);
   g_mkdir (self->overrides, 0700);
-  self->overrides_bin = g_build_filename (self->overrides, "bin", NULL);
-  g_mkdir (self->overrides_bin, 0700);
 
-  self->runtime_usr = g_build_filename (self->source_files, "usr", NULL);
+  self->runtime_usr = g_build_filename (self->runtime_files, "usr", NULL);
 
   if (g_file_test (self->runtime_usr, G_FILE_TEST_IS_DIR))
     {
@@ -344,10 +735,10 @@ pv_runtime_initable_init (GInitable *initable,
     }
   else
     {
-      /* source_files is just a merged /usr. */
+      /* runtime_files is just a merged /usr. */
       self->runtime_is_just_usr = TRUE;
       g_free (self->runtime_usr);
-      self->runtime_usr = g_strdup (self->source_files);
+      self->runtime_usr = g_strdup (self->runtime_files);
     }
 
   return TRUE;
@@ -367,7 +758,6 @@ pv_runtime_cleanup (PvRuntime *self)
                  local_error->message);
     }
 
-  g_clear_pointer (&self->overrides_bin, g_free);
   g_clear_pointer (&self->overrides, g_free);
   g_clear_pointer (&self->container_access, g_free);
   g_clear_pointer (&self->container_access_adverb, flatpak_bwrap_free);
@@ -381,6 +771,10 @@ pv_runtime_finalize (GObject *object)
 
   pv_runtime_cleanup (self);
   g_free (self->bubblewrap);
+  glnx_close_fd (&self->mutable_parent_fd);
+  g_free (self->mutable_parent);
+  glnx_close_fd (&self->mutable_sysroot_fd);
+  g_free (self->mutable_sysroot);
   g_free (self->runtime_usr);
   g_free (self->source_files);
   g_free (self->tools_dir);
@@ -415,6 +809,14 @@ pv_runtime_class_init (PvRuntimeClass *cls)
                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
                          G_PARAM_STATIC_STRINGS));
 
+  properties[PROP_MUTABLE_PARENT] =
+    g_param_spec_string ("mutable-parent", "Mutable parent",
+                         ("Path to a directory in which to create a "
+                          "mutable copy of source-files, or NULL"),
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+
   properties[PROP_SOURCE_FILES] =
     g_param_spec_string ("source-files", "Source files",
                          ("Path to read-only runtime files (merged-/usr "
@@ -435,6 +837,7 @@ pv_runtime_class_init (PvRuntimeClass *cls)
 
 PvRuntime *
 pv_runtime_new (const char *source_files,
+                const char *mutable_parent,
                 const char *bubblewrap,
                 const char *tools_dir,
                 PvRuntimeFlags flags,
@@ -449,6 +852,7 @@ pv_runtime_new (const char *source_files,
                          NULL,
                          error,
                          "bubblewrap", bubblewrap,
+                         "mutable-parent", mutable_parent,
                          "source-files", source_files,
                          "tools-directory", tools_dir,
                          "flags", flags,
@@ -467,9 +871,11 @@ pv_runtime_append_lock_adverb (PvRuntime *self,
 {
   g_return_if_fail (PV_IS_RUNTIME (self));
   g_return_if_fail (!pv_bwrap_was_finished (bwrap));
+  /* This will be true if pv_runtime_bind() was successfully called. */
+  g_return_if_fail (self->with_lock_in_container != NULL);
 
   flatpak_bwrap_add_args (bwrap,
-                          "/run/pressure-vessel/bin/pressure-vessel-with-lock",
+                          self->with_lock_in_container,
                           "--subreaper",
                           NULL);
 
@@ -541,12 +947,11 @@ pv_runtime_provide_container_access (PvRuntime *self,
        * shape" that the final system is going to be.
        *
        * In particular, if we are working with a writeable copy of a runtime
-       * that we are editing in-place, we can arrange that it's always
-       * like that. */
+       * that we are editing in-place, it's always like that. */
       g_debug ("%s: Setting up runtime without using bwrap",
                G_STRFUNC);
       self->container_access_adverb = flatpak_bwrap_new (NULL);
-      self->container_access = g_strdup (self->source_files);
+      self->container_access = g_strdup (self->runtime_files);
 
       /* This is going to go poorly for us if the runtime is not complete.
        * !self->runtime_is_just_usr means we know it has a /usr subdirectory,
@@ -563,7 +968,7 @@ pv_runtime_provide_container_access (PvRuntime *self,
        * so we don't check for it. */
       for (i = 0; i < G_N_ELEMENTS (need_top_level); i++)
         {
-          g_autofree gchar *path = g_build_filename (self->source_files,
+          g_autofree gchar *path = g_build_filename (self->runtime_files,
                                                      need_top_level[i],
                                                      NULL);
 
@@ -579,6 +984,11 @@ pv_runtime_provide_container_access (PvRuntime *self,
       g_debug ("%s: Using bwrap to set up runtime that is just /usr",
                G_STRFUNC);
 
+      /* By design, writeable copies of the runtime never need this:
+       * the writeable copy is a complete sysroot, not just a merged /usr. */
+      g_assert (self->mutable_sysroot == NULL);
+      g_assert (self->tmpdir != NULL);
+
       self->container_access = g_build_filename (self->tmpdir, "mnt", NULL);
       g_mkdir (self->container_access, 0700);
 
@@ -590,7 +1000,7 @@ pv_runtime_provide_container_access (PvRuntime *self,
                               "--tmpfs", self->container_access,
                               NULL);
       if (!pv_bwrap_bind_usr (self->container_access_adverb,
-                              self->source_files,
+                              self->runtime_files,
                               self->container_access,
                               error))
         return FALSE;
@@ -715,6 +1125,8 @@ ensure_locales (PvRuntime *self,
   g_autoptr(FlatpakBwrap) run_locale_gen = NULL;
   g_autofree gchar *locale_gen = NULL;
   g_autofree gchar *locales = g_build_filename (self->overrides, "locales", NULL);
+  g_autofree gchar *locales_in_container = g_build_filename (self->overrides_in_container,
+                                                             "locales", NULL);
   g_autoptr(GDir) dir = NULL;
   int exit_status;
 
@@ -746,7 +1158,10 @@ ensure_locales (PvRuntime *self,
                                      NULL);
 
       flatpak_bwrap_append_bwrap (run_locale_gen, bwrap);
-      pv_bwrap_copy_tree (run_locale_gen, self->overrides, "/overrides");
+      flatpak_bwrap_add_args (bwrap,
+                              "--ro-bind", self->overrides,
+                              self->overrides_in_container,
+                              NULL);
 
       if (!flatpak_bwrap_bundle_args (run_locale_gen, 1, -1, FALSE,
                                       &local_error))
@@ -758,9 +1173,9 @@ ensure_locales (PvRuntime *self,
 
       flatpak_bwrap_add_args (run_locale_gen,
                               "--ro-bind", self->tools_dir, "/run/host/tools",
-                              "--bind", locales, "/overrides/locales",
+                              "--bind", locales, locales_in_container,
                               locale_gen,
-                              "--output-dir", "/overrides/locales",
+                              "--output-dir", locales_in_container,
                               "--verbose",
                               NULL);
     }
@@ -792,7 +1207,7 @@ ensure_locales (PvRuntime *self,
 
       g_debug ("%s is non-empty", locales);
 
-      locpath = g_string_new ("/overrides/locales");
+      locpath = g_string_new (locales_in_container);
       pv_search_path_append (locpath, g_getenv ("LOCPATH"));
       flatpak_bwrap_add_args (bwrap,
                               "--setenv", "LOCPATH", locpath->str,
@@ -1032,9 +1447,34 @@ bind_runtime (PvRuntime *self,
   g_return_val_if_fail (!pv_bwrap_was_finished (bwrap), FALSE);
   g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
 
-  if (!pv_bwrap_bind_usr (bwrap, self->source_files, "/", error))
+  if (!pv_bwrap_bind_usr (bwrap, self->runtime_files, "/", error))
     return FALSE;
 
+  /* In the case where we have a mutable sysroot, we mount the overrides
+   * as part of /usr. Make /overrides a symbolic link, to be nice to
+   * older steam-runtime-tools versions. */
+
+  if (self->mutable_sysroot != NULL)
+    {
+      g_assert (self->overrides_in_container[0] == '/');
+      g_assert (g_strcmp0 (self->overrides_in_container, "/overrides") != 0);
+      flatpak_bwrap_add_args (bwrap,
+                              "--symlink",
+                              &self->overrides_in_container[1],
+                              "/overrides",
+                              NULL);
+
+      /* Also make a matching symbolic link on disk, to make it easier
+       * to inspect the sysroot. */
+      if (TEMP_FAILURE_RETRY (symlinkat (&self->overrides_in_container[1],
+                                         self->mutable_sysroot_fd,
+                                         "overrides")) != 0)
+        return glnx_throw_errno_prefix (error,
+                                        "Unable to create symlink \"%s/overrides\" -> \"%s\"",
+                                        self->mutable_sysroot,
+                                        &self->overrides_in_container[1]);
+    }
+
   flatpak_bwrap_add_args (bwrap,
                           "--setenv", "XDG_RUNTIME_DIR", xrd,
                           "--tmpfs", "/run",
@@ -1048,7 +1488,7 @@ bind_runtime (PvRuntime *self,
 
   for (i = 0; i < G_N_ELEMENTS (bind_mutable); i++)
     {
-      g_autofree gchar *path = g_build_filename (self->source_files,
+      g_autofree gchar *path = g_build_filename (self->runtime_files,
                                                  bind_mutable[i],
                                                  NULL);
       g_autoptr(GDir) dir = NULL;
@@ -1070,7 +1510,7 @@ bind_runtime (PvRuntime *self,
           if (g_strv_contains (dont_bind, dest))
             continue;
 
-          full = g_build_filename (self->source_files,
+          full = g_build_filename (self->runtime_files,
                                    bind_mutable[i],
                                    member,
                                    NULL);
@@ -1148,7 +1588,16 @@ bind_runtime (PvRuntime *self,
   flatpak_run_add_pulseaudio_args (bwrap);
   flatpak_run_add_session_dbus_args (bwrap);
   flatpak_run_add_system_dbus_args (bwrap);
-  pv_bwrap_copy_tree (bwrap, self->overrides, "/overrides");
+
+  if (self->mutable_sysroot == NULL)
+    {
+      /* self->overrides is in a temporary directory that will be
+       * cleaned up before we enter the container, so we need to convert
+       * it into a series of --dir and --symlink instructions.
+       *
+       * We have to do this late, because it adds data fds. */
+      pv_bwrap_copy_tree (bwrap, self->overrides, self->overrides_in_container);
+    }
 
   /* /etc/localtime and /etc/resolv.conf can not exist (or be symlinks to
    * non-existing targets), in which case we don't want to attempt to create
@@ -1197,6 +1646,425 @@ bind_runtime (PvRuntime *self,
   return TRUE;
 }
 
+typedef enum
+{
+  TAKE_FROM_HOST_FLAGS_IF_DIR = (1 << 0),
+  TAKE_FROM_HOST_FLAGS_IF_EXISTS = (1 << 1),
+  TAKE_FROM_HOST_FLAGS_IF_CONTAINER_COMPATIBLE = (1 << 2),
+  TAKE_FROM_HOST_FLAGS_COPY_FALLBACK = (1 << 3),
+  TAKE_FROM_HOST_FLAGS_NONE = 0
+} TakeFromHostFlags;
+
+static gboolean
+pv_runtime_take_from_host (PvRuntime *self,
+                           FlatpakBwrap *bwrap,
+                           const char *source_in_host,
+                           const char *dest_in_container,
+                           TakeFromHostFlags flags,
+                           GError **error)
+{
+  g_return_val_if_fail (PV_IS_RUNTIME (self), FALSE);
+  g_return_val_if_fail (!pv_bwrap_was_finished (bwrap), FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+  if (flags & TAKE_FROM_HOST_FLAGS_IF_DIR)
+    {
+      if (!g_file_test (source_in_host, G_FILE_TEST_IS_DIR))
+        return TRUE;
+    }
+
+  if (flags & TAKE_FROM_HOST_FLAGS_IF_EXISTS)
+    {
+      if (!g_file_test (source_in_host, G_FILE_TEST_EXISTS))
+        return TRUE;
+    }
+
+  if (self->mutable_sysroot != NULL)
+    {
+      /* Replace ${mutable_sysroot}/usr/lib/locale with a symlink to
+       * /run/host/usr/lib/locale, or similar */
+      g_autofree gchar *parent_in_container = NULL;
+      g_autofree gchar *target = NULL;
+      const char *base;
+      glnx_autofd int parent_dirfd = -1;
+
+      parent_in_container = g_path_get_dirname (dest_in_container);
+      parent_dirfd = pv_resolve_in_sysroot (self->mutable_sysroot_fd,
+                                            parent_in_container,
+                                            PV_RESOLVE_FLAGS_MKDIR_P,
+                                            NULL, error);
+
+      if (parent_dirfd < 0)
+        return FALSE;
+
+      base = glnx_basename (dest_in_container);
+
+      if (!glnx_shutil_rm_rf_at (parent_dirfd, base, NULL, error))
+        return FALSE;
+
+      /* If it isn't in /usr, /lib, etc., then the symlink will be
+       * dangling and this probably isn't going to work. */
+      if (!path_visible_in_run_host (source_in_host))
+        {
+          if (flags & TAKE_FROM_HOST_FLAGS_COPY_FALLBACK)
+            {
+              return glnx_file_copy_at (AT_FDCWD, source_in_host, NULL,
+                                        parent_dirfd, base,
+                                        GLNX_FILE_COPY_OVERWRITE,
+                                        NULL, error);
+            }
+          else
+            {
+              g_warning ("\"%s\" is unlikely to appear in /run/host",
+                         source_in_host);
+              /* ... but try it anyway, it can't hurt */
+            }
+        }
+
+      target = g_build_filename ("/run/host", source_in_host, NULL);
+
+      if (TEMP_FAILURE_RETRY (symlinkat (target, parent_dirfd, base)) != 0)
+        return glnx_throw_errno_prefix (error,
+                                        "Unable to create symlink \"%s/%s\" -> \"%s\"",
+                                        self->mutable_sysroot,
+                                        dest_in_container, target);
+    }
+  else
+    {
+      /* We can't edit the runtime in-place, so tell bubblewrap to mount
+       * a new version over the top */
+
+      if (flags & TAKE_FROM_HOST_FLAGS_IF_CONTAINER_COMPATIBLE)
+        {
+          g_autofree gchar *dest = NULL;
+
+          if (g_str_has_prefix (dest_in_container, "/usr/"))
+            dest = g_build_filename (self->runtime_usr,
+                                     dest_in_container + strlen ("/usr/"),
+                                     NULL);
+          else
+            dest = g_build_filename (self->runtime_files,
+                                     dest_in_container,
+                                     NULL);
+
+          if (g_file_test (source_in_host, G_FILE_TEST_IS_DIR))
+            {
+              if (!g_file_test (dest, G_FILE_TEST_IS_DIR))
+                {
+                  g_warning ("Not mounting \"%s\" over non-directory file or "
+                             "nonexistent path \"%s\"",
+                             source_in_host, dest);
+                  return TRUE;
+                }
+            }
+          else
+            {
+              if (!(g_file_test (dest, G_FILE_TEST_EXISTS) &&
+                    g_file_test (dest, G_FILE_TEST_IS_DIR)))
+                {
+                  g_warning ("Not mounting \"%s\" over directory or "
+                             "nonexistent path \"%s\"",
+                             source_in_host, dest);
+                  return TRUE;
+                }
+            }
+        }
+
+      flatpak_bwrap_add_args (bwrap,
+                              "--ro-bind", source_in_host, dest_in_container,
+                              NULL);
+    }
+
+  return TRUE;
+}
+
+static gboolean
+pv_runtime_remove_overridden_libraries (PvRuntime *self,
+                                        RuntimeArchitecture *arch,
+                                        GError **error)
+{
+  static const char * const libdirs[] = { "lib", "usr/lib", "usr/lib/mesa" };
+  GHashTable *delete[G_N_ELEMENTS (libdirs)] = { NULL };
+  GLnxDirFdIterator iters[G_N_ELEMENTS (libdirs)] = { { FALSE } };
+  gchar *multiarch_libdirs[G_N_ELEMENTS (libdirs)] = { NULL };
+  gboolean ret = FALSE;
+  gsize i;
+
+  g_return_val_if_fail (PV_IS_RUNTIME (self), FALSE);
+  g_return_val_if_fail (arch != NULL, FALSE);
+  g_return_val_if_fail (arch->ld_so != NULL, FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+  /* Not applicable/possible if we don't have a mutable sysroot */
+  g_return_val_if_fail (self->mutable_sysroot != NULL, FALSE);
+
+  /* We have to figure out what we want to delete before we delete anything,
+   * because we can't tell whether a symlink points to a library of a
+   * particular SONAME if we already deleted the library. */
+  for (i = 0; i < G_N_ELEMENTS (libdirs); i++)
+    {
+      glnx_autofd int libdir_fd = -1;
+      struct dirent *dent;
+
+      multiarch_libdirs[i] = g_build_filename (libdirs[i], arch->tuple, NULL);
+
+      /* Mostly ignore error: if the library directory cannot be opened,
+       * presumably we don't need to do anything with it... */
+        {
+          g_autoptr(GError) local_error = NULL;
+
+          libdir_fd = pv_resolve_in_sysroot (self->mutable_sysroot_fd,
+                                             multiarch_libdirs[i],
+                                             PV_RESOLVE_FLAGS_READABLE,
+                                             NULL, &local_error);
+
+          if (libdir_fd < 0)
+            {
+              g_debug ("Cannot resolve \"%s\" in \"%s\", so no need to delete "
+                       "libraries from it: %s",
+                       multiarch_libdirs[i], self->mutable_sysroot,
+                       local_error->message);
+              g_clear_error (&local_error);
+              continue;
+            }
+        }
+
+      g_debug ("Removing overridden %s libraries from \"%s\" in \"%s\"...",
+               arch->tuple, multiarch_libdirs[i], self->mutable_sysroot);
+
+      if (!glnx_dirfd_iterator_init_take_fd (&libdir_fd, &iters[i], error))
+        {
+          glnx_prefix_error (error, "Unable to start iterating \"%s/%s\"",
+                             self->mutable_sysroot,
+                             multiarch_libdirs[i]);
+          goto out;
+        }
+
+      delete[i] = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                         g_free, g_free);
+
+      while (TRUE)
+        {
+          g_autoptr(Elf) elf = NULL;
+          g_autoptr(GError) local_error = NULL;
+          glnx_autofd int libfd = -1;
+          g_autofree gchar *path = NULL;
+          g_autofree gchar *soname = NULL;
+          g_autofree gchar *target = NULL;
+
+          if (!glnx_dirfd_iterator_next_dent_ensure_dtype (&iters[i], &dent,
+                                                           NULL, error))
+            return glnx_prefix_error (error, "Unable to iterate over \"%s/%s\"",
+                                      self->mutable_sysroot,
+                                      multiarch_libdirs[i]);
+
+          if (dent == NULL)
+            break;
+
+          switch (dent->d_type)
+            {
+              case DT_REG:
+              case DT_LNK:
+                break;
+
+              case DT_BLK:
+              case DT_CHR:
+              case DT_DIR:
+              case DT_FIFO:
+              case DT_SOCK:
+              case DT_UNKNOWN:
+              default:
+                continue;
+            }
+
+          if (!g_str_has_prefix (dent->d_name, "lib"))
+            continue;
+
+          if (!g_str_has_suffix (dent->d_name, ".so") &&
+              strstr (dent->d_name, ".so.") == NULL)
+            continue;
+
+          path = g_build_filename (multiarch_libdirs[i], dent->d_name, NULL);
+
+          /* scope for soname_link */
+            {
+              g_autofree gchar *soname_link = NULL;
+
+              soname_link = g_build_filename (arch->libdir_on_host,
+                                              dent->d_name, NULL);
+
+              /* If we found libfoo.so.1 in the container, and libfoo.so.1
+               * also exists among the overrides, delete it. */
+              if (g_file_test (soname_link, G_FILE_TEST_IS_SYMLINK))
+                {
+                  g_hash_table_replace (delete[i],
+                                        g_strdup (dent->d_name),
+                                        g_steal_pointer (&soname_link));
+                  continue;
+                }
+            }
+
+          target = glnx_readlinkat_malloc (iters[i].fd, dent->d_name,
+                                           NULL, NULL);
+
+          if (target != NULL)
+            {
+              g_autofree gchar *soname_link = NULL;
+
+              soname_link = g_build_filename (arch->libdir_on_host,
+                                              glnx_basename (target),
+                                              NULL);
+
+              /* If the symlink in the container points to
+               * /foo/bar/libfoo.so.1, and libfoo.so.1 also exists among
+               * the overrides, delete it. */
+              if (g_file_test (soname_link, G_FILE_TEST_IS_SYMLINK))
+                {
+                  g_hash_table_replace (delete[i],
+                                        g_strdup (dent->d_name),
+                                        g_steal_pointer (&soname_link));
+                  continue;
+                }
+            }
+
+          libfd = pv_resolve_in_sysroot (self->mutable_sysroot_fd, path,
+                                         PV_RESOLVE_FLAGS_READABLE, NULL,
+                                         &local_error);
+
+          if (libfd < 0)
+            {
+              g_warning ("Unable to open %s/%s for reading: %s",
+                         self->mutable_sysroot, path, local_error->message);
+              g_clear_error (&local_error);
+              continue;
+            }
+
+          elf = pv_elf_open_fd (libfd, &local_error);
+
+          if (elf != NULL)
+            soname = pv_elf_get_soname (elf, &local_error);
+
+          if (soname == NULL)
+            {
+              g_warning ("Unable to get SONAME of %s/%s: %s",
+                         self->mutable_sysroot, path, local_error->message);
+              g_clear_error (&local_error);
+              continue;
+            }
+
+          /* If we found a library with SONAME libfoo.so.1 in the
+           * container, and libfoo.so.1 also exists among the overrides,
+           * delete it. */
+            {
+              g_autofree gchar *soname_link = NULL;
+
+              soname_link = g_build_filename (arch->libdir_on_host, soname,
+                                              NULL);
+
+              if (g_file_test (soname_link, G_FILE_TEST_IS_SYMLINK))
+                {
+                  g_hash_table_replace (delete[i],
+                                        g_strdup (dent->d_name),
+                                        g_steal_pointer (&soname_link));
+                  continue;
+                }
+            }
+        }
+    }
+
+  for (i = 0; i < G_N_ELEMENTS (libdirs); i++)
+    {
+      if (delete[i] == NULL)
+        continue;
+
+      g_assert (iters[i].initialized);
+      g_assert (iters[i].fd >= 0);
+
+      GLNX_HASH_TABLE_FOREACH_KV (delete[i],
+                                  const char *, name,
+                                  const char *, reason)
+        {
+          g_autoptr(GError) local_error = NULL;
+
+          g_debug ("Deleting %s/%s/%s because %s replaces it",
+                   self->mutable_sysroot, multiarch_libdirs[i], name, reason);
+
+          if (!glnx_unlinkat (iters[i].fd, name, 0, &local_error))
+            {
+              g_warning ("Unable to delete %s/%s/%s: %s",
+                         self->mutable_sysroot, multiarch_libdirs[i],
+                         name, local_error->message);
+              g_clear_error (&local_error);
+            }
+        }
+    }
+
+  ret = TRUE;
+
+out:
+  for (i = 0; i < G_N_ELEMENTS (libdirs); i++)
+    {
+      g_clear_pointer (&delete[i], g_hash_table_unref);
+      glnx_dirfd_iterator_clear (&iters[i]);
+      g_free (multiarch_libdirs[i]);
+    }
+
+  return ret;
+}
+
+static gboolean
+pv_runtime_take_ld_so_from_host (PvRuntime *self,
+                                 RuntimeArchitecture *arch,
+                                 const gchar *ld_so_in_runtime,
+                                 FlatpakBwrap *bwrap,
+                                 GError **error)
+{
+  g_autofree gchar *ld_so_in_host = NULL;
+
+  g_debug ("Making host ld.so visible in container");
+
+  ld_so_in_host = realpath (arch->ld_so, NULL);
+
+  if (ld_so_in_host == NULL)
+    return glnx_throw_errno_prefix (error,
+                                    "Unable to determine host path to %s",
+                                    arch->ld_so);
+
+  g_debug ("Host path: %s -> %s", arch->ld_so, ld_so_in_host);
+  /* Might be either absolute, or relative to the root */
+  g_debug ("Container path: %s -> %s", arch->ld_so, ld_so_in_runtime);
+
+  /* If we have a mutable sysroot, we can delete the interoperable path
+   * and replace it with a symlink to what we want.
+   * For example, overwrite /lib/ld-linux.so.2 with a symlink to
+   * /run/host/lib/i386-linux-gnu/ld-2.30.so, or similar. This avoids
+   * having to dereference a long chain of symlinks every time we run
+   * an executable. */
+  if (self->mutable_sysroot != NULL &&
+      !pv_runtime_take_from_host (self, bwrap, ld_so_in_host,
+                                  arch->ld_so, TAKE_FROM_HOST_FLAGS_NONE,
+                                  error))
+    return FALSE;
+
+  /* If we don't have a mutable sysroot, we cannot replace symlinks,
+   * and we also cannot mount onto symlinks (they get dereferenced),
+   * so our only choice is to bind-mount
+   * /lib/i386-linux-gnu/ld-2.30.so onto
+   * /lib/i386-linux-gnu/ld-2.15.so and so on.
+   *
+   * In the mutable sysroot case, we don't strictly need to
+   * overwrite /lib/i386-linux-gnu/ld-2.15.so with a symlink to
+   * /run/host/lib/i386-linux-gnu/ld-2.30.so, but we might as well do
+   * it anyway, for extra robustness: if we ever run a ld.so that
+   * doesn't match the libc we are using (perhaps via an OS-specific,
+   * non-standard path), that's pretty much a disaster, because it will
+   * just crash. However, all of those (chains of) non-standard symlinks
+   * will end up pointing to ld_so_in_runtime. */
+  return pv_runtime_take_from_host (self, bwrap, ld_so_in_host,
+                                    ld_so_in_runtime,
+                                    TAKE_FROM_HOST_FLAGS_NONE, error);
+}
+
 static gboolean
 pv_runtime_use_host_graphics_stack (PvRuntime *self,
                                     FlatpakBwrap *bwrap,
@@ -1226,7 +2094,6 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self,
                                                                        g_free, NULL);
   g_autoptr(GHashTable) gconv_from_host = g_hash_table_new_full (g_str_hash, g_str_equal,
                                                                  g_free, NULL);
-  g_autofree gchar *libdrm_data_in_runtime = NULL;
   g_autofree gchar *best_libdrm_data_from_host = NULL;
   GHashTableIter iter;
   const gchar *gconv_path;
@@ -1319,32 +2186,55 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self,
           g_autofree gchar *this_dri_path_in_container = g_build_filename (arch->libdir_in_container,
                                                                            "dri", NULL);
           g_autofree gchar *libc = NULL;
+          /* Can either be relative to the sysroot, or absolute */
           g_autofree gchar *ld_so_in_runtime = NULL;
           g_autofree gchar *libdrm = NULL;
           g_autoptr(SrtObjectList) vdpau_drivers = NULL;
           g_autoptr(SrtObjectList) va_api_drivers = NULL;
 
-          temp_bwrap = flatpak_bwrap_new (NULL);
-          flatpak_bwrap_add_args (temp_bwrap,
-                                  self->bubblewrap,
-                                  NULL);
+          if (self->mutable_sysroot != NULL)
+            {
+              glnx_autofd int fd = -1;
 
-          if (!pv_bwrap_bind_usr (temp_bwrap,
-                                  self->source_files,
-                                  "/",
-                                  error))
-            return FALSE;
+              fd = pv_resolve_in_sysroot (self->mutable_sysroot_fd,
+                                          arch->ld_so,
+                                          PV_RESOLVE_FLAGS_NONE,
+                                          &ld_so_in_runtime,
+                                          error);
 
-          flatpak_bwrap_add_args (temp_bwrap,
-                                  "env", "PATH=/usr/bin:/bin",
-                                  "readlink", "-e", arch->ld_so,
-                                  NULL);
-          flatpak_bwrap_finish (temp_bwrap);
+              if (fd < 0)
+                return FALSE;
+            }
+          else
+            {
+              /* Do it the hard way, by asking a process running in the
+               * container (or at least a container resembling the one we
+               * are going to use) to resolve it for us */
+              temp_bwrap = flatpak_bwrap_new (NULL);
+              flatpak_bwrap_add_args (temp_bwrap,
+                                      self->bubblewrap,
+                                      NULL);
 
-          ld_so_in_runtime = pv_capture_output ((const char * const *) temp_bwrap->argv->pdata,
-                                                NULL);
+              if (!pv_bwrap_bind_usr (temp_bwrap,
+                                      self->runtime_files,
+                                      "/",
+                                      error))
+                return FALSE;
 
-          g_clear_pointer (&temp_bwrap, flatpak_bwrap_free);
+              if (!pv_bwrap_bind_usr (temp_bwrap, "/", "/run/host", error))
+                return FALSE;
+
+              flatpak_bwrap_add_args (temp_bwrap,
+                                      "env", "PATH=/usr/bin:/bin",
+                                      "readlink", "-e", arch->ld_so,
+                                      NULL);
+              flatpak_bwrap_finish (temp_bwrap);
+
+              ld_so_in_runtime = pv_capture_output (
+                  (const char * const *) temp_bwrap->argv->pdata, NULL);
+
+              g_clear_pointer (&temp_bwrap, flatpak_bwrap_free);
+            }
 
           if (ld_so_in_runtime == NULL)
             {
@@ -1511,30 +2401,22 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self,
               g_ptr_array_add (va_api_icd_details, g_steal_pointer (&details));
             }
 
+          if (self->mutable_sysroot != NULL &&
+              !pv_runtime_remove_overridden_libraries (self, arch, error))
+            return FALSE;
+
           libc = g_build_filename (arch->libdir_on_host, "libc.so.6", NULL);
 
           /* If we are going to use the host system's libc6 (likely)
            * then we have to use its ld.so too. */
           if (g_file_test (libc, G_FILE_TEST_IS_SYMLINK))
             {
-              g_autofree gchar *ld_so_in_host = NULL;
               g_autofree char *libc_target = NULL;
 
-              g_debug ("Making host ld.so visible in container");
-
-              ld_so_in_host = realpath (arch->ld_so, NULL);
-
-              if (ld_so_in_host == NULL)
-                return glnx_throw_errno_prefix (error,
-                                                "Unable to determine host path to %s",
-                                                arch->ld_so);
-
-              g_debug ("Host path: %s -> %s", arch->ld_so, ld_so_in_host);
-              g_debug ("Container path: %s -> %s", arch->ld_so, ld_so_in_runtime);
-              flatpak_bwrap_add_args (bwrap,
-                                      "--ro-bind", ld_so_in_host,
-                                      ld_so_in_runtime,
-                                      NULL);
+              if (!pv_runtime_take_ld_so_from_host (self, arch,
+                                                    ld_so_in_runtime,
+                                                    bwrap, error))
+                return FALSE;
 
               /* Collect miscellaneous libraries that libc might dlopen.
                * At the moment this is just libidn2. */
@@ -1720,17 +2602,19 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self,
     {
       g_debug ("Making host locale data visible in container");
 
-      if (g_file_test ("/usr/lib/locale", G_FILE_TEST_EXISTS))
-        flatpak_bwrap_add_args (bwrap,
-                                "--ro-bind", "/usr/lib/locale",
-                                "/usr/lib/locale",
-                                NULL);
+      if (!pv_runtime_take_from_host (self, bwrap,
+                                      "/usr/lib/locale",
+                                      "/usr/lib/locale",
+                                      TAKE_FROM_HOST_FLAGS_IF_EXISTS,
+                                      error))
+        return FALSE;
 
-      if (g_file_test ("/usr/share/i18n", G_FILE_TEST_EXISTS))
-        flatpak_bwrap_add_args (bwrap,
-                                "--ro-bind", "/usr/share/i18n",
-                                "/usr/share/i18n",
-                                NULL);
+      if (!pv_runtime_take_from_host (self, bwrap,
+                                      "/usr/share/i18n",
+                                      "/usr/share/i18n",
+                                      TAKE_FROM_HOST_FLAGS_IF_EXISTS,
+                                      error))
+        return FALSE;
 
       localedef = g_find_program_in_path ("localedef");
 
@@ -1738,15 +2622,12 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self,
         {
           g_warning ("Cannot find localedef in PATH");
         }
-      else
+      else if (!pv_runtime_take_from_host (self, bwrap, localedef,
+                                           "/usr/bin/localedef",
+                                           TAKE_FROM_HOST_FLAGS_IF_CONTAINER_COMPATIBLE,
+                                           error))
         {
-          g_autofree gchar *target = g_build_filename ("/run/host",
-                                                       localedef, NULL);
-
-          flatpak_bwrap_add_args (bwrap,
-                                  "--symlink", target,
-                                  "/overrides/bin/localedef",
-                                  NULL);
+          return FALSE;
         }
 
       locale = g_find_program_in_path ("locale");
@@ -1755,15 +2636,12 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self,
         {
           g_warning ("Cannot find locale in PATH");
         }
-      else
+      else if (!pv_runtime_take_from_host (self, bwrap, locale,
+                                           "/usr/bin/locale",
+                                           TAKE_FROM_HOST_FLAGS_IF_CONTAINER_COMPATIBLE,
+                                           error))
         {
-          g_autofree gchar *target = g_build_filename ("/run/host",
-                                                       locale, NULL);
-
-          flatpak_bwrap_add_args (bwrap,
-                                  "--symlink", target,
-                                  "/overrides/bin/locale",
-                                  NULL);
+          return FALSE;
         }
 
       ldconfig = g_find_program_in_path ("ldconfig");
@@ -1780,11 +2658,13 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self,
         {
           g_warning ("Cannot find ldconfig in PATH, /sbin or /usr/sbin");
         }
-      else
+      else if (!pv_runtime_take_from_host (self, bwrap,
+                                           ldconfig,
+                                           "/sbin/ldconfig",
+                                           TAKE_FROM_HOST_FLAGS_NONE,
+                                           error))
         {
-          flatpak_bwrap_add_args (bwrap,
-                                  "--ro-bind", ldconfig, "/sbin/ldconfig",
-                                  NULL);
+          return FALSE;
         }
 
       g_debug ("Making host gconv modules visible in container");
@@ -1792,19 +2672,12 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self,
       g_hash_table_iter_init (&iter, gconv_from_host);
       while (g_hash_table_iter_next (&iter, (gpointer *)&gconv_path, NULL))
         {
-          g_autofree gchar *gconv_in_runtime = NULL;
-
-          if (g_str_has_prefix (gconv_path, "/usr/"))
-            gconv_in_runtime = g_build_filename (self->runtime_usr, gconv_path + strlen ("/usr"), NULL);
-          else
-            gconv_in_runtime = g_build_filename (self->runtime_usr, gconv_path, NULL);
-
-          if (g_file_test (gconv_in_runtime, G_FILE_TEST_IS_DIR))
-            {
-              flatpak_bwrap_add_args (bwrap,
-                                      "--ro-bind", gconv_path, gconv_path,
-                                      NULL);
-            }
+          if (!pv_runtime_take_from_host (self, bwrap,
+                                          gconv_path,
+                                          gconv_path,
+                                          TAKE_FROM_HOST_FLAGS_IF_DIR,
+                                          error))
+            return FALSE;
         }
     }
   else
@@ -1836,14 +2709,14 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self,
         best_libdrm_data_from_host = g_strdup (pv_hash_table_get_arbitrary_key (libdrm_data_from_host));
     }
 
-  libdrm_data_in_runtime = g_build_filename (self->runtime_usr, "share", "libdrm", NULL);
-
-  if (best_libdrm_data_from_host != NULL &&
-      g_file_test (libdrm_data_in_runtime, G_FILE_TEST_IS_DIR))
+  if (best_libdrm_data_from_host != NULL)
     {
-      flatpak_bwrap_add_args (bwrap,
-                              "--ro-bind", best_libdrm_data_from_host, "/usr/share/libdrm",
-                              NULL);
+      if (!pv_runtime_take_from_host (self, bwrap,
+                                      best_libdrm_data_from_host,
+                                      "/usr/share/libdrm",
+                                      TAKE_FROM_HOST_FLAGS_IF_CONTAINER_COMPATIBLE,
+                                      error))
+        return FALSE;
     }
 
   g_debug ("Setting up EGL ICD JSON...");
@@ -1883,8 +2756,9 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self,
               json_base = g_strdup_printf ("%" G_GSIZE_FORMAT "-%s.json",
                                            j, multiarch_tuples[i]);
               json_on_host = g_build_filename (dir_on_host, json_base, NULL);
-              json_in_container = g_build_filename ("/overrides", "share",
-                                                    "glvnd", "egl_vendor.d",
+              json_in_container = g_build_filename (self->overrides_in_container,
+                                                    "share", "glvnd",
+                                                    "egl_vendor.d",
                                                     json_base, NULL);
 
               replacement = srt_egl_icd_new_replace_library_path (icd,
@@ -1909,13 +2783,18 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self,
           const char *json_on_host = srt_egl_icd_get_json_path (icd);
 
           json_base = g_strdup_printf ("%" G_GSIZE_FORMAT ".json", j);
-          json_in_container = g_build_filename ("/overrides", "share",
-                                                "glvnd", "egl_vendor.d",
+          json_in_container = g_build_filename (self->overrides_in_container,
+                                                "share", "glvnd",
+                                                "egl_vendor.d",
                                                 json_base, NULL);
 
-          flatpak_bwrap_add_args (bwrap,
-                                  "--ro-bind", json_on_host, json_in_container,
-                                  NULL);
+          if (!pv_runtime_take_from_host (self, bwrap,
+                                          json_on_host,
+                                          json_in_container,
+                                          TAKE_FROM_HOST_FLAGS_COPY_FALLBACK,
+                                          error))
+            return FALSE;
+
           pv_search_path_append (egl_path, json_in_container);
         }
     }
@@ -1957,8 +2836,8 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self,
               json_base = g_strdup_printf ("%" G_GSIZE_FORMAT "-%s.json",
                                            j, multiarch_tuples[i]);
               json_on_host = g_build_filename (dir_on_host, json_base, NULL);
-              json_in_container = g_build_filename ("/overrides", "share",
-                                                    "vulkan", "icd.d",
+              json_in_container = g_build_filename (self->overrides_in_container,
+                                                    "share", "vulkan", "icd.d",
                                                     json_base, NULL);
 
               replacement = srt_vulkan_icd_new_replace_library_path (icd,
@@ -1983,13 +2862,17 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self,
           const char *json_on_host = srt_vulkan_icd_get_json_path (icd);
 
           json_base = g_strdup_printf ("%" G_GSIZE_FORMAT ".json", j);
-          json_in_container = g_build_filename ("/overrides", "share",
-                                                "vulkan", "icd.d",
+          json_in_container = g_build_filename (self->overrides_in_container,
+                                                "share", "vulkan", "icd.d",
                                                 json_base, NULL);
 
-          flatpak_bwrap_add_args (bwrap,
-                                  "--ro-bind", json_on_host, json_in_container,
-                                  NULL);
+          if (!pv_runtime_take_from_host (self, bwrap,
+                                          json_on_host,
+                                          json_in_container,
+                                          TAKE_FROM_HOST_FLAGS_COPY_FALLBACK,
+                                          error))
+            return FALSE;
+
           pv_search_path_append (vulkan_path, json_in_container);
         }
     }
@@ -2069,8 +2952,10 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self,
    * already. */
   flatpak_bwrap_add_args (bwrap,
                           "--setenv", "VDPAU_DRIVER_PATH",
-                          "/overrides/lib/${PLATFORM}-linux-gnu/vdpau",
                           NULL);
+  flatpak_bwrap_add_arg_printf (bwrap,
+                                "%s/lib/${PLATFORM}-linux-gnu/vdpau",
+                                self->overrides_in_container);
 
   static const char * const extra_multiarch_tuples[] =
   {
@@ -2123,15 +3008,49 @@ pv_runtime_bind (PvRuntime *self,
   if (!bind_runtime (self, bwrap, error))
     return FALSE;
 
-  pressure_vessel_prefix = g_path_get_dirname (self->tools_dir);
-
-  /* Make sure pressure-vessel itself is visible there. */
+  /* steam-runtime-system-info uses this to detect pressure-vessel, so we
+   * need to create it even if it will be empty */
   flatpak_bwrap_add_args (bwrap,
-                          "--ro-bind",
-                          pressure_vessel_prefix,
+                          "--dir",
                           "/run/pressure-vessel",
                           NULL);
 
+  pressure_vessel_prefix = g_path_get_dirname (self->tools_dir);
+
+  /* Make sure pressure-vessel itself is visible there. */
+  if (self->mutable_sysroot != NULL)
+    {
+      g_autofree gchar *dest = NULL;
+      glnx_autofd int parent_dirfd = -1;
+
+      parent_dirfd = pv_resolve_in_sysroot (self->mutable_sysroot_fd,
+                                            "/usr/lib/pressure-vessel",
+                                            PV_RESOLVE_FLAGS_MKDIR_P,
+                                            NULL, error);
+
+      if (parent_dirfd < 0)
+        return FALSE;
+
+      if (!glnx_shutil_rm_rf_at (parent_dirfd, "from-host", NULL, error))
+        return FALSE;
+
+      dest = glnx_fdrel_abspath (parent_dirfd, "from-host");
+
+      if (!pv_cheap_tree_copy (pressure_vessel_prefix, dest, error))
+        return FALSE;
+
+      self->with_lock_in_container = "/usr/lib/pressure-vessel/from-host/bin/pressure-vessel-with-lock";
+    }
+  else
+    {
+      flatpak_bwrap_add_args (bwrap,
+                              "--ro-bind",
+                              pressure_vessel_prefix,
+                              "/run/pressure-vessel/pv-from-host",
+                              NULL);
+      self->with_lock_in_container = "/run/pressure-vessel/pv-from-host/bin/pressure-vessel-with-lock";
+    }
+
   pv_runtime_set_search_paths (self, bwrap);
 
   return TRUE;
@@ -2154,8 +3073,8 @@ pv_runtime_set_search_paths (PvRuntime *self,
       {
         g_autofree gchar *ld_path = NULL;
 
-        ld_path = g_build_filename ("/overrides", "lib",
-                                   multiarch_tuples[i], NULL);
+        ld_path = g_build_filename (self->overrides_in_container, "lib",
+                                    multiarch_tuples[i], NULL);
 
         pv_search_path_append (ld_library_path, ld_path);
       }
@@ -2167,7 +3086,7 @@ pv_runtime_set_search_paths (PvRuntime *self,
                            * really make sense inside the container:
                            * in principle the layout could be totally
                            * different. */
-                          "--setenv", "PATH", "/overrides/bin:/usr/bin:/bin",
+                          "--setenv", "PATH", "/usr/bin:/bin",
                           "--setenv", "LD_LIBRARY_PATH",
                           ld_library_path->str,
                           NULL);
diff --git a/src/runtime.h b/src/runtime.h
index eb3a16f24376922fe89028e2f8ddd99aa75c9dde..3c004c72254b4782baff9fc056e327efc110cc6f 100644
--- a/src/runtime.h
+++ b/src/runtime.h
@@ -33,6 +33,7 @@
  * PvRuntimeFlags:
  * @PV_RUNTIME_FLAGS_HOST_GRAPHICS_STACK: Use host system graphics stack
  * @PV_RUNTIME_FLAGS_GENERATE_LOCALES: Generate missing locales
+ * @PV_RUNTIME_FLAGS_GC_RUNTIMES: Garbage-collect old temporary runtimes
  * @PV_RUNTIME_FLAGS_NONE: None of the above
  *
  * Flags affecting how we set up the runtime.
@@ -41,12 +42,14 @@ typedef enum
 {
   PV_RUNTIME_FLAGS_HOST_GRAPHICS_STACK = (1 << 0),
   PV_RUNTIME_FLAGS_GENERATE_LOCALES = (1 << 1),
+  PV_RUNTIME_FLAGS_GC_RUNTIMES = (1 << 2),
   PV_RUNTIME_FLAGS_NONE = 0
 } PvRuntimeFlags;
 
 #define PV_RUNTIME_FLAGS_MASK \
   (PV_RUNTIME_FLAGS_HOST_GRAPHICS_STACK \
    | PV_RUNTIME_FLAGS_GENERATE_LOCALES \
+   | PV_RUNTIME_FLAGS_GC_RUNTIMES \
    )
 
 typedef struct _PvRuntime PvRuntime;
@@ -61,6 +64,7 @@ typedef struct _PvRuntimeClass PvRuntimeClass;
 GType pv_runtime_get_type (void);
 
 PvRuntime *pv_runtime_new (const char *source_files,
+                           const char *mutable_parent,
                            const char *bubblewrap,
                            const char *tools_dir,
                            PvRuntimeFlags flags,
diff --git a/src/wrap.c b/src/wrap.c
index 56126d873d66f6d13d3baa09bfb7bff8467fe58e..2791c167fc7134bbd0d2d2528c3a1ea117f61627 100644
--- a/src/wrap.c
+++ b/src/wrap.c
@@ -100,7 +100,8 @@ find_bwrap (const char *tools_dir)
 }
 
 static gchar *
-check_bwrap (const char *tools_dir)
+check_bwrap (const char *tools_dir,
+             gboolean only_prepare)
 {
   g_autoptr(GError) local_error = NULL;
   GError **error = &local_error;
@@ -119,6 +120,14 @@ check_bwrap (const char *tools_dir)
     {
       g_warning ("Cannot find bwrap");
     }
+  else if (only_prepare)
+    {
+      /* With --only-prepare we don't necessarily expect to be able to run
+       * it anyway (we are probably in a Docker container that doesn't allow
+       * creation of nested user namespaces), so just assume that it's the
+       * right one. */
+      return g_steal_pointer (&bwrap_executable);
+    }
   else
     {
       int wait_status;
@@ -316,14 +325,17 @@ typedef enum
   TRISTATE_MAYBE
 } Tristate;
 
+static char *opt_copy_runtime_into = NULL;
 static char **opt_env_if_host = NULL;
 static char *opt_fake_home = NULL;
 static char *opt_freedesktop_app_id = NULL;
 static char *opt_steam_app_id = NULL;
+static gboolean opt_gc_runtimes = TRUE;
 static gboolean opt_generate_locales = TRUE;
 static char *opt_home = NULL;
 static gboolean opt_host_fallback = FALSE;
 static gboolean opt_host_graphics = TRUE;
+static gboolean opt_only_prepare = FALSE;
 static gboolean opt_remove_game_overlay = FALSE;
 static PvShell opt_shell = PV_SHELL_NONE;
 static GPtrArray *opt_ld_preload = NULL;
@@ -495,6 +507,11 @@ opt_share_home_cb (const gchar *option_name,
 
 static GOptionEntry options[] =
 {
+  { "copy-runtime-into", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &opt_copy_runtime_into,
+    "If a --runtime is used, copy it into DIR and edit the copy in-place. "
+    "[Default: $PRESSURE_VESSEL_COPY_RUNTIME_INTO or empty]",
+    "DIR" },
   { "env-if-host", '\0',
     G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING_ARRAY, &opt_env_if_host,
     "Set VAR=VAL if COMMAND is run with /usr from the host system, "
@@ -509,6 +526,15 @@ static GOptionEntry options[] =
     G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &opt_steam_app_id,
     "Make --unshare-home use ~/.var/app/com.steampowered.AppN "
     "as home directory. [Default: $SteamAppId]", "N" },
+  { "gc-runtimes", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_gc_runtimes,
+    "If using --copy-runtime-into, garbage-collect old temporary "
+    "runtimes. [Default, unless $PRESSURE_VESSEL_GC_RUNTIMES is 0]",
+    NULL },
+  { "no-gc-runtimes", '\0',
+    G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &opt_gc_runtimes,
+    "If using --copy-runtime-into, don't garbage-collect old "
+    "temporary runtimes.", NULL },
   { "generate-locales", '\0',
     G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_generate_locales,
     "If using --runtime, attempt to generate any missing locales. "
@@ -626,6 +652,9 @@ static GOptionEntry options[] =
   { "test", '\0',
     G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_test,
     "Smoke test pressure-vessel-wrap and exit.", NULL },
+  { "only-prepare", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_only_prepare,
+    "Prepare runtime, but do not actually run anything.", NULL },
   { NULL }
 };
 
@@ -734,6 +763,7 @@ main (int argc,
   opt_remove_game_overlay = boolean_environment ("PRESSURE_VESSEL_REMOVE_GAME_OVERLAY",
                                                  FALSE);
   opt_share_home = tristate_environment ("PRESSURE_VESSEL_SHARE_HOME");
+  opt_gc_runtimes = boolean_environment ("PRESSURE_VESSEL_GC_RUNTIMES", TRUE);
   opt_generate_locales = boolean_environment ("PRESSURE_VESSEL_GENERATE_LOCALES", TRUE);
   opt_host_graphics = boolean_environment ("PRESSURE_VESSEL_HOST_GRAPHICS",
                                            TRUE);
@@ -773,6 +803,13 @@ main (int argc,
       opt_runtime = g_build_filename (opt_runtime_base, tmp, NULL);
     }
 
+  if (opt_copy_runtime_into == NULL)
+    opt_copy_runtime_into = g_strdup (g_getenv ("PRESSURE_VESSEL_COPY_RUNTIME_INTO"));
+
+  if (opt_copy_runtime_into != NULL
+      && opt_copy_runtime_into[0] == '\0')
+    opt_copy_runtime_into = NULL;
+
   if (opt_version_only)
     {
       g_print ("%s\n", VERSION);
@@ -790,7 +827,7 @@ main (int argc,
       goto out;
     }
 
-  if (argc < 2 && !opt_test)
+  if (argc < 2 && !opt_test && !opt_only_prepare)
     {
       g_printerr ("%s: An executable to run is required\n",
                   g_get_prgname ());
@@ -875,6 +912,13 @@ main (int argc,
         }
     }
 
+  if (opt_only_prepare && opt_test)
+    {
+      g_printerr ("%s: --only-prepare and --test are mutually exclusive",
+                  g_get_prgname ());
+      goto out;
+    }
+
   /* Finished parsing arguments, so any subsequent failures will make
    * us exit 1. */
   ret = 1;
@@ -993,7 +1037,7 @@ main (int argc,
   flatpak_bwrap_append_argsv (wrapped_command, &argv[1], argc - 1);
 
   g_debug ("Checking for bwrap...");
-  bwrap_executable = check_bwrap (tools_dir);
+  bwrap_executable = check_bwrap (tools_dir, opt_only_prepare);
 
   if (opt_test)
     {
@@ -1070,6 +1114,9 @@ main (int argc,
     {
       PvRuntimeFlags flags = PV_RUNTIME_FLAGS_NONE;
 
+      if (opt_gc_runtimes)
+        flags |= PV_RUNTIME_FLAGS_GC_RUNTIMES;
+
       if (opt_generate_locales)
         flags |= PV_RUNTIME_FLAGS_GENERATE_LOCALES;
 
@@ -1079,6 +1126,7 @@ main (int argc,
       g_debug ("Configuring runtime %s...", opt_runtime);
 
       runtime = pv_runtime_new (opt_runtime,
+                                opt_copy_runtime_into,
                                 bwrap_executable,
                                 tools_dir,
                                 flags,
@@ -1308,7 +1356,11 @@ main (int argc,
     pv_runtime_cleanup (runtime);
 
   flatpak_bwrap_finish (bwrap);
-  pv_bwrap_execve (bwrap, error);
+
+  if (opt_only_prepare)
+    ret = 0;
+  else
+    pv_bwrap_execve (bwrap, error);
 
 out:
   if (local_error != NULL)
diff --git a/tests/containers.py b/tests/containers.py
index ae474cfc45dbd9a589ed7489afbc1dc96c07e952..e437ae73a1784baf4cde8554544e9b734cd58dbe 100755
--- a/tests/containers.py
+++ b/tests/containers.py
@@ -78,11 +78,16 @@ SteamOS 2 'brewmaster', Debian 8 'jessie', Ubuntu 14.04 'trusty'.
 """
 
 import contextlib
+import fcntl
+import glob
+import json
 import logging
 import os
 import shutil
+import struct
 import subprocess
 import sys
+import tempfile
 import unittest
 
 try:
@@ -105,6 +110,7 @@ logger = logging.getLogger('test-containers')
 class TestContainers(BaseTest):
     bwrap = None            # type: typing.Optional[str]
     containers_dir = ''
+    host_srsi_parsed = {}   # type: typing.Dict[str, typing.Any]
     pv_dir = ''
     pv_wrap = ''
 
@@ -122,9 +128,8 @@ class TestContainers(BaseTest):
             stdout=2,
             stderr=2,
         ).returncode != 0:
-            # Right now this means we will skip all the tests, but in
-            # future we will be able to do some tests that just do the
-            # setup and do not actually go as far as running the container.
+            # We can only do tests that just do setup and don't actually
+            # try to run the container.
             cls.bwrap = None
         else:
             cls.bwrap = bwrap
@@ -270,8 +275,15 @@ class TestContainers(BaseTest):
             os.environ['HOST_STEAM_RUNTIME_SYSTEM_INFO_JSON'] = os.path.join(
                 cls.artifacts, 'host-srsi.json',
             )
+
+            with open(
+                os.path.join(cls.artifacts, 'host-srsi.json'),
+                'r',
+            ) as reader:
+                cls.host_srsi_parsed = json.load(reader)
         else:
             os.environ.pop('HOST_STEAM_RUNTIME_SYSTEM_INFO_JSON', None)
+            cls.host_srsi_parsed = {}
 
         try:
             os.environ['HOST_LD_LINUX_SO_REALPATH'] = os.path.realpath(
@@ -292,6 +304,7 @@ class TestContainers(BaseTest):
         cls = self.__class__
         self.bwrap = cls.bwrap
         self.containers_dir = cls.containers_dir
+        self.host_srsi_parsed = cls.host_srsi_parsed
         self.pv_dir = cls.pv_dir
         self.pv_wrap = cls.pv_wrap
 
@@ -306,6 +319,21 @@ class TestContainers(BaseTest):
                 os.path.join(cls.artifacts, 'tmp', f),
             )
 
+        # This parsing is sufficiently "cheap" that we repeat it for
+        # each test-case rather than introducing more class variables.
+
+        self.host_os_release = cls.host_srsi_parsed.get('os-release', {})
+
+        if self.host_os_release.get('id') == 'debian':
+            logger.info('Host OS is Debian')
+            self.host_is_debian_derived = True
+        elif 'debian' in self.host_os_release.get('id_like', []):
+            logger.info('Host OS is Debian-derived')
+            self.host_is_debian_derived = True
+        else:
+            logger.info('Host OS is not Debian-derived')
+            self.host_is_debian_derived = False
+
     def tearDown(self) -> None:
         with contextlib.suppress(FileNotFoundError):
             shutil.rmtree(os.path.join(self.artifacts, 'tmp'))
@@ -334,9 +362,12 @@ class TestContainers(BaseTest):
         test_name: str,
         scout: str,
         *,
-        locales: bool = False
+        copy: bool = False,
+        gc: bool = True,
+        locales: bool = False,
+        only_prepare: bool = False
     ) -> None:
-        if self.bwrap is None:
+        if self.bwrap is None and not only_prepare:
             self.skipTest('Unable to run bwrap (in a container?)')
 
         if not os.path.isdir(scout):
@@ -354,30 +385,442 @@ class TestContainers(BaseTest):
             '--verbose',
         ]
 
+        var = os.path.join(self.containers_dir, 'var')
+        os.makedirs(var, exist_ok=True)
+
         if not locales:
             argv.append('--no-generate-locales')
 
-        argv.extend([
-            '--',
-            'env',
-            'TEST_INSIDE_SCOUT_ARTIFACTS=' + artifacts,
-            'TEST_INSIDE_SCOUT_LOCALES=' + ('1' if locales else ''),
-            'python3.5',
-            os.path.join(self.artifacts, 'tmp', 'inside-scout.py'),
-        ])
-
-        with tee_file_and_stderr(
-            os.path.join(artifacts, 'inside-scout.log')
-        ) as tee:
-            completed = self.run_subprocess(
-                argv,
-                cwd=self.artifacts,
-                stdout=tee.stdin,
-                stderr=tee.stdin,
-                universal_newlines=True,
+        with tempfile.TemporaryDirectory(prefix='test-', dir=var) as temp:
+            if copy:
+                argv.extend(['--copy-runtime-into', temp])
+
+                if not gc:
+                    argv.append('--no-gc-runtimes')
+
+            if only_prepare:
+                argv.append('--only-prepare')
+            else:
+                argv.extend([
+                    '--',
+                    'env',
+                    'TEST_INSIDE_SCOUT_ARTIFACTS=' + artifacts,
+                    'TEST_INSIDE_SCOUT_IS_COPY=' + ('1' if copy else ''),
+                    'TEST_INSIDE_SCOUT_LOCALES=' + ('1' if locales else ''),
+                    'python3.5',
+                    os.path.join(self.artifacts, 'tmp', 'inside-scout.py'),
+                ])
+
+            # Create directories representing previous runs of
+            # pressure-vessel-wrap, so that we can assert that they are
+            # GC'd (or not) as desired.
+
+            # Do not delete because its name does not start with tmp-
+            os.makedirs(os.path.join(temp, 'donotdelete'), exist_ok=True)
+            # Delete
+            os.makedirs(os.path.join(temp, 'tmp-deleteme'), exist_ok=True)
+            # Delete, and assert that it is recursive
+            os.makedirs(
+                os.path.join(temp, 'tmp-deleteme2', 'usr', 'lib'),
+                exist_ok=True,
             )
+            # Do not delete because it has ./keep
+            os.makedirs(os.path.join(temp, 'tmp-keep', 'keep'), exist_ok=True)
+            # Do not delete because we will read-lock .ref
+            os.makedirs(os.path.join(temp, 'tmp-rlock'), exist_ok=True)
+            # Do not delete because we will write-lock .ref
+            os.makedirs(os.path.join(temp, 'tmp-wlock'), exist_ok=True)
 
-        self.assertEqual(completed.returncode, 0)
+            with open(
+                os.path.join(temp, 'tmp-rlock', '.ref'), 'w+'
+            ) as rlock_writer, open(
+                os.path.join(temp, 'tmp-wlock', '.ref'), 'w'
+            ) as wlock_writer, tee_file_and_stderr(
+                os.path.join(artifacts, 'inside-scout.log')
+            ) as tee:
+                lockdata = struct.pack('hhlli', fcntl.F_RDLCK, 0, 0, 0, 0)
+                fcntl.fcntl(rlock_writer.fileno(), fcntl.F_SETLKW, lockdata)
+                lockdata = struct.pack('hhlli', fcntl.F_WRLCK, 0, 0, 0, 0)
+                fcntl.fcntl(wlock_writer.fileno(), fcntl.F_SETLKW, lockdata)
+
+                # Put this in a subtest so that if it fails, we still get
+                # to inspect the copied sysroot
+                with self.subTest('run', copy=copy, scout=scout):
+                    completed = self.run_subprocess(
+                        argv,
+                        cwd=self.artifacts,
+                        stdout=tee.stdin,
+                        stderr=tee.stdin,
+                        universal_newlines=True,
+                    )
+                    self.assertEqual(completed.returncode, 0)
+
+            if copy:
+                members = set(os.listdir(temp))
+
+                self.assertIn('.ref', members)
+                self.assertIn('donotdelete', members)
+                self.assertIn('tmp-keep', members)
+                self.assertIn('tmp-rlock', members)
+                self.assertIn('tmp-wlock', members)
+                if gc:
+                    self.assertNotIn('tmp-deleteme', members)
+                    self.assertNotIn('tmp-deleteme2', members)
+                else:
+                    # These would have been deleted if not for --no-gc-runtimes
+                    self.assertIn('tmp-deleteme', members)
+                    self.assertIn('tmp-deleteme2', members)
+
+                members.discard('.ref')
+                members.discard('donotdelete')
+                members.discard('tmp-deleteme')
+                members.discard('tmp-deleteme2')
+                members.discard('tmp-keep')
+                members.discard('tmp-rlock')
+                members.discard('tmp-wlock')
+                # After discarding those, there should be exactly one left:
+                # the one we just created
+                self.assertEqual(len(members), 1)
+                tree = os.path.join(temp, members.pop())
+
+                with self.subTest('mutable sysroot'):
+                    self._assert_mutable_sysroot(
+                        tree,
+                        artifacts,
+                        is_scout=True,
+                    )
+
+    def _assert_mutable_sysroot(
+        self,
+        tree: str,
+        artifacts: str,
+        *,
+        is_scout: bool = True
+    ) -> None:
+        with open(
+            os.path.join(artifacts, 'contents.txt'),
+            'w',
+        ) as writer:
+            self.run_subprocess([
+                'find',
+                '.',
+                '-ls',
+            ], cwd=tree, stderr=2, stdout=writer)
+
+        self.assertTrue(os.path.isdir(os.path.join(tree, 'bin')))
+        self.assertTrue(os.path.isdir(os.path.join(tree, 'etc')))
+        self.assertTrue(os.path.isdir(os.path.join(tree, 'lib')))
+        self.assertTrue(os.path.isdir(os.path.join(tree, 'usr')))
+        self.assertFalse(
+            os.path.isdir(os.path.join(tree, 'usr', 'usr'))
+        )
+        self.assertTrue(os.path.isdir(os.path.join(tree, 'sbin')))
+
+        target = os.readlink(os.path.join(tree, 'overrides'))
+        self.assertEqual(target, 'usr/lib/pressure-vessel/overrides')
+        self.assertTrue(
+            os.path.isdir(os.path.join(tree, 'overrides', 'lib')),
+        )
+        self.assertTrue(
+            os.path.isfile(
+                os.path.join(
+                    tree, 'usr', 'lib', 'pressure-vessel', 'from-host',
+                    'bin', 'pressure-vessel-with-lock',
+                )
+            )
+        )
+        self.assertTrue(
+            os.path.isdir(
+                os.path.join(
+                    tree, 'usr', 'lib', 'pressure-vessel', 'overrides', 'lib',
+                )
+            )
+        )
+
+        for multiarch, arch_info in self.host_srsi_parsed.get(
+            'architectures', {}
+        ).items():
+            overrides_libdir = os.path.join(
+                tree, 'overrides', 'lib', multiarch,
+            )
+            root_libdir = os.path.join(
+                tree, 'lib', multiarch,
+            )
+            usr_libdir = os.path.join(
+                tree, 'usr', 'lib', multiarch,
+            )
+            host_root_libdir = os.path.join(
+                '/', 'lib', multiarch,
+            )
+            host_usr_libdir = os.path.join(
+                '/', 'usr', 'lib', multiarch,
+            )
+            with self.subTest(arch=multiarch):
+
+                self.assertTrue(os.path.isdir(overrides_libdir))
+                self.assertTrue(os.path.isdir(root_libdir))
+                self.assertTrue(os.path.isdir(usr_libdir))
+
+                if is_scout:
+                    for soname in (
+                        'libBrokenLocale.so.1',
+                        'libanl.so.1',
+                        'libc.so.6',
+                        'libcrypt.so.1',
+                        'libdl.so.2',
+                        'libm.so.6',
+                        'libnsl.so.1',
+                        'libpthread.so.0',
+                        'libresolv.so.2',
+                        'librt.so.1',
+                        'libutil.so.1',
+                    ):
+                        # These are from glibc, which is depended on by
+                        # Mesa, and is at least as new as scout's version
+                        # in every supported version of the Steam Runtime.
+                        with self.subTest(soname=soname):
+                            target = os.readlink(
+                                os.path.join(overrides_libdir, soname)
+                            )
+                            self.assertRegex(target, r'^/run/host/')
+
+                            devlib = soname.split('.so.', 1)[0] + '.so'
+
+                            # It was deleted from /lib, if present...
+                            with self.assertRaises(FileNotFoundError):
+                                print(
+                                    '#',
+                                    os.path.join(root_libdir, soname),
+                                    '->',
+                                    os.readlink(
+                                        os.path.join(root_libdir, soname)
+                                    ),
+                                )
+                            # ... and /usr/lib, if present
+                            with self.assertRaises(FileNotFoundError):
+                                print(
+                                    '#',
+                                    os.path.join(usr_libdir, soname),
+                                    '->',
+                                    os.readlink(
+                                        os.path.join(usr_libdir, soname)
+                                    ),
+                                )
+
+                            # In most cases we expect the development
+                            # symlink to be removed, too - but some of
+                            # them are actually linker scripts or other
+                            # non-ELF things
+                            if soname not in (
+                                'libc.so.6',
+                                'libpthread.so.0',
+                            ):
+                                with self.assertRaises(FileNotFoundError):
+                                    print(
+                                        '#',
+                                        os.path.join(usr_libdir, devlib),
+                                        '->',
+                                        os.readlink(
+                                            os.path.join(usr_libdir, devlib)
+                                        ),
+                                    )
+                                with self.assertRaises(FileNotFoundError):
+                                    print(
+                                        '#',
+                                        os.path.join(root_libdir, devlib),
+                                        '->',
+                                        os.readlink(
+                                            os.path.join(root_libdir, devlib)
+                                        ),
+                                    )
+
+                    for soname in (
+                        # These are some examples of libraries in the
+                        # graphics stack that we don't upgrade, so we
+                        # expect the host version to be newer (or possibly
+                        # absent if the host graphics stack doesn't use
+                        # them, for example static linking or something).
+                        'libdrm.so.2',
+                        'libudev.so.0',
+                    ):
+                        with self.subTest(soname=soname):
+                            if (
+                                not os.path.exists(
+                                    os.path.join(host_usr_libdir, soname)
+                                )
+                                and not os.path.exists(
+                                    os.path.join(host_root_libdir, soname)
+                                )
+                            ):
+                                self.assertTrue(
+                                    os.path.exists(
+                                        os.path.join(usr_libdir, soname)
+                                    )
+                                    or os.path.exists(
+                                        os.path.join(root_libdir, soname)
+                                    )
+                                )
+                                continue
+
+                            devlib = soname.split('.so.', 1)[0] + '.so'
+                            pattern = soname + '.*'
+
+                            target = os.readlink(
+                                os.path.join(overrides_libdir, soname)
+                            )
+                            self.assertRegex(target, r'^/run/host/')
+
+                            # It was deleted from /lib, if present...
+                            with self.assertRaises(FileNotFoundError):
+                                os.readlink(
+                                    os.path.join(root_libdir, soname)
+                                )
+                            with self.assertRaises(FileNotFoundError):
+                                os.readlink(
+                                    os.path.join(root_libdir, devlib)
+                                )
+                            self.assertEqual(
+                                glob.glob(os.path.join(root_libdir, pattern)),
+                                [],
+                            )
+
+                            # ... and /usr/lib, if present
+                            with self.assertRaises(FileNotFoundError):
+                                os.readlink(
+                                    os.path.join(usr_libdir, soname)
+                                )
+                            with self.assertRaises(FileNotFoundError):
+                                os.readlink(
+                                    os.path.join(usr_libdir, devlib)
+                                )
+                            self.assertEqual(
+                                glob.glob(os.path.join(usr_libdir, pattern)),
+                                [],
+                            )
+
+                    for soname in (
+                        'libSDL-1.2.so.0',
+                        'libfltk.so.1.1',
+                    ):
+                        with self.subTest(soname=soname):
+                            with self.assertRaises(FileNotFoundError):
+                                os.readlink(
+                                    os.path.join(overrides_libdir, soname),
+                                )
+
+                # Keys are the basename of a DRI driver.
+                # Values are lists of paths to DRI drivers of that name
+                # on the host. We assert that for each name, an arbitrary
+                # one of the DRI drivers of that name appears in the
+                # container.
+                expect_symlinks = {
+                }    # type: typing.Dict[str, typing.List[str]]
+
+                for dri in arch_info.get('dri_drivers', ()):
+                    path = dri['library_path']
+
+                    if path.startswith((    # any of:
+                        '/usr/lib/dri/',
+                        '/usr/lib32/dri/',
+                        '/usr/lib64/dri/',
+                        '/usr/lib/{}/dri/'.format(multiarch),
+                    )):
+                        # We don't make any assertion about the search
+                        # order here.
+
+                        # Take the realpath() on non-Debian-derived hosts,
+                        # because on Arch Linux, we find drivers in
+                        # /usr/lib64 that are physically in /usr/lib.
+                        # Be more strict on Debian because we know more
+                        # about the canonical paths there.
+                        if not self.host_is_debian_derived:
+                            with contextlib.suppress(OSError):
+                                path = os.path.realpath(path)
+
+                        expect_symlinks.setdefault(
+                            os.path.basename(path), []
+                        ).append(path)
+
+                for k, vs in expect_symlinks.items():
+                    with self.subTest(dri_symlink=k):
+                        link = os.path.join(overrides_libdir, 'dri', k)
+                        target = os.readlink(link)
+                        self.assertEqual(target[:10], '/run/host/')
+                        target = target[9:]     # includes the / after host/
+
+                        # Again, take the realpath() on non-Debian-derived
+                        # hosts, but be more strict on Debian.
+                        if not self.host_is_debian_derived:
+                            with contextlib.suppress(OSError):
+                                target = os.path.realpath(target)
+
+                        self.assertIn(target, vs)
+
+                if is_scout:
+                    if self.host_is_debian_derived:
+                        link = os.path.join(
+                            tree, 'usr', 'lib', multiarch, 'gconv',
+                        )
+                        target = os.readlink(link)
+                        self.assertEqual(
+                            target,
+                            '/run/host/usr/lib/{}/gconv'.format(multiarch),
+                        )
+
+                    if os.path.isdir('/usr/share/libdrm'):
+                        link = os.path.join(
+                            tree, 'usr', 'share', 'libdrm',
+                        )
+                        target = os.readlink(link)
+                        self.assertEqual(target, '/run/host/usr/share/libdrm')
+
+        if is_scout:
+            if os.path.isdir('/usr/lib/locale'):
+                link = os.path.join(tree, 'usr', 'lib', 'locale')
+                target = os.readlink(link)
+                self.assertEqual(target, '/run/host/usr/lib/locale')
+
+            if os.path.isdir('/usr/share/i18n'):
+                link = os.path.join(tree, 'usr', 'share', 'i18n')
+                target = os.readlink(link)
+                self.assertEqual(target, '/run/host/usr/share/i18n')
+
+            link = os.path.join(tree, 'sbin', 'ldconfig')
+            target = os.readlink(link)
+            # Might not be /sbin/ldconfig, for example on non-Debian hosts
+            self.assertRegex(target, r'^/run/host/')
+
+            if os.path.isfile('/usr/bin/locale'):
+                link = os.path.join(tree, 'usr', 'bin', 'locale')
+                target = os.readlink(link)
+                self.assertEqual(target, '/run/host/usr/bin/locale')
+
+            if os.path.isfile('/usr/bin/localedef'):
+                link = os.path.join(tree, 'usr', 'bin', 'localedef')
+                target = os.readlink(link)
+                self.assertEqual(target, '/run/host/usr/bin/localedef')
+
+            for ldso, scout_impl in (
+                (
+                    '/lib/ld-linux.so.2',
+                    '/lib/i386-linux-gnu/ld-2.15.so',
+                ),
+                (
+                    '/lib64/ld-linux-x86-64.so.2',
+                    '/lib/x86_64-linux-gnu/ld-2.15.so',
+                ),
+            ):
+                try:
+                    host_path = os.path.realpath(ldso)
+                except OSError:
+                    pass
+                else:
+                    link = os.path.join(tree, './' + ldso)
+                    target = os.readlink(link)
+                    self.assertEqual(target, '/run/host' + host_path)
+                    link = os.path.join(tree, './' + scout_impl)
+                    target = os.readlink(link)
+                    self.assertEqual(target, '/run/host' + host_path)
 
     def test_scout_sysroot(self) -> None:
         scout = os.path.join(self.containers_dir, 'scout_sysroot')
@@ -385,12 +828,29 @@ class TestContainers(BaseTest):
         if os.path.isdir(os.path.join(scout, 'files')):
             scout = os.path.join(scout, 'files')
 
-        self._test_scout('scout_sysroot', scout, locales=True)
+        with self.subTest('only-prepare'):
+            self._test_scout(
+                'scout_sysroot_prep', scout,
+                copy=True, only_prepare=True,
+            )
+
+        with self.subTest('copy'):
+            self._test_scout('scout_sysroot_copy', scout, copy=True, gc=False)
+
+        with self.subTest('transient'):
+            self._test_scout('scout_sysroot', scout, locales=True)
 
     def test_scout_usr(self) -> None:
         scout = os.path.join(self.containers_dir, 'scout', 'files')
 
-        self._test_scout('scout', scout, locales=True)
+        with self.subTest('only-prepare'):
+            self._test_scout('scout_prep', scout, copy=True, only_prepare=True)
+
+        with self.subTest('copy'):
+            self._test_scout('scout_copy', scout, copy=True, locales=True)
+
+        with self.subTest('transient'):
+            self._test_scout('scout', scout)
 
 
 if __name__ == '__main__':
diff --git a/tests/inside-scout.py b/tests/inside-scout.py
index b7e19585ee2db2c8ae6fbc3dbe9f68ad5733f211..58e402ff8b7cc4cf3b9e6298412b52f5a2127ce2 100755
--- a/tests/inside-scout.py
+++ b/tests/inside-scout.py
@@ -5,6 +5,7 @@
 
 import contextlib
 import ctypes
+import errno
 import json
 import logging
 import os
@@ -146,6 +147,14 @@ class TestInsideScout(BaseTest):
         )
         # No actual *tests* here just yet - we just log what's there.
 
+    def test_overrides(self) -> None:
+        if os.getenv('TEST_INSIDE_SCOUT_IS_COPY'):
+            target = os.readlink('/overrides')
+            self.assertEqual(target, 'usr/lib/pressure-vessel/overrides')
+
+        self.assertTrue(Path('/overrides').is_dir())
+        self.assertTrue(Path('/overrides/lib').is_dir())
+
     def test_glibc(self) -> None:
         """
         Assert that we took the glibc version from the host OS.
@@ -155,6 +164,8 @@ class TestInsideScout(BaseTest):
         and in cases where our glibc is the same version as the glibc of
         the host OS, we prefer the host.
         """
+        overrides = Path('/overrides').resolve()
+
         glibc = ctypes.cdll.LoadLibrary('libc.so.6')
         gnu_get_libc_version = glibc.gnu_get_libc_version
         gnu_get_libc_version.restype = ctypes.c_char_p
@@ -167,7 +178,10 @@ class TestInsideScout(BaseTest):
         # closely enough. On x86_64 it does, and on i386 we have
         # symlinks /overrides/lib/i[456]86-linux-gnu.
         host_glibc = ctypes.cdll.LoadLibrary(
-            '/overrides/lib/{}-linux-gnu/libc.so.6'.format(os.uname().machine),
+            '/{}/lib/{}-linux-gnu/libc.so.6'.format(
+                overrides,
+                os.uname().machine,
+            ),
         )
         gnu_get_libc_version = host_glibc.gnu_get_libc_version
         gnu_get_libc_version.restype = ctypes.c_char_p
@@ -214,6 +228,8 @@ class TestInsideScout(BaseTest):
                 self.assertEqual(really_stat.st_ino, expected_stat.st_ino)
 
     def test_srsi(self) -> None:
+        overrides = Path('/overrides').resolve()
+
         if 'HOST_STEAM_RUNTIME_SYSTEM_INFO_JSON' in os.environ:
             with open(
                 os.environ['HOST_STEAM_RUNTIME_SYSTEM_INFO_JSON'],
@@ -416,7 +432,7 @@ class TestInsideScout(BaseTest):
                 # version of the Steam Runtime.
                 self.assertEqual(
                     arch_info['library-details'][soname]['path'],
-                    '/overrides/lib/{}/{}'.format(multiarch, soname),
+                    '{}/lib/{}/{}'.format(overrides, multiarch, soname),
                 )
 
             for soname in (
@@ -461,7 +477,9 @@ class TestInsideScout(BaseTest):
 
                 for k, vs in expect_symlinks.items():
                     with self.subTest(dri_symlink=k):
-                        link = '/overrides/lib/{}/dri/{}'.format(multiarch, k)
+                        link = '{}/lib/{}/dri/{}'.format(
+                            overrides, multiarch, k,
+                        )
                         logger.info('Target of %s should be in %s', link, vs)
                         target = os.readlink(link)
 
@@ -541,6 +559,46 @@ class TestInsideScout(BaseTest):
                     set(arch_info['library-issues-summary']),
                 )
 
+    def test_read_only(self) -> None:
+        for read_only_place in (
+            '/bin',
+            '/etc/ld.so.conf.d',
+            '/lib',
+            '/overrides',
+            '/overrides/lib',
+            '/run/host/bin',
+            '/run/host/lib',
+            '/run/host/usr',
+            '/run/host/usr/bin',
+            '/run/host/usr/lib',
+            '/run/pressure-vessel/pv-from-host',
+            '/run/pressure-vessel/pv-from-host/bin',
+            '/sbin',
+            '/usr',
+            '/usr/lib',
+            '/usr/lib/pressure-vessel/from-host/bin',
+            '/usr/lib/pressure-vessel/overrides',
+            '/usr/lib/pressure-vessel/overrides/lib',
+        ):
+            with self.subTest(read_only_place):
+                if (
+                    read_only_place.startswith('/overrides')
+                    and not os.getenv('TEST_INSIDE_SCOUT_IS_COPY')
+                ):
+                    # If we aren't working from a temporary copy of the
+                    # runtime, /overrides is on a tmpfs
+                    continue
+
+                with self.assertRaises(OSError) as raised:
+                    open(os.path.join(read_only_place, 'hello'), 'w')
+
+                if isinstance(raised.exception, FileNotFoundError):
+                    # Some of these paths don't exist under all
+                    # circumstances
+                    continue
+
+                self.assertEqual(raised.exception.errno, errno.EROFS)
+
 
 if __name__ == '__main__':
     assert sys.version_info >= (3, 5), 'Python 3.5+ is required'
diff --git a/tests/invocation.py b/tests/invocation.py
index 44248abddb33dfd7d54fe0f415e5c828bb5ec078..7d8a7f737981c1f7879274d4200f224e427d0a83 100644
--- a/tests/invocation.py
+++ b/tests/invocation.py
@@ -64,6 +64,14 @@ class TestInvocation(BaseTest):
         )
         self.assertEqual(completed.returncode, 2)
 
+    def test_only_prepare_exclusive(self) -> None:
+        completed = run_subprocess(
+            [self.pv_wrap, '--only-prepare', '--test'],
+            stdout=2,
+            stderr=2,
+        )
+        self.assertEqual(completed.returncode, 2)
+
 
 if __name__ == '__main__':
     assert sys.version_info >= (3, 4), \