diff --git a/pressure-vessel/runtime.c b/pressure-vessel/runtime.c
index 62fb34ef2fdd41f2b2c39249cd48802d8770d22f..443bd05f5280bb4d2f186257f13603d488f06027 100644
--- a/pressure-vessel/runtime.c
+++ b/pressure-vessel/runtime.c
@@ -5751,3 +5751,103 @@ pv_runtime_get_modified_app (PvRuntime *self)
   else
     return NULL;
 }
+
+/*
+ * Return %TRUE if the runtime provides @library, either directly or
+ * via the graphics-stack provider.
+ */
+gboolean
+pv_runtime_has_library (PvRuntime *self,
+                        const char *library)
+{
+  glnx_autofd int source_files_fd = -1;
+  gsize i;
+
+  g_return_val_if_fail (PV_IS_RUNTIME (self), FALSE);
+  g_return_val_if_fail (library != NULL, FALSE);
+
+  g_debug ("Checking whether runtime has library: %s", library);
+
+  for (i = 0; i < PV_N_SUPPORTED_ARCHITECTURES; i++)
+    {
+      const PvMultiarchDetails *details = &pv_multiarch_details[i];
+      g_autoptr(GPtrArray) dirs = NULL;
+      gsize j;
+
+      dirs = pv_multiarch_details_get_libdirs (details,
+                                               PV_MULTIARCH_LIBDIRS_FLAGS_NONE);
+
+      for (j = 0; j < dirs->len; j++)
+        {
+          const char *libdir = g_ptr_array_index (dirs, j);
+          g_autofree gchar *path = g_build_filename (libdir, library, NULL);
+
+          if (self->mutable_sysroot_fd >= 0)
+            {
+              glnx_autofd int fd = -1;
+
+              fd = _srt_resolve_in_sysroot (self->mutable_sysroot_fd, path,
+                                            SRT_RESOLVE_FLAGS_NONE,
+                                            NULL, NULL);
+
+              if (fd >= 0)
+                {
+                  g_debug ("-> yes, ${mutable_sysroot}/%s", path);
+                  return TRUE;
+                }
+            }
+          else
+            {
+              glnx_autofd int fd = -1;
+
+              /* The runtime isn't necessarily a sysroot (it might just be a
+               * merged /usr) but in practice it'll be close enough: we look
+               * up each library in /usr/foo and /foo anyway. */
+              if (source_files_fd < 0)
+                {
+                  if (!glnx_opendirat (AT_FDCWD, self->source_files, TRUE,
+                                       &source_files_fd, NULL))
+                    continue;
+                }
+
+              fd = _srt_resolve_in_sysroot (source_files_fd, path,
+                                            SRT_RESOLVE_FLAGS_NONE,
+                                            NULL, NULL);
+
+              if (fd >= 0)
+                {
+                  g_debug ("-> yes, ${source_files}/%s", path);
+                  return TRUE;
+                }
+            }
+
+          /* If the graphics stack provider is not the same as the current
+           * namespace (in practice this rarely/never happens), we also
+           * want to steer clear of libraries that only exist in the
+           * graphics stack provider.
+           *
+           * If the graphics stack provider *is* the current namespace,
+           * and the library doesn't exist in the container runtime, then
+           * it's OK to use libraries from it in LD_PRELOAD, because there
+           * is no other version that might have been meant. */
+          if (self->provider != NULL
+              && g_strcmp0 (self->provider->path_in_current_ns, "/") != 0)
+            {
+              glnx_autofd int fd = -1;
+
+              fd = _srt_resolve_in_sysroot (self->provider->fd, path,
+                                            SRT_RESOLVE_FLAGS_NONE,
+                                            NULL, NULL);
+
+              if (fd >= 0)
+                {
+                  g_debug ("-> yes, ${provider}/%s", path);
+                  return TRUE;
+                }
+            }
+        }
+    }
+
+  g_debug ("-> no");
+  return FALSE;
+}
diff --git a/pressure-vessel/runtime.h b/pressure-vessel/runtime.h
index dfb9f19f47ba299fe111763bdf8cdc04dd570985..6370deea3b411809feeed5bc61f6b7ca6004d93b 100644
--- a/pressure-vessel/runtime.h
+++ b/pressure-vessel/runtime.h
@@ -113,4 +113,7 @@ gboolean pv_runtime_use_shared_sockets (PvRuntime *self,
                                         PvEnviron *container_env,
                                         GError **error);
 
+gboolean pv_runtime_has_library (PvRuntime *self,
+                                 const char *library);
+
 G_DEFINE_AUTOPTR_CLEANUP_FUNC (PvRuntime, g_object_unref)
diff --git a/pressure-vessel/utils.c b/pressure-vessel/utils.c
index f1a5d4a0ba7051a0784207219222abc2cf645abe..322ca0ff62632909dece050f3323f2a4225455f9 100644
--- a/pressure-vessel/utils.c
+++ b/pressure-vessel/utils.c
@@ -994,57 +994,3 @@ pv_delete_dangling_symlink (int dirfd,
                debug_path, name, g_strerror (saved_errno));
     }
 }
-
-/**
- * pv_append_preload_module:
- * @preload_modules: #PreloadModule objects array
- * @n_preload_modules: Size of @preload_modules
- * @variable: Variable where @value will be appended to
- * @value: Value that needs to be appended
- *
- * Append @value to the #PreloadModule whose "variable" is the same as the
- * provided @variable.
- */
-void
-pv_append_preload_module (PreloadModule preload_modules[],
-                          gsize n_preload_modules,
-                          const char *variable,
-                          const char *value)
-{
-  gsize i;
-
-  g_return_if_fail (preload_modules != NULL);
-  g_return_if_fail (n_preload_modules != 0);
-  g_return_if_fail (variable != NULL);
-
-  for (i = 0; i < n_preload_modules; i++)
-    {
-      if (strcmp (variable, preload_modules[i].variable) == 0)
-        break;
-    }
-
-  if (i == n_preload_modules)
-    g_return_if_reached ();
-
-  if (preload_modules[i].values == NULL)
-    preload_modules[i].values = g_ptr_array_new_with_free_func (g_free);
-
-  g_ptr_array_add (preload_modules[i].values, g_strdup (value));
-}
-
-/**
- * pv_preload_modules_free:
- * @preload_modules: #PreloadModule objects array
- * @n_preload_modules: Size of @preload_modules
- *
- * Free the values stored in @preload_modules.
- */
-void
-pv_preload_modules_free (PreloadModule preload_modules[],
-                         gsize n_preload_modules)
-{
-  gsize i;
-
-  for (i = 0; i < n_preload_modules; i++)
-    g_clear_pointer (&preload_modules[i].values, g_ptr_array_unref);
-}
diff --git a/pressure-vessel/utils.h b/pressure-vessel/utils.h
index fb92733a0f7e1682862840c07588120f46db1368..97e1a0a8d3faafa034df6ee0d625af869f5b8875 100644
--- a/pressure-vessel/utils.h
+++ b/pressure-vessel/utils.h
@@ -44,12 +44,6 @@
 #define pv_log_failure(...) \
   g_log (G_LOG_DOMAIN, PV_LOG_LEVEL_FAILURE, __VA_ARGS__)
 
-typedef struct
-{
-  const char *variable;
-  GPtrArray *values;
-} PreloadModule;
-
 void pv_get_current_dirs (gchar **cwd_p,
                           gchar **cwd_l);
 
@@ -87,12 +81,3 @@ void pv_set_up_logging (gboolean opt_verbose);
 void pv_delete_dangling_symlink (int dirfd,
                                  const char *debug_path,
                                  const char *name);
-
-void pv_append_preload_module (PreloadModule preload_modules[],
-                               gsize n_preload_modules,
-                               const char *variable,
-                               const char *value);
-
-void
-pv_preload_modules_free (PreloadModule preload_modules[],
-                         gsize n_preload_modules);
diff --git a/pressure-vessel/wrap-setup.c b/pressure-vessel/wrap-setup.c
index b381bdbe8a1438f589bc4cd20485c2b8631fb3be..c9982426e1d374c7938dcf5784e0851aea7ee05a 100644
--- a/pressure-vessel/wrap-setup.c
+++ b/pressure-vessel/wrap-setup.c
@@ -21,6 +21,7 @@
 #include "wrap-setup.h"
 
 #include "steam-runtime-tools/glib-backports-internal.h"
+#include "steam-runtime-tools/libdl-internal.h"
 #include "steam-runtime-tools/utils-internal.h"
 #include "libglnx/libglnx.h"
 
@@ -28,6 +29,8 @@
 
 #include "bwrap.h"
 #include "flatpak-run-private.h"
+#include "flatpak-utils-private.h"
+#include "supported-architectures.h"
 
 /*
  * Use code borrowed from Flatpak to share various bits of the
@@ -330,3 +333,440 @@ pv_wrap_move_into_scope (const char *steam_app_id)
   if (local_error != NULL)
     g_debug ("Cannot move into a systemd scope: %s", local_error->message);
 }
+
+static void
+append_preload_internal (GPtrArray *argv,
+                         const char *option,
+                         const char *multiarch_tuple,
+                         const char *export_path,
+                         const char *original_path,
+                         GStrv env,
+                         PvAppendPreloadFlags flags,
+                         PvRuntime *runtime,
+                         FlatpakExports *exports)
+{
+  gboolean flatpak_subsandbox = ((flags & PV_APPEND_PRELOAD_FLAGS_FLATPAK_SUBSANDBOX) != 0);
+
+  if (runtime != NULL
+      && (g_str_has_prefix (original_path, "/usr/")
+          || g_str_has_prefix (original_path, "/lib")
+          || (flatpak_subsandbox && g_str_has_prefix (original_path, "/app/"))))
+    {
+      g_autofree gchar *adjusted_path = NULL;
+      const char *target = flatpak_subsandbox ? "/run/parent" : "/run/host";
+
+      adjusted_path = g_build_filename (target, original_path, NULL);
+      g_debug ("%s -> %s", original_path, adjusted_path);
+
+      if (multiarch_tuple != NULL)
+        g_ptr_array_add (argv, g_strdup_printf ("%s=%s:abi=%s",
+                                                option, adjusted_path,
+                                                multiarch_tuple));
+      else
+        g_ptr_array_add (argv, g_strdup_printf ("%s=%s",
+                                                option, adjusted_path));
+    }
+  else
+    {
+      g_debug ("%s -> unmodified", original_path);
+
+      if (multiarch_tuple != NULL)
+        g_ptr_array_add (argv, g_strdup_printf ("%s=%s:abi=%s",
+                                                option, original_path,
+                                                multiarch_tuple));
+      else
+        g_ptr_array_add (argv, g_strdup_printf ("%s=%s",
+                                                option, original_path));
+
+      if (exports != NULL && export_path != NULL && export_path[0] == '/')
+        {
+          const gchar *steam_path = g_environ_getenv (env, "STEAM_COMPAT_CLIENT_INSTALL_PATH");
+
+          if (steam_path != NULL
+              && flatpak_has_path_prefix (export_path, steam_path))
+            {
+              g_debug ("Skipping exposing \"%s\" because it is located "
+                       "under the Steam client install path that we "
+                       "bind by default", export_path);
+            }
+          else
+            {
+              g_debug ("%s needs adding to exports", export_path);
+              flatpak_exports_add_path_expose (exports,
+                                               FLATPAK_FILESYSTEM_MODE_READ_ONLY,
+                                               export_path);
+            }
+        }
+    }
+}
+
+/*
+ * Deal with a LD_PRELOAD or LD_AUDIT module that contains tokens whose
+ * expansion we can't control or predict, such as ${ORIGIN} or future
+ * additions. We can't do much with these, because we can't assume that
+ * the dynamic string tokens will expand in the same way for us as they
+ * will for other programs.
+ *
+ * We mostly have to pass them into the container and hope for the best.
+ * We can rewrite a /usr/, /lib or /app/ prefix, and we can export the
+ * directory containing the first path component that has a dynamic
+ * string token: for example, /opt/plat-${PLATFORM}/preload.so or
+ * /opt/$PLATFORM/preload.so both have to be exported as /opt.
+ *
+ * Arguments are the same as for pv_wrap_append_preload().
+ */
+static void
+append_preload_unsupported_token (GPtrArray *argv,
+                                  const char *option,
+                                  const char *preload,
+                                  GStrv env,
+                                  PvAppendPreloadFlags flags,
+                                  PvRuntime *runtime,
+                                  FlatpakExports *exports)
+{
+  g_autofree gchar *export_path = NULL;
+  char *dollar;
+  char *slash;
+
+  g_debug ("Found $ORIGIN or unsupported token in \"%s\"",
+           preload);
+
+  if (preload[0] == '/')
+    {
+      export_path = g_strdup (preload);
+      dollar = strchr (export_path, '$');
+      g_assert (dollar != NULL);
+      /* Truncate before '$' */
+      dollar[0] = '\0';
+      slash = strrchr (export_path, '/');
+      /* It's an absolute path, so there is definitely a '/' before '$' */
+      g_assert (slash != NULL);
+      /* Truncate before last '/' before '$' */
+      slash[0] = '\0';
+
+      /* If that truncation leaves it empty, don't try to expose
+       * the whole root filesystem */
+      if (export_path[0] != '/')
+        {
+          g_debug ("Not exporting root filesystem for \"%s\"",
+                   preload);
+          g_clear_pointer (&export_path, g_free);
+        }
+      else
+        {
+          g_debug ("Exporting \"%s\" for \"%s\"",
+                   export_path, preload);
+        }
+    }
+  else
+    {
+      /* Original path was relative and contained an unsupported
+       * token like $ORIGIN. Pass it through as-is, without any extra
+       * exports (because we don't know what the token means!), and
+       * hope for the best. export_path stays NULL. */
+      g_debug ("Not exporting \"%s\": not an absolute path, or starts "
+               "with $ORIGIN",
+               preload);
+    }
+
+  append_preload_internal (argv,
+                           option,
+                           NULL,
+                           export_path,
+                           preload,
+                           env,
+                           flags,
+                           runtime,
+                           exports);
+}
+
+/*
+ * Deal with a LD_PRELOAD or LD_AUDIT module that contains tokens whose
+ * expansion is ABI-dependent but otherwise fixed. We do these by
+ * breaking it up into several ABI-dependent LD_PRELOAD modules, which
+ * are recombined by pv-adverb. We have to do this because the expansion
+ * of the ABI-dependent tokens could be different in the container, due
+ * to using a different glibc.
+ *
+ * Arguments are the same as for pv_wrap_append_preload().
+ */
+static void
+append_preload_per_architecture (GPtrArray *argv,
+                                 const char *option,
+                                 const char *preload,
+                                 GStrv env,
+                                 PvAppendPreloadFlags flags,
+                                 PvRuntime *runtime,
+                                 FlatpakExports *exports)
+{
+  g_autoptr(SrtSystemInfo) system_info = srt_system_info_new (NULL);
+  gsize i;
+
+  if (system_info == NULL)
+    system_info = srt_system_info_new (NULL);
+
+  for (i = 0; i < PV_N_SUPPORTED_ARCHITECTURES; i++)
+    {
+      g_autoptr(GString) mock_path = NULL;
+      g_autoptr(SrtLibrary) details = NULL;
+      const char *path;
+
+      srt_system_info_check_library (system_info,
+                                     pv_multiarch_details[i].tuple,
+                                     preload,
+                                     &details);
+      path = srt_library_get_absolute_path (details);
+
+      if (flags & PV_APPEND_PRELOAD_FLAGS_IN_UNIT_TESTS)
+        {
+          /* Use mock results to get predictable behaviour in the unit
+           * tests, replacing the real result (above). This avoids needing
+           * to have real libraries in place when we do unit testing.
+           *
+           * tests/pressure-vessel/wrap-setup.c is the other side of this. */
+          g_autofree gchar *lib = NULL;
+          const char *platform = NULL;
+
+          /* As a mock ${LIB}, behave like Debian or the fdo SDK. */
+          lib = g_strdup_printf ("lib/%s", pv_multiarch_details[i].tuple);
+
+          /* As a mock ${PLATFORM}, use the first one listed. */
+          platform = pv_multiarch_details[i].platforms[0];
+
+          mock_path = g_string_new (preload);
+
+          if (strchr (preload, '/') == NULL)
+            {
+              g_string_printf (mock_path, "/path/to/%s/%s", lib, preload);
+            }
+          else
+            {
+              g_string_replace (mock_path, "$LIB", lib, 0);
+              g_string_replace (mock_path, "${LIB}", lib, 0);
+              g_string_replace (mock_path, "$PLATFORM", platform, 0);
+              g_string_replace (mock_path, "${PLATFORM}", platform, 0);
+            }
+
+          path = mock_path->str;
+
+          /* As a special case, pretend one 64-bit library failed to load,
+           * so we can exercise what happens when there's only a 32-bit
+           * library available. */
+          if (strstr (path, "only-32-bit") != NULL
+              && strcmp (pv_multiarch_details[i].tuple,
+                         SRT_ABI_I386) != 0)
+            path = NULL;
+        }
+
+      if (path != NULL)
+        {
+          g_debug ("Found %s version of %s at %s",
+                   pv_multiarch_details[i].tuple, preload, path);
+          append_preload_internal (argv,
+                                   option,
+                                   pv_multiarch_details[i].tuple,
+                                   path,
+                                   path,
+                                   env,
+                                   flags,
+                                   runtime,
+                                   exports);
+        }
+      else
+        {
+          g_info ("Unable to load %s version of %s",
+                  pv_multiarch_details[i].tuple,
+                  preload);
+        }
+    }
+}
+
+static void
+append_preload_basename (GPtrArray *argv,
+                         const char *option,
+                         const char *preload,
+                         GStrv env,
+                         PvAppendPreloadFlags flags,
+                         PvRuntime *runtime,
+                         FlatpakExports *exports)
+{
+  gboolean runtime_has_library = FALSE;
+
+  if (runtime != NULL)
+    runtime_has_library = pv_runtime_has_library (runtime, preload);
+
+  if (flags & PV_APPEND_PRELOAD_FLAGS_IN_UNIT_TESTS)
+    {
+      /* Mock implementation for unit tests: behave as though the
+       * container has everything except libfakeroot/libfakechroot. */
+      if (g_str_has_prefix (preload, "libfake"))
+        runtime_has_library = FALSE;
+      else
+        runtime_has_library = TRUE;
+    }
+
+  if (runtime_has_library)
+    {
+      /* If the library exists in the container runtime or in the
+       * stack we imported from the graphics provider, e.g.
+       * LD_PRELOAD=libpthread.so.0, then we certainly don't want
+       * to be loading it from the current namespace: that would
+       * bypass our logic for comparing library versions and picking
+       * the newest. Just pass through the LD_PRELOAD item into the
+       * container, and let the dynamic linker in the container choose
+       * what it means (container runtime or graphics provider as
+       * appropriate). */
+      g_debug ("Found \"%s\" in runtime or graphics stack provider, "
+               "passing %s through as-is",
+               preload, option);
+      append_preload_internal (argv,
+                               option,
+                               NULL,
+                               NULL,
+                               preload,
+                               env,
+                               flags,
+                               runtime,
+                               NULL);
+    }
+  else
+    {
+      /* There's no such library in the container runtime or in the
+       * graphics provider, so it's OK to inject the version from the
+       * current namespace. Use the same trick as for ${PLATFORM} to
+       * turn it into (up to) one absolute path per ABI. */
+      g_debug ("Did not find \"%s\" in runtime or graphics stack provider, "
+               "splitting architectures",
+               preload);
+      append_preload_per_architecture (argv,
+                                       option,
+                                       preload,
+                                       env,
+                                       flags,
+                                       runtime,
+                                       exports);
+    }
+}
+
+/**
+ * pv_wrap_append_preload:
+ * @argv: (element-type filename): Array of command-line options to populate
+ * @variable: (type filename): Environment variable from which this
+ *  preload module was taken, either `LD_AUDIT` or `LD_PRELOAD`
+ * @option: (type filename): Command-line option to add to @argv,
+ *  either `--ld-audit` or `--ld-preload`
+ * @preload: (type filename): Path of preloadable module in current
+ *  namespace, possibly including special ld.so tokens such as `$LIB`,
+ *  or basename of a preloadable module to be found in the standard
+ *  library search path
+ * @env: (array zero-terminated=1) (element-type filename): Environment
+ *  variables to be used instead of `environ`
+ * @flags: Flags to adjust behaviour
+ * @runtime: (nullable): Runtime to be used in container
+ * @exports: (nullable): Used to configure extra paths that need to be
+ *  exported into the container
+ *
+ * Adjust @preload to be valid for the container and append it
+ * to @argv.
+ */
+void
+pv_wrap_append_preload (GPtrArray *argv,
+                        const char *variable,
+                        const char *option,
+                        const char *preload,
+                        GStrv env,
+                        PvAppendPreloadFlags flags,
+                        PvRuntime *runtime,
+                        FlatpakExports *exports)
+{
+  SrtLoadableKind kind;
+  SrtLoadableFlags loadable_flags;
+
+  g_return_if_fail (argv != NULL);
+  g_return_if_fail (option != NULL);
+  g_return_if_fail (preload != NULL);
+  g_return_if_fail (runtime == NULL || PV_IS_RUNTIME (runtime));
+
+  if (strstr (preload, "gtk3-nocsd") != NULL)
+    {
+      g_warning ("Disabling gtk3-nocsd %s: it is known to cause crashes.",
+                 variable);
+      return;
+    }
+
+  if ((flags & PV_APPEND_PRELOAD_FLAGS_REMOVE_GAME_OVERLAY)
+      && g_str_has_suffix (preload, "/gameoverlayrenderer.so"))
+    {
+      g_info ("Disabling Steam Overlay: %s", preload);
+      return;
+    }
+
+  kind = _srt_loadable_classify (preload, &loadable_flags);
+
+  switch (kind)
+    {
+      case SRT_LOADABLE_KIND_BASENAME:
+        /* Basenames can't have dynamic string tokens. */
+        g_warn_if_fail ((loadable_flags & SRT_LOADABLE_FLAGS_DYNAMIC_TOKENS) == 0);
+        append_preload_basename (argv,
+                                 option,
+                                 preload,
+                                 env,
+                                 flags,
+                                 runtime,
+                                 exports);
+        break;
+
+      case SRT_LOADABLE_KIND_PATH:
+        /* Paths can have dynamic string tokens. */
+        if (loadable_flags & (SRT_LOADABLE_FLAGS_ORIGIN
+                              | SRT_LOADABLE_FLAGS_UNKNOWN_TOKENS))
+          {
+            append_preload_unsupported_token (argv,
+                                              option,
+                                              preload,
+                                              env,
+                                              flags,
+                                              runtime,
+                                              exports);
+          }
+        else if (loadable_flags & SRT_LOADABLE_FLAGS_ABI_DEPENDENT)
+          {
+            g_debug ("Found $LIB or $PLATFORM in \"%s\", splitting architectures",
+                     preload);
+            append_preload_per_architecture (argv,
+                                             option,
+                                             preload,
+                                             env,
+                                             flags,
+                                             runtime,
+                                             exports);
+          }
+        else
+          {
+            /* All dynamic tokens should be handled above, so we can
+             * assume that preload is a concrete filename */
+            g_warn_if_fail ((loadable_flags & SRT_LOADABLE_FLAGS_DYNAMIC_TOKENS) == 0);
+            append_preload_internal (argv,
+                                     option,
+                                     NULL,
+                                     preload,
+                                     preload,
+                                     env,
+                                     flags,
+                                     runtime,
+                                     exports);
+          }
+        break;
+
+      case SRT_LOADABLE_KIND_ERROR:
+      default:
+        /* Empty string or similar syntactically invalid token:
+         * ignore with a warning. Since steam-runtime-tools!352 and
+         * steamlinuxruntime!64, the wrapper scripts don't give us
+         * an empty argument any more. */
+        g_warning ("Ignoring invalid loadable module \"%s\"", preload);
+
+        break;
+    }
+}
diff --git a/pressure-vessel/wrap-setup.h b/pressure-vessel/wrap-setup.h
index cfeef954a5360116cee4cac94eac54ea1287aa2c..12dac2cd63dc206be3201d75003debdef768227e 100644
--- a/pressure-vessel/wrap-setup.h
+++ b/pressure-vessel/wrap-setup.h
@@ -26,6 +26,7 @@
 
 #include "flatpak-bwrap-private.h"
 #include "flatpak-exports-private.h"
+#include "runtime.h"
 #include "wrap-pipewire.h"
 
 void pv_wrap_share_sockets (FlatpakBwrap *bwrap,
@@ -40,3 +41,29 @@ gboolean pv_wrap_use_host_os (FlatpakExports *exports,
 void pv_wrap_move_into_scope (const char *steam_app_id);
 
 const char *pv_wrap_get_steam_app_id (const char *from_command_line);
+
+/**
+ * PvAppendPreloadFlags:
+ * @PV_APPEND_PRELOAD_FLAGS_FLATPAK_SUBSANDBOX: The game will be run in
+ *  a Flatpak subsandbox
+ * @PV_APPEND_PRELOAD_FLAGS_REMOVE_GAME_OVERLAY: Disable the Steam Overlay
+ * @PV_APPEND_PRELOAD_FLAGS_NONE: None of the above
+ *
+ * Flags affecting the behaviour of pv_wrap_append_preload().
+ */
+typedef enum
+{
+  PV_APPEND_PRELOAD_FLAGS_FLATPAK_SUBSANDBOX = (1 << 0),
+  PV_APPEND_PRELOAD_FLAGS_REMOVE_GAME_OVERLAY = (1 << 1),
+  PV_APPEND_PRELOAD_FLAGS_IN_UNIT_TESTS = (1 << 2),
+  PV_APPEND_PRELOAD_FLAGS_NONE = 0
+} PvAppendPreloadFlags;
+
+void pv_wrap_append_preload (GPtrArray *argv,
+                             const char *variable,
+                             const char *option,
+                             const char *preload,
+                             GStrv env,
+                             PvAppendPreloadFlags flags,
+                             PvRuntime *runtime,
+                             FlatpakExports *exports);
diff --git a/pressure-vessel/wrap.c b/pressure-vessel/wrap.c
index 13acbb2a330673069dda6019f4a58cd5b16a39f0..38fe6b7db2736771928cff87538148a76b30bc6c 100644
--- a/pressure-vessel/wrap.c
+++ b/pressure-vessel/wrap.c
@@ -696,6 +696,7 @@ static gboolean opt_remove_game_overlay = FALSE;
 static gboolean opt_import_vulkan_layers = TRUE;
 static PvShell opt_shell = PV_SHELL_NONE;
 static GArray *opt_pass_fds = NULL;
+static GArray *opt_preload_modules = NULL;
 static char *opt_runtime = NULL;
 static char *opt_runtime_archive = NULL;
 static char *opt_runtime_base = NULL;
@@ -714,28 +715,50 @@ static gboolean opt_test = FALSE;
 static PvTerminal opt_terminal = PV_TERMINAL_AUTO;
 static char *opt_write_final_argv = NULL;
 
-static PreloadModule opt_preload_modules[] =
+typedef enum
 {
-  { "LD_AUDIT", NULL },
-  { "LD_PRELOAD", NULL },
-};
-static const char * const adverb_preload_options[G_N_ELEMENTS (opt_preload_modules)] =
+  PRELOAD_VARIABLE_INDEX_LD_AUDIT,
+  PRELOAD_VARIABLE_INDEX_LD_PRELOAD,
+} PreloadVariableIndex;
+
+static const struct
 {
-  /* Must be in the same order as opt_preload_modules */
-  "--ld-audit",
-  "--ld-preload",
+  const char *variable;
+  const char *adverb_option;
+} preload_options[] =
+{
+  [PRELOAD_VARIABLE_INDEX_LD_AUDIT] = { "LD_AUDIT", "--ld-audit" },
+  [PRELOAD_VARIABLE_INDEX_LD_PRELOAD] = { "LD_PRELOAD", "--ld-preload" },
 };
 
+typedef struct
+{
+  PreloadVariableIndex which;
+  gchar *preload;
+} WrapPreloadModule;
+
+static void
+wrap_preload_module_clear (gpointer p)
+{
+  WrapPreloadModule *self = p;
+
+  g_free (self->preload);
+}
+
 static gboolean
-opt_host_ld_preload_cb (const gchar *option_name,
-                        const gchar *value,
-                        gpointer data,
-                        GError **error)
+opt_ld_something (PreloadVariableIndex which,
+                  const char *value,
+                  GError **error)
 {
-  g_warning ("%s is deprecated, use --ld-preload=%s instead",
-             option_name, value);
-  pv_append_preload_module (opt_preload_modules, G_N_ELEMENTS (opt_preload_modules),
-                            "LD_PRELOAD", value);
+  WrapPreloadModule module = { which, g_strdup (value) };
+
+  if (opt_preload_modules == NULL)
+    {
+      opt_preload_modules = g_array_new (FALSE, FALSE, sizeof (WrapPreloadModule));
+      g_array_set_clear_func (opt_preload_modules, wrap_preload_module_clear);
+    }
+
+  g_array_append_val (opt_preload_modules, module);
   return TRUE;
 }
 
@@ -745,9 +768,7 @@ opt_ld_audit_cb (const gchar *option_name,
                  gpointer data,
                  GError **error)
 {
-  pv_append_preload_module (opt_preload_modules, G_N_ELEMENTS (opt_preload_modules),
-                            "LD_AUDIT", value);
-  return TRUE;
+  return opt_ld_something (PRELOAD_VARIABLE_INDEX_LD_AUDIT, value, error);
 }
 
 static gboolean
@@ -756,9 +777,18 @@ opt_ld_preload_cb (const gchar *option_name,
                    gpointer data,
                    GError **error)
 {
-  pv_append_preload_module (opt_preload_modules, G_N_ELEMENTS (opt_preload_modules),
-                            "LD_PRELOAD", value);
-  return TRUE;
+  return opt_ld_something (PRELOAD_VARIABLE_INDEX_LD_PRELOAD, value, error);
+}
+
+static gboolean
+opt_host_ld_preload_cb (const gchar *option_name,
+                        const gchar *value,
+                        gpointer data,
+                        GError **error)
+{
+  g_warning ("%s is deprecated, use --ld-preload=%s instead",
+             option_name, value);
+  return opt_ld_preload_cb (option_name, value, data, error);
 }
 
 static gboolean
@@ -1320,6 +1350,7 @@ main (int argc,
   const char *steam_app_id;
   g_autoptr(GPtrArray) adverb_preload_argv = NULL;
   int result;
+  PvAppendPreloadFlags append_preload_flags = PV_APPEND_PRELOAD_FLAGS_NONE;
 
   setlocale (LC_ALL, "");
 
@@ -1763,6 +1794,10 @@ main (int argc,
       bwrap_filesystem_arguments = flatpak_bwrap_new (flatpak_bwrap_empty_env);
       exports = flatpak_exports_new ();
     }
+  else
+    {
+      append_preload_flags |= PV_APPEND_PRELOAD_FLAGS_FLATPAK_SUBSANDBOX;
+    }
 
   /* Invariant: we have bwrap or exports iff we also have the other */
   g_assert ((bwrap != NULL) == (exports != NULL));
@@ -2085,129 +2120,35 @@ main (int argc,
 
   adverb_preload_argv = g_ptr_array_new_with_free_func (g_free);
 
+  if (opt_remove_game_overlay)
+    append_preload_flags |= PV_APPEND_PRELOAD_FLAGS_REMOVE_GAME_OVERLAY;
+
   /* We need the LD_PRELOADs from Steam visible at the paths that were
    * used for them, which might be their physical rather than logical
    * locations. Steam doesn't generally use LD_AUDIT, but the Steam app
    * on Flathub does, and it needs similar handling. */
-  for (i = 0; i < G_N_ELEMENTS (opt_preload_modules); i++)
+  if (opt_preload_modules != NULL)
     {
-      const char *variable = opt_preload_modules[i].variable;
-      const GPtrArray *values = opt_preload_modules[i].values;
-      const char *option = adverb_preload_options[i];
-      gsize len;
       gsize j;
 
-      g_debug ("Adjusting %s...", variable);
+      g_debug ("Adjusting LD_AUDIT/LD_PRELOAD modules...");
 
-      if (values == NULL)
-        len = 0;
-      else
-        len = values->len;
-
-      for (j = 0; j < len; j++)
+      for (j = 0; j < opt_preload_modules->len; j++)
         {
-          const char *preload = g_ptr_array_index (values, j);
-
-          g_assert (preload != NULL);
-
-          if (*preload == '\0')
-            continue;
-
-          if (strstr (preload, "gtk3-nocsd") != NULL)
-            {
-              g_warning ("Disabling gtk3-nocsd %s: it is known to cause crashes.",
-                         variable);
-              continue;
-            }
-
-          if (opt_remove_game_overlay
-              && g_str_has_suffix (preload, "/gameoverlayrenderer.so"))
-            {
-              g_info ("Disabling Steam Overlay: %s", preload);
-              continue;
-            }
-
-          /* A subsandbox will just have the same LD_PRELOAD as the
-           * Flatpak itself, except that we have to redirect /usr and /app
-           * into /run/parent. */
-          if (flatpak_subsandbox != NULL)
-            {
-              if (runtime != NULL
-                  && (g_str_has_prefix (preload, "/usr/")
-                      || g_str_has_prefix (preload, "/app/")
-                      || g_str_has_prefix (preload, "/lib")))
-                {
-                  g_autofree gchar *adjusted_path = NULL;
-
-                  adjusted_path = g_build_filename ("/run/parent", preload, NULL);
-                  g_debug ("%s -> %s", preload, adjusted_path);
-                  g_ptr_array_add (adverb_preload_argv,
-                                   g_strdup_printf ("%s=%s",
-                                                    option,
-                                                    adjusted_path));
-                }
-              else
-                {
-                  g_debug ("%s -> unmodified", preload);
-                  g_ptr_array_add (adverb_preload_argv,
-                                   g_strdup_printf ("%s=%s",
-                                                    option,
-                                                    preload));
-                }
-
-              /* No FlatpakExports here: any file not in /usr or /app that
-               * is visible to our "parent" Flatpak app is also visible
-               * to us. */
-              continue;
-            }
-
-          if (g_file_test (preload, G_FILE_TEST_EXISTS))
-            {
-              if (runtime != NULL
-                  && (g_str_has_prefix (preload, "/usr/")
-                      || g_str_has_prefix (preload, "/lib")))
-                {
-                  g_autofree gchar *adjusted_path = NULL;
-
-                  adjusted_path = g_build_filename ("/run/host", preload, NULL);
-                  g_debug ("%s -> %s", preload, adjusted_path);
-                  /* When using a runtime we can't write to /usr/ or /libQUAL/,
-                   * so redirect this preloaded module to the corresponding
-                   * location in /run/host. */
-                  g_ptr_array_add (adverb_preload_argv,
-                                   g_strdup_printf ("%s=%s",
-                                                    option,
-                                                    adjusted_path));
-                }
-              else
-                {
-                  const gchar *steam_path = NULL;
-                  steam_path = g_getenv ("STEAM_COMPAT_CLIENT_INSTALL_PATH");
-
-                  if (steam_path != NULL && flatpak_has_path_prefix (preload, steam_path))
-                    {
-                      g_debug ("Skipping exposing \"%s\" because it is located "
-                               "under the Steam client install path that we "
-                               "bind by default", preload);
-                    }
-                  else
-                    {
-                      g_debug ("%s -> unmodified, but added to exports", preload);
-                      flatpak_exports_add_path_expose (exports,
-                                                       FLATPAK_FILESYSTEM_MODE_READ_ONLY,
-                                                       preload);
-                    }
-
-                  g_ptr_array_add (adverb_preload_argv,
-                                   g_strdup_printf ("%s=%s",
-                                                    option,
-                                                    preload));
-                }
-            }
-          else
-            {
-              g_info ("%s module '%s' does not exist", variable, preload);
-            }
+          const WrapPreloadModule *module = &g_array_index (opt_preload_modules,
+                                                            WrapPreloadModule,
+                                                            j);
+
+          g_assert (module->which >= 0);
+          g_assert (module->which < G_N_ELEMENTS (preload_options));
+          pv_wrap_append_preload (adverb_preload_argv,
+                                  preload_options[module->which].variable,
+                                  preload_options[module->which].adverb_option,
+                                  module->preload,
+                                  environ,
+                                  append_preload_flags,
+                                  runtime,
+                                  exports);
         }
     }
 
@@ -2787,8 +2728,7 @@ out:
   if (local_error != NULL)
     pv_log_failure ("%s", local_error->message);
 
-  pv_preload_modules_free (opt_preload_modules, G_N_ELEMENTS (opt_preload_modules));
-
+  g_clear_pointer (&opt_preload_modules, g_array_unref);
   g_clear_pointer (&adverb_preload_argv, g_ptr_array_unref);
   g_clear_pointer (&opt_env_if_host, g_strfreev);
   g_clear_pointer (&opt_freedesktop_app_id, g_free);
diff --git a/steam-runtime-tools/libdl-internal.h b/steam-runtime-tools/libdl-internal.h
index dc3b70179a36918ee4521ebb22d480d507501119..09799f90d1e0b8ac13f811d3a2eab6123a3f7d09 100644
--- a/steam-runtime-tools/libdl-internal.h
+++ b/steam-runtime-tools/libdl-internal.h
@@ -35,3 +35,23 @@ G_GNUC_INTERNAL gchar *_srt_libdl_detect_lib (gchar **envp,
                                               const char *helpers_path,
                                               const char *multiarch_tuple,
                                               GError **error);
+
+typedef enum
+{
+  SRT_LOADABLE_KIND_ERROR,
+  SRT_LOADABLE_KIND_BASENAME,
+  SRT_LOADABLE_KIND_PATH,
+} SrtLoadableKind;
+
+typedef enum
+{
+  SRT_LOADABLE_FLAGS_DYNAMIC_TOKENS = (1 << 0),
+  SRT_LOADABLE_FLAGS_ABI_DEPENDENT = (1 << 1),
+  SRT_LOADABLE_FLAGS_ORIGIN = (1 << 2),
+  SRT_LOADABLE_FLAGS_UNKNOWN_TOKENS = (1 << 3),
+  SRT_LOADABLE_FLAGS_NONE = 0
+} SrtLoadableFlags;
+
+G_GNUC_INTERNAL
+SrtLoadableKind _srt_loadable_classify (const char *loadable,
+                                        SrtLoadableFlags *flags_out);
diff --git a/steam-runtime-tools/libdl.c b/steam-runtime-tools/libdl.c
index 7223f4366ca559578025d635459abbb8f1b756ab..5debbef237c4101970355cbfffe40702b0962fee 100644
--- a/steam-runtime-tools/libdl.c
+++ b/steam-runtime-tools/libdl.c
@@ -126,3 +126,97 @@ _srt_libdl_detect_lib (gchar **envp,
                                 "detect-lib",
                                 error);
 }
+
+SrtLoadableKind
+_srt_loadable_classify (const char *loadable,
+                        SrtLoadableFlags *flags_out)
+{
+  SrtLoadableFlags flags = SRT_LOADABLE_FLAGS_NONE;
+  SrtLoadableKind kind;
+  gsize i;
+
+  g_return_val_if_fail (loadable != NULL, SRT_LOADABLE_KIND_ERROR);
+
+  if (loadable[0] == '\0')
+    {
+      kind = SRT_LOADABLE_KIND_ERROR;
+      goto out;
+    }
+
+  if (strchr (loadable, '/') != NULL)
+    {
+      kind = SRT_LOADABLE_KIND_PATH;
+    }
+  else
+    {
+      /* Dynamic string tokens are not interpreted in a bare SONAME
+       * so we don't need to do that part. */
+      kind = SRT_LOADABLE_KIND_BASENAME;
+      goto out;
+    }
+
+  for (i = 0; loadable[i] != '\0'; i++)
+    {
+      if (loadable[i] == '$')
+        {
+          const char *token = loadable + i + 1;
+          gsize len;
+
+          flags |= SRT_LOADABLE_FLAGS_DYNAMIC_TOKENS;
+
+          if (token[0] == '{')
+            {
+              token++;
+              len = strcspn (token, "}");
+            }
+          else
+            {
+              len = 0;
+
+              while (g_ascii_isalnum (token[len]) || token[len] == '_')
+                len++;
+            }
+
+          switch (len)
+            {
+              case 3:
+                if (strncmp ("LIB", token, len) == 0)
+                  {
+                    flags |= SRT_LOADABLE_FLAGS_ABI_DEPENDENT;
+                    continue;
+                  }
+
+                break;
+
+              case 6:
+                if (strncmp ("ORIGIN", token, len) == 0)
+                  {
+                    flags |= SRT_LOADABLE_FLAGS_ORIGIN;
+                    continue;
+                  }
+
+                break;
+
+              case 8:
+                if (strncmp ("PLATFORM", token, len) == 0)
+                  {
+                    flags |= SRT_LOADABLE_FLAGS_ABI_DEPENDENT;
+                    continue;
+                  }
+
+                break;
+
+              default:
+                break;
+            }
+
+          flags |= SRT_LOADABLE_FLAGS_UNKNOWN_TOKENS;
+        }
+    }
+
+out:
+  if (flags_out != NULL)
+    *flags_out = flags;
+
+  return kind;
+}
diff --git a/tests/libdl.c b/tests/libdl.c
new file mode 100644
index 0000000000000000000000000000000000000000..964d0c54295ab1cc42202ed73aa45cd100b17063
--- /dev/null
+++ b/tests/libdl.c
@@ -0,0 +1,131 @@
+/*
+ * Copyright © 2019 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <steam-runtime-tools/steam-runtime-tools.h>
+
+#include <string.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include "steam-runtime-tools/libdl-internal.h"
+#include "test-utils.h"
+
+typedef struct
+{
+  int unused;
+} Fixture;
+
+typedef struct
+{
+  int unused;
+} Config;
+
+static void
+setup (Fixture *f,
+       gconstpointer context)
+{
+  G_GNUC_UNUSED const Config *config = context;
+}
+
+static void
+teardown (Fixture *f,
+          gconstpointer context)
+{
+  G_GNUC_UNUSED const Config *config = context;
+}
+
+#define ERROR SRT_LOADABLE_KIND_ERROR
+#define PATH SRT_LOADABLE_KIND_PATH
+#define BASE SRT_LOADABLE_KIND_BASENAME
+#define DYN SRT_LOADABLE_FLAGS_DYNAMIC_TOKENS
+#define ABI SRT_LOADABLE_FLAGS_ABI_DEPENDENT
+#define ORIGIN SRT_LOADABLE_FLAGS_ORIGIN
+#define UNKNOWN SRT_LOADABLE_FLAGS_UNKNOWN_TOKENS
+#define NONE SRT_LOADABLE_FLAGS_NONE
+static const struct
+{
+  const char *loadable;
+  SrtLoadableKind kind;
+  SrtLoadableFlags flags;
+}
+libdl_classify_tests[] =
+{
+    { "", ERROR, NONE },
+    { "/usr/lib/libc.so.6", PATH, NONE },
+    { "/usr/$LIB/libMangoHud.so", PATH, DYN|ABI },
+    { "${LIB}/libfoo.so", PATH, DYN|ABI },
+    { "/usr/plat-$PLATFORM/libc.so.6", PATH, DYN|ABI },
+    { "${PLATFORM}/libc.so.6", PATH, DYN|ABI },
+    { "${ORIGIN}/../${LIB}/libc.so.6", PATH, DYN|ABI|ORIGIN },
+    { "/$ORIGIN/libfoo", PATH, DYN|ORIGIN },
+    { "$ORIGIN/$FUTURE/${PLATFORM}-libfoo.so", PATH, DYN|ORIGIN|ABI|UNKNOWN },
+    { "${FUTURE}/libfoo.so", PATH, DYN|UNKNOWN },
+    { "libc.so.6", BASE, NONE },
+    { "libMangoHud.so", BASE, NONE },
+    { "looks-like-${TOKENS}-interpreted-but-not-really", BASE, NONE },
+};
+#undef ERROR
+#undef PATH
+#undef BASE
+#undef DYN
+#undef ABI
+#undef ORIGIN
+#undef UNKNOWN
+#undef NONE
+
+static void
+test_libdl_classify (Fixture *f,
+                     gconstpointer context)
+{
+  gsize i;
+
+  for (i = 0; i < G_N_ELEMENTS (libdl_classify_tests); i++)
+    {
+      SrtLoadableFlags flags = -1;
+      SrtLoadableKind kind;
+
+      kind = _srt_loadable_classify (libdl_classify_tests[i].loadable, &flags);
+
+      g_assert_cmpuint (kind, ==, libdl_classify_tests[i].kind);
+      g_assert_cmphex (flags, ==, libdl_classify_tests[i].flags);
+
+      kind = _srt_loadable_classify (libdl_classify_tests[i].loadable, NULL);
+      g_assert_cmpuint (kind, ==, libdl_classify_tests[i].kind);
+    }
+}
+
+int
+main (int argc,
+      char **argv)
+{
+  g_test_init (&argc, &argv, NULL);
+
+  g_test_add ("/libdl/classify", Fixture, NULL, setup,
+              test_libdl_classify, teardown);
+
+  return g_test_run ();
+}
diff --git a/tests/meson.build b/tests/meson.build
index 101e75e31900a75015316012b12cd4b6e6627e54..ff8b1be0d1dfa32104d1d2deb0f52c033fe07d57 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -42,6 +42,7 @@ tests = [
     ],
   },
   {'name': 'json-utils', 'static': true},
+  {'name': 'libdl', 'static': true},
   {'name': 'library'},
   {'name': 'locale'},
   {'name': 'system-info'},
diff --git a/tests/pressure-vessel/meson.build b/tests/pressure-vessel/meson.build
index 19b153611bb6b5386eedd089318f4c05ee4347db..6b713d72336c6ef5938164fbe8a8ff7bff21efc5 100644
--- a/tests/pressure-vessel/meson.build
+++ b/tests/pressure-vessel/meson.build
@@ -45,9 +45,16 @@ compiled_tests = [
   'bwrap-lock',
   'resolve-in-sysroot',
   'wait-for-child-processes',
+  'wrap-setup',
   'utils',
 ]
 
+# The wrap-setup test fails to link unless -lelf is forced in, with undefined
+# references to elf_version() etc. It isn't clear why... but this seems to work.
+force_libelf = declare_dependency(
+  link_args : ['-Wl,--no-as-needed,-lelf,--as-needed'],
+)
+
 foreach test_name : compiled_tests
   exe = executable(
     'test-' + test_name,
@@ -57,6 +64,7 @@ foreach test_name : compiled_tests
       libglnx_dep,
       test_utils_static_libsteamrt_dep,
       pressure_vessel_wrap_lib_dep,
+      force_libelf,
     ],
     include_directories : pv_include_dirs,
     install : false,
diff --git a/tests/pressure-vessel/wrap-setup.c b/tests/pressure-vessel/wrap-setup.c
new file mode 100644
index 0000000000000000000000000000000000000000..fad380efb8d9e2c1c536c8e424492203f3562cf6
--- /dev/null
+++ b/tests/pressure-vessel/wrap-setup.c
@@ -0,0 +1,882 @@
+/*
+ * Copyright © 2019-2021 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include "steam-runtime-tools/glib-backports-internal.h"
+#include "steam-runtime-tools/utils-internal.h"
+#include "libglnx/libglnx.h"
+
+#include "tests/test-utils.h"
+
+#include "supported-architectures.h"
+#include "wrap-setup.h"
+#include "utils.h"
+
+/* These match the first entry in PvMultiArchdetails.platforms,
+ * which is the easiest realistic thing for a mock implementation of
+ * srt_system_info_check_library() to use. */
+#define MOCK_PLATFORM_32 "i686"
+#define MOCK_PLATFORM_64 "xeon_phi"
+
+/* These match Debian multiarch, which is as good a thing as any for
+ * a mock implementation of srt_system_info_check_library() to use. */
+#define MOCK_LIB_32 "lib/" SRT_ABI_I386
+#define MOCK_LIB_64 "lib/" SRT_ABI_X86_64
+
+typedef struct
+{
+  TestsOpenFdSet old_fds;
+  gchar *tmpdir;
+  gchar *mock_host;
+  gchar *mock_runtime;
+  gchar *var;
+  GStrv env;
+} Fixture;
+
+typedef struct
+{
+  int unused;
+} Config;
+
+static int
+open_or_die (const char *path,
+             int flags,
+             int mode)
+{
+  glnx_autofd int fd = open (path, flags | O_CLOEXEC, mode);
+
+  if (fd >= 0)
+    return glnx_steal_fd (&fd);
+  else
+    g_error ("open(%s, 0x%x): %s", path, flags, g_strerror (errno));
+}
+
+static FlatpakExports *
+fixture_create_exports (Fixture *f)
+{
+  g_autoptr(FlatpakExports) exports = flatpak_exports_new ();
+  glnx_autofd int fd = open_or_die (f->mock_host, O_RDONLY | O_DIRECTORY, 0755);
+
+  flatpak_exports_take_host_fd (exports, glnx_steal_fd (&fd));
+  return g_steal_pointer (&exports);
+}
+
+static PvRuntime *
+fixture_create_runtime (Fixture *f,
+                        PvRuntimeFlags flags)
+{
+  g_autoptr(GError) local_error = NULL;
+  g_autoptr(PvGraphicsProvider) graphics_provider = NULL;
+  g_autoptr(PvRuntime) runtime = NULL;
+  const char *gfx_in_container;
+
+  if (flags & PV_RUNTIME_FLAGS_FLATPAK_SUBSANDBOX)
+    gfx_in_container = "/run/parent";
+  else
+    gfx_in_container = "/run/host";
+
+  graphics_provider = pv_graphics_provider_new ("/", gfx_in_container,
+                                                &local_error);
+  g_assert_no_error (local_error);
+  g_assert_nonnull (graphics_provider);
+
+  runtime = pv_runtime_new (f->mock_runtime,
+                            "mock_platform_1.0",
+                            f->var,
+                            NULL,
+                            graphics_provider,
+                            environ,
+                            (flags
+                             | PV_RUNTIME_FLAGS_VERBOSE
+                             | PV_RUNTIME_FLAGS_SINGLE_THREAD),
+                            &local_error);
+  g_assert_no_error (local_error);
+  g_assert_nonnull (runtime);
+  return g_steal_pointer (&runtime);
+}
+
+static void
+setup (Fixture *f,
+       gconstpointer context)
+{
+  static const char * const touch[] =
+  {
+    "app/lib/libpreloadA.so",
+    "future/libs-post2038/.exists",
+    "home/me/libpreloadH.so",
+    "lib/libpreload-rootfs.so",
+    "opt/" MOCK_LIB_32 "/libpreloadL.so",
+    "opt/" MOCK_LIB_64 "/libpreloadL.so",
+    "overlay/libs/usr/lib/libpreloadO.so",
+    "platform/plat-" MOCK_PLATFORM_32 "/libpreloadP.so",
+    "platform/plat-" MOCK_PLATFORM_64 "/libpreloadP.so",
+    "steam/lib/gameoverlayrenderer.so",
+    "usr/lib/libpreloadU.so",
+    "usr/local/lib/libgtk3-nocsd.so.0",
+    "in-root-plat-" MOCK_PLATFORM_32 "-only-32-bit.so",
+  };
+  g_autoptr(GError) local_error = NULL;
+  gsize i;
+
+  f->old_fds = tests_check_fd_leaks_enter ();
+
+  f->tmpdir = g_dir_make_tmp ("pressure-vessel-tests.XXXXXX", &local_error);
+  g_assert_no_error (local_error);
+  f->mock_host = g_build_filename (f->tmpdir, "host", NULL);
+  f->mock_runtime = g_build_filename (f->tmpdir, "runtime", NULL);
+  f->var = g_build_filename (f->tmpdir, "var", NULL);
+  g_assert_no_errno (g_mkdir (f->mock_host, 0755));
+  g_assert_no_errno (g_mkdir (f->mock_runtime, 0755));
+  g_assert_no_errno (g_mkdir (f->var, 0755));
+
+  for (i = 0; i < G_N_ELEMENTS (touch); i++)
+    {
+      g_autofree char *dir = g_path_get_dirname (touch[i]);
+      g_autofree char *full_dir = g_build_filename (f->mock_host, dir, NULL);
+      g_autofree char *full_path = g_build_filename (f->mock_host, touch[i], NULL);
+
+      g_test_message ("Creating %s", full_dir);
+      g_assert_no_errno (g_mkdir_with_parents (full_dir, 0755));
+      g_test_message ("Creating %s", full_path);
+      g_file_set_contents (full_path, "", 0, &local_error);
+      g_assert_no_error (local_error);
+    }
+
+  f->env = g_get_environ ();
+  f->env = g_environ_setenv (f->env, "STEAM_COMPAT_CLIENT_INSTALL_PATH",
+                             "/steam", TRUE);
+}
+
+static void
+teardown (Fixture *f,
+          gconstpointer context)
+{
+  g_autoptr(GError) local_error = NULL;
+
+  if (f->tmpdir != NULL)
+    {
+      glnx_shutil_rm_rf_at (-1, f->tmpdir, NULL, &local_error);
+      g_assert_no_error (local_error);
+    }
+
+  tests_check_fd_leaks_leave (f->old_fds);
+  g_clear_pointer (&f->mock_host, g_free);
+  g_clear_pointer (&f->mock_runtime, g_free);
+  g_clear_pointer (&f->tmpdir, g_free);
+  g_clear_pointer (&f->var, g_free);
+  g_clear_pointer (&f->env, g_strfreev);
+}
+
+static void
+populate_ld_preload (Fixture *f,
+                     GPtrArray *argv,
+                     PvAppendPreloadFlags flags,
+                     PvRuntime *runtime,
+                     FlatpakExports *exports)
+{
+  static const struct
+  {
+    const char *string;
+    const char *warning;
+  } preloads[] =
+  {
+    { "", .warning = "Ignoring invalid loadable module \"\"" },
+    { "", .warning = "Ignoring invalid loadable module \"\"" },
+    { "/app/lib/libpreloadA.so" },
+    { "/platform/plat-$PLATFORM/libpreloadP.so" },
+    { "/opt/${LIB}/libpreloadL.so" },
+    { "/lib/libpreload-rootfs.so" },
+    { "/usr/lib/libpreloadU.so" },
+    { "/home/me/libpreloadH.so" },
+    { "/steam/lib/gameoverlayrenderer.so" },
+    { "/overlay/libs/${ORIGIN}/../lib/libpreloadO.so" },
+    { "/future/libs-$FUTURE/libpreloadF.so" },
+    { "/in-root-plat-${PLATFORM}-only-32-bit.so" },
+    { "/in-root-${FUTURE}.so" },
+    { "./${RELATIVE}.so" },
+    { "./relative.so" },
+    { "libfakeroot.so" },
+    { "libpthread.so.0" },
+    {
+      "/usr/local/lib/libgtk3-nocsd.so.0",
+      .warning = "Disabling gtk3-nocsd LD_PRELOAD: it is known to cause crashes.",
+    },
+    { "", .warning = "Ignoring invalid loadable module \"\"" },
+  };
+  gsize i;
+
+  for (i = 0; i < G_N_ELEMENTS (preloads); i++)
+    {
+      GLogLevelFlags old_fatal_mask = G_LOG_FATAL_MASK;
+
+      /* We expect a warning for libgtk3-nocsd.so.0, but the test framework
+       * makes warnings and critical warnings fatal, in addition to the
+       * usual fatal errors. Temporarily relax that to just critical
+       * warnings and fatal errors. */
+      if (preloads[i].warning != NULL)
+        {
+          old_fatal_mask = g_log_set_always_fatal (G_LOG_FATAL_MASK | G_LOG_LEVEL_CRITICAL);
+#if GLIB_CHECK_VERSION(2, 34, 0)
+          /* We can't check for the message during unit testing when
+           * compiling with GLib 2.32 from scout, but we can check for it
+           * in developer builds against a newer GLib. Note that this
+           * assumes pressure-vessel doesn't define G_LOG_USE_STRUCTURED,
+           * but that's a GLib 2.50 feature which we are unlikely to use
+           * while we are still building against scout. */
+          g_test_expect_message ("pressure-vessel",
+                                 G_LOG_LEVEL_WARNING,
+                                 preloads[i].warning);
+#endif
+        }
+
+      pv_wrap_append_preload (argv,
+                              "LD_PRELOAD",
+                              "--ld-preload",
+                              preloads[i].string,
+                              f->env,
+                              flags | PV_APPEND_PRELOAD_FLAGS_IN_UNIT_TESTS,
+                              runtime,
+                              exports);
+
+      /* If we modified the fatal mask, put back the old value. */
+      if (preloads[i].warning != NULL)
+        {
+#if GLIB_CHECK_VERSION(2, 34, 0)
+          g_test_assert_expected_messages ();
+#endif
+          g_log_set_always_fatal (old_fatal_mask);
+        }
+    }
+
+  for (i = 0; i < argv->len; i++)
+    g_test_message ("argv[%" G_GSIZE_FORMAT "]: %s",
+                    i, (const char *) g_ptr_array_index (argv, i));
+
+  g_test_message ("argv->len: %" G_GSIZE_FORMAT, i);
+}
+
+static void
+test_remap_ld_preload (Fixture *f,
+                       gconstpointer context)
+{
+  g_autoptr(FlatpakExports) exports = fixture_create_exports (f);
+  g_autoptr(GPtrArray) argv = g_ptr_array_new_with_free_func (g_free);
+  g_autoptr(PvRuntime) runtime = fixture_create_runtime (f, PV_RUNTIME_FLAGS_NONE);
+  gsize i;
+
+  populate_ld_preload (f, argv, PV_APPEND_PRELOAD_FLAGS_NONE, runtime, exports);
+
+  i = 0;
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/app/lib/libpreloadA.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/platform/plat-"
+                    MOCK_PLATFORM_64
+                    "/libpreloadP.so:abi="
+                    SRT_ABI_X86_64));
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/platform/plat-"
+                    MOCK_PLATFORM_32
+                    "/libpreloadP.so:abi="
+                    SRT_ABI_I386));
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/opt/"
+                    MOCK_LIB_64
+                    "/libpreloadL.so:abi="
+                    SRT_ABI_X86_64));
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/opt/"
+                    MOCK_LIB_32
+                    "/libpreloadL.so:abi="
+                    SRT_ABI_I386));
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/run/host/lib/libpreload-rootfs.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/run/host/usr/lib/libpreloadU.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/home/me/libpreloadH.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/steam/lib/gameoverlayrenderer.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/overlay/libs/${ORIGIN}/../lib/libpreloadO.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/future/libs-$FUTURE/libpreloadF.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/in-root-plat-"
+                    MOCK_PLATFORM_32
+                    "-only-32-bit.so:abi="
+                    SRT_ABI_I386));
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/in-root-${FUTURE}.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=./${RELATIVE}.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=./relative.so");
+
+  /* Our mock implementation of pv_runtime_has_library() behaves as though
+   * libfakeroot is not in the runtime or graphics stack provider, only
+   * the current namespace */
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/path/to/"
+                    MOCK_LIB_64
+                    "/libfakeroot.so:abi="
+                    SRT_ABI_X86_64));
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/path/to/"
+                    MOCK_LIB_32
+                    "/libfakeroot.so:abi="
+                    SRT_ABI_I386));
+
+  /* Our mock implementation of pv_runtime_has_library() behaves as though
+   * libpthread.so.0 *is* in the runtime, as we would expect */
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=libpthread.so.0");
+
+  g_assert_cmpuint (argv->len, ==, i);
+
+  /* FlatpakExports never exports /app */
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/app"));
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/app/lib"));
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/app/lib/libpreloadA.so"));
+
+  /* We don't always export /home etc. so we have to explicitly export
+   * this one */
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/home"));
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/home/me"));
+  g_assert_true (flatpak_exports_path_is_visible (exports, "/home/me/libpreloadH.so"));
+
+  /* We don't always export /opt, so we have to explicitly export
+   * these. */
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/opt"));
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/opt/lib"));
+  g_assert_true (flatpak_exports_path_is_visible (exports, "/opt/" MOCK_LIB_32 "/libpreloadL.so"));
+  g_assert_true (flatpak_exports_path_is_visible (exports, "/opt/" MOCK_LIB_64 "/libpreloadL.so"));
+
+  /* We don't always export /platform, so we have to explicitly export
+   * these. */
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/platform"));
+  g_assert_true (flatpak_exports_path_is_visible (exports, "/platform/plat-" MOCK_PLATFORM_32 "/libpreloadP.so"));
+  g_assert_true (flatpak_exports_path_is_visible (exports, "/platform/plat-" MOCK_PLATFORM_64 "/libpreloadP.so"));
+
+  /* FlatpakExports never exports /lib as /lib */
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/lib"));
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/lib/libpreload-rootfs.so"));
+
+  /* FlatpakExports never exports /usr as /usr */
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/usr"));
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/usr/lib"));
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/usr/lib/libpreloadU.so"));
+
+  /* We assume STEAM_COMPAT_CLIENT_INSTALL_PATH is dealt with separately */
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/steam"));
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/steam/lib"));
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/steam/lib/gameoverlayrenderer.so"));
+
+  /* We don't know what ${ORIGIN} will expand to, so we have to cut off at
+   * /overlay/libs */
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/overlay"));
+  g_assert_true (flatpak_exports_path_is_visible (exports, "/overlay/libs"));
+
+  /* We don't know what ${FUTURE} will expand to, so we have to cut off at
+   * /future */
+  g_assert_true (flatpak_exports_path_is_visible (exports, "/future"));
+
+  /* We don't export the entire root directory just because it has a
+   * module in it */
+  g_assert_true (flatpak_exports_path_is_visible (exports, "/"));
+}
+
+static void
+test_remap_ld_preload_flatpak (Fixture *f,
+                               gconstpointer context)
+{
+  g_autoptr(GPtrArray) argv = g_ptr_array_new_with_free_func (g_free);
+  g_autoptr(PvRuntime) runtime = fixture_create_runtime (f, PV_RUNTIME_FLAGS_FLATPAK_SUBSANDBOX);
+  gsize i;
+
+  populate_ld_preload (f, argv, PV_APPEND_PRELOAD_FLAGS_FLATPAK_SUBSANDBOX,
+                       runtime, NULL);
+
+  i = 0;
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/run/parent/app/lib/libpreloadA.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/platform/plat-"
+                    MOCK_PLATFORM_64
+                    "/libpreloadP.so:abi="
+                    SRT_ABI_X86_64));
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/platform/plat-"
+                    MOCK_PLATFORM_32
+                    "/libpreloadP.so:abi="
+                    SRT_ABI_I386));
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/opt/"
+                    MOCK_LIB_64
+                    "/libpreloadL.so:abi="
+                    SRT_ABI_X86_64));
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/opt/"
+                    MOCK_LIB_32
+                    "/libpreloadL.so:abi="
+                    SRT_ABI_I386));
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/run/parent/lib/libpreload-rootfs.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/run/parent/usr/lib/libpreloadU.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/home/me/libpreloadH.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/steam/lib/gameoverlayrenderer.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/overlay/libs/${ORIGIN}/../lib/libpreloadO.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/future/libs-$FUTURE/libpreloadF.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/in-root-plat-"
+                    MOCK_PLATFORM_32
+                    "-only-32-bit.so:abi="
+                    SRT_ABI_I386));
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/in-root-${FUTURE}.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=./${RELATIVE}.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=./relative.so");
+
+  /* Our mock implementation of pv_runtime_has_library() behaves as though
+   * libfakeroot is not in the runtime or graphics stack provider, only
+   * the current namespace */
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/path/to/"
+                    MOCK_LIB_64
+                    "/libfakeroot.so:abi="
+                    SRT_ABI_X86_64));
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/path/to/"
+                    MOCK_LIB_32
+                    "/libfakeroot.so:abi="
+                    SRT_ABI_I386));
+
+  /* Our mock implementation of pv_runtime_has_library() behaves as though
+   * libpthread.so.0 *is* in the runtime, as we would expect */
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=libpthread.so.0");
+
+  g_assert_cmpuint (argv->len, ==, i);
+}
+
+/*
+ * In addition to testing the rare case where there's no runtime,
+ * this one also exercises PV_APPEND_PRELOAD_FLAGS_REMOVE_GAME_OVERLAY,
+ * which is the implementation of --remove-game-overlay.
+ */
+static void
+test_remap_ld_preload_no_runtime (Fixture *f,
+                                  gconstpointer context)
+{
+  g_autoptr(GPtrArray) argv = g_ptr_array_new_with_free_func (g_free);
+  g_autoptr(FlatpakExports) exports = fixture_create_exports (f);
+  gsize i;
+
+  populate_ld_preload (f, argv, PV_APPEND_PRELOAD_FLAGS_REMOVE_GAME_OVERLAY,
+                       NULL, exports);
+
+  i = 0;
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/app/lib/libpreloadA.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/platform/plat-"
+                    MOCK_PLATFORM_64
+                    "/libpreloadP.so:abi="
+                    SRT_ABI_X86_64));
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/platform/plat-"
+                    MOCK_PLATFORM_32
+                    "/libpreloadP.so:abi="
+                    SRT_ABI_I386));
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/opt/"
+                    MOCK_LIB_64
+                    "/libpreloadL.so:abi="
+                    SRT_ABI_X86_64));
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/opt/"
+                    MOCK_LIB_32
+                    "/libpreloadL.so:abi="
+                    SRT_ABI_I386));
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/lib/libpreload-rootfs.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/usr/lib/libpreloadU.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/home/me/libpreloadH.so");
+
+  /* /steam/lib/gameoverlayrenderer.so is missing because we used the
+   * REMOVE_GAME_OVERLAY flag */
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/overlay/libs/${ORIGIN}/../lib/libpreloadO.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/future/libs-$FUTURE/libpreloadF.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/in-root-plat-"
+                    MOCK_PLATFORM_32
+                    "-only-32-bit.so:abi="
+                    SRT_ABI_I386));
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/in-root-${FUTURE}.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=./${RELATIVE}.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=./relative.so");
+
+  /* Our mock implementation of pv_runtime_has_library() behaves as though
+   * libfakeroot is not in the runtime or graphics stack provider, only
+   * the current namespace */
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/path/to/"
+                    MOCK_LIB_64
+                    "/libfakeroot.so:abi="
+                    SRT_ABI_X86_64));
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/path/to/"
+                    MOCK_LIB_32
+                    "/libfakeroot.so:abi="
+                    SRT_ABI_I386));
+
+  /* Our mock implementation of pv_runtime_has_library() behaves as though
+   * libpthread.so.0 *is* in the runtime, as we would expect */
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=libpthread.so.0");
+
+  g_assert_cmpuint (argv->len, ==, i);
+
+  /* FlatpakExports never exports /app */
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/app"));
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/app/lib"));
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/app/lib/libpreloadA.so"));
+
+  /* We don't always export /home etc. so we have to explicitly export
+   * this one */
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/home"));
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/home/me"));
+  g_assert_true (flatpak_exports_path_is_visible (exports, "/home/me/libpreloadH.so"));
+
+  /* We don't always export /opt, so we have to explicitly export
+   * these. */
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/opt"));
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/opt/lib"));
+  g_assert_true (flatpak_exports_path_is_visible (exports, "/opt/" MOCK_LIB_32 "/libpreloadL.so"));
+  g_assert_true (flatpak_exports_path_is_visible (exports, "/opt/" MOCK_LIB_64 "/libpreloadL.so"));
+
+  /* We don't always export /platform, so we have to explicitly export
+   * these. */
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/platform"));
+  g_assert_true (flatpak_exports_path_is_visible (exports, "/platform/plat-" MOCK_PLATFORM_32 "/libpreloadP.so"));
+  g_assert_true (flatpak_exports_path_is_visible (exports, "/platform/plat-" MOCK_PLATFORM_64 "/libpreloadP.so"));
+
+  /* FlatpakExports never exports /lib as /lib */
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/lib"));
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/lib/libpreload-rootfs.so"));
+
+  /* FlatpakExports never exports /usr as /usr */
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/usr"));
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/usr/lib"));
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/usr/lib/libpreloadU.so"));
+
+  /* We don't know what ${ORIGIN} will expand to, so we have to cut off at
+   * /overlay/libs */
+  g_assert_false (flatpak_exports_path_is_visible (exports, "/overlay"));
+  g_assert_true (flatpak_exports_path_is_visible (exports, "/overlay/libs"));
+
+  /* We don't know what ${FUTURE} will expand to, so we have to cut off at
+   * /future */
+  g_assert_true (flatpak_exports_path_is_visible (exports, "/future"));
+
+  /* We don't export the entire root directory just because it has a
+   * module in it */
+  g_assert_true (flatpak_exports_path_is_visible (exports, "/"));
+}
+
+static void
+test_remap_ld_preload_flatpak_no_runtime (Fixture *f,
+                                          gconstpointer context)
+{
+  g_autoptr(GPtrArray) argv = g_ptr_array_new_with_free_func (g_free);
+  gsize i;
+
+  populate_ld_preload (f, argv, PV_APPEND_PRELOAD_FLAGS_FLATPAK_SUBSANDBOX,
+                       NULL, NULL);
+
+  i = 0;
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/app/lib/libpreloadA.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/platform/plat-"
+                    MOCK_PLATFORM_64
+                    "/libpreloadP.so:abi="
+                    SRT_ABI_X86_64));
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/platform/plat-"
+                    MOCK_PLATFORM_32
+                    "/libpreloadP.so:abi="
+                    SRT_ABI_I386));
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/opt/"
+                    MOCK_LIB_64
+                    "/libpreloadL.so:abi="
+                    SRT_ABI_X86_64));
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/opt/"
+                    MOCK_LIB_32
+                    "/libpreloadL.so:abi="
+                    SRT_ABI_I386));
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/lib/libpreload-rootfs.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/usr/lib/libpreloadU.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/home/me/libpreloadH.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/steam/lib/gameoverlayrenderer.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/overlay/libs/${ORIGIN}/../lib/libpreloadO.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/future/libs-$FUTURE/libpreloadF.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/in-root-plat-"
+                    MOCK_PLATFORM_32
+                    "-only-32-bit.so:abi="
+                    SRT_ABI_I386));
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=/in-root-${FUTURE}.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=./${RELATIVE}.so");
+
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=./relative.so");
+
+  /* Our mock implementation of pv_runtime_has_library() behaves as though
+   * libfakeroot is not in the runtime or graphics stack provider, only
+   * the current namespace */
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/path/to/"
+                    MOCK_LIB_64
+                    "/libfakeroot.so:abi="
+                    SRT_ABI_X86_64));
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==,
+                   ("--ld-preload=/path/to/"
+                    MOCK_LIB_32
+                    "/libfakeroot.so:abi="
+                    SRT_ABI_I386));
+
+  /* Our mock implementation of pv_runtime_has_library() behaves as though
+   * libpthread.so.0 *is* in the runtime, as we would expect */
+  g_assert_cmpuint (argv->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (argv, i++),
+                   ==, "--ld-preload=libpthread.so.0");
+
+  g_assert_cmpuint (argv->len, ==, i);
+}
+
+int
+main (int argc,
+      char **argv)
+{
+  _srt_setenv_disable_gio_modules ();
+
+  g_test_init (&argc, &argv, NULL);
+  g_test_add ("/remap-ld-preload", Fixture, NULL,
+              setup, test_remap_ld_preload, teardown);
+  g_test_add ("/remap-ld-preload-flatpak", Fixture, NULL,
+              setup, test_remap_ld_preload_flatpak, teardown);
+  g_test_add ("/remap-ld-preload-no-runtime", Fixture, NULL,
+              setup, test_remap_ld_preload_no_runtime, teardown);
+  g_test_add ("/remap-ld-preload-flatpak-no-runtime", Fixture, NULL,
+              setup, test_remap_ld_preload_flatpak_no_runtime, teardown);
+
+  return g_test_run ();
+}