diff --git a/debian/control b/debian/control
index 4bba83d208fcee7028c7483315419f4497de9b25..8ec09e31f18c7df95060300338f527d40e5c2894 100644
--- a/debian/control
+++ b/debian/control
@@ -6,6 +6,7 @@ Standards-Version: 4.4.0
 Build-Depends:
  debhelper (>= 9),
  g++ (>= 4:4.8) | g++-4.8,
+ libelf-dev,
  libglib2.0-dev,
  libsteam-runtime-tools-0-dev (>= 0.20200331.1~),
  libxau-dev,
diff --git a/meson.build b/meson.build
index e29eb930e14ac0cce9bd53674cbff1aaca85127f..78edcb1e5b1db960329fca5efdaaaff8b3ff4da4 100644
--- a/meson.build
+++ b/meson.build
@@ -150,6 +150,11 @@ glib_tap_support = dependency(
   required : false,
 )
 xau = dependency('xau', required : true)
+libelf = dependency('libelf', required : false)
+
+if not libelf.found()
+  libelf = declare_dependency(link_args : ['-lelf'])
+endif
 
 scripts = [
   'pressure-vessel-locale-gen',
diff --git a/src/elf-utils.c b/src/elf-utils.c
new file mode 100644
index 0000000000000000000000000000000000000000..5fcb790bc29f69b7998fd6283334d7dfabdff4a0
--- /dev/null
+++ b/src/elf-utils.c
@@ -0,0 +1,158 @@
+/*
+ * Copyright © 2017-2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "elf-utils.h"
+
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <gio/gio.h>
+#include <libelf.h>
+
+#include "libglnx/libglnx.h"
+
+#include "glib-backports.h"
+
+#define throw_elf_error(error, format, ...) \
+  glnx_null_throw (error, format ": %s", ##__VA_ARGS__, elf_errmsg (elf_errno ()))
+
+/*
+ * pv_elf_open_fd:
+ * @fd: An open file descriptor
+ *
+ * Returns: (transfer full): A libelf object representing the library
+ */
+Elf *
+pv_elf_open_fd (int fd,
+                GError **error)
+{
+  g_autoptr(Elf) elf = NULL;
+
+  g_return_val_if_fail (fd >= 0, NULL);
+  g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+  if (elf_version (EV_CURRENT) == EV_NONE)
+    return throw_elf_error (error, "elf_version(EV_CURRENT)");
+
+  elf = elf_begin (fd, ELF_C_READ, NULL);
+
+  if (elf == NULL)
+    return throw_elf_error (error, "elf_begin");
+
+  return g_steal_pointer (&elf);
+}
+
+/*
+ * pv_elf_get_soname:
+ * @elf: A libelf object
+ *
+ * Return the `DT_SONAME` header, or %NULL on error
+ */
+gchar *
+pv_elf_get_soname (Elf *elf,
+                   GError **error)
+{
+  GElf_Ehdr ehdr;
+  size_t phdr_count = 0;
+  size_t i;
+  GElf_Phdr phdr_mem;
+  GElf_Phdr *phdr = NULL;
+  GElf_Dyn *dynamic_header = NULL;
+  GElf_Dyn dynamic_header_mem;
+  Elf_Scn *dynamic_section = NULL;
+  GElf_Shdr dynamic_section_header_mem;
+  GElf_Shdr *dynamic_section_header = NULL;
+  Elf_Scn *string_table_section = NULL;
+  GElf_Shdr string_table_header_mem;
+  GElf_Shdr *string_table_header = NULL;
+  Elf_Data *data = NULL;
+  size_t size_per_dyn;
+  const char *soname = NULL;
+
+  g_return_val_if_fail (elf != NULL, NULL);
+  g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+  if (elf_kind (elf) != ELF_K_ELF)
+    return glnx_null_throw (error, "elf_kind %d, expected ELF_K_ELF=%d",
+                            elf_kind (elf), ELF_K_ELF);
+
+  if (gelf_getehdr (elf, &ehdr) == NULL)
+    return throw_elf_error (error, "elf_getehdr");
+
+  if (ehdr.e_type != ET_DYN)
+    return glnx_null_throw (error, "ehdr.e_type %d, expected ET_DYN=%d",
+                            ehdr.e_type, ET_DYN);
+
+  if (elf_getphdrnum (elf, &phdr_count) != 0)
+    return throw_elf_error (error, "elf_getphdrnum");
+
+  for (i = 0; i < phdr_count; i++)
+    {
+      phdr = gelf_getphdr (elf, i, &phdr_mem);
+
+      if (phdr != NULL && phdr->p_type == PT_DYNAMIC)
+        {
+          dynamic_section = gelf_offscn (elf, phdr->p_offset);
+          dynamic_section_header = gelf_getshdr (dynamic_section,
+                                                 &dynamic_section_header_mem);
+          break;
+        }
+    }
+
+  if (dynamic_section == NULL || dynamic_section_header == NULL)
+    return glnx_null_throw (error, "Unable to find dynamic section header");
+
+  string_table_section = elf_getscn (elf, dynamic_section_header->sh_link);
+  string_table_header = gelf_getshdr (string_table_section,
+                                      &string_table_header_mem);
+
+  if (string_table_section == NULL || string_table_header == NULL)
+    return glnx_null_throw (error, "Unable to find linked string table");
+
+  data = elf_getdata (dynamic_section, NULL);
+
+  if (data == NULL)
+    return throw_elf_error (error, "elf_getdata(dynamic_section)");
+
+  size_per_dyn = gelf_fsize (elf, ELF_T_DYN, 1, EV_CURRENT);
+
+  for (i = 0; i < dynamic_section_header->sh_size / size_per_dyn; i++)
+    {
+      dynamic_header = gelf_getdyn (data, i, &dynamic_header_mem);
+
+      if (dynamic_header == NULL)
+        break;
+
+      if (dynamic_header->d_tag == DT_SONAME)
+        {
+          soname = elf_strptr (elf, dynamic_section_header->sh_link,
+                               dynamic_header->d_un.d_val);
+
+          if (soname == NULL)
+            return glnx_null_throw (error, "Unable to read DT_SONAME");
+
+
+        }
+    }
+
+  if (soname == NULL)
+    return glnx_null_throw (error, "Unable to find DT_SONAME");
+
+  return g_strdup (soname);
+}
+
diff --git a/src/elf-utils.h b/src/elf-utils.h
new file mode 100644
index 0000000000000000000000000000000000000000..fa89652e727feb5ca26b5ced048203434fceffb5
--- /dev/null
+++ b/src/elf-utils.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <gelf.h>
+#include <glib.h>
+#include <libelf.h>
+
+#include "libglnx/libglnx.h"
+
+#include "glib-backports.h"
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (Elf, elf_end);
+
+Elf *pv_elf_open_fd (int fd,
+                     GError **error);
+
+gchar *pv_elf_get_soname (Elf *elf,
+                          GError **error);
diff --git a/src/meson.build b/src/meson.build
index 9d9e816a184bf100c1c91729cddb2b5a4782f337..220d1d30be08deec869384d4fab3bf2573979338 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -23,6 +23,7 @@
 
 # Headers to scan for enum/flags types.
 headers = [
+  'resolve-in-sysroot.h',
   'runtime.h',
 ]
 
@@ -36,17 +37,22 @@ pressure_vessel_utils = static_library(
   sources : [
     'bwrap-lock.c',
     'bwrap-lock.h',
+    'elf-utils.c',
+    'elf-utils.h',
     'flatpak-utils-base.c',
     'flatpak-utils-base-private.h',
     'flatpak-utils.c',
     'flatpak-utils-private.h',
     'glib-backports.c',
     'glib-backports.h',
+    'resolve-in-sysroot.c',
+    'resolve-in-sysroot.h',
     'utils.c',
     'utils.h',
   ],
   dependencies : [
     gio_unix,
+    libelf,
     libglnx.get_variable('libglnx_dep'),
   ],
   include_directories : project_include_dirs,
diff --git a/src/resolve-in-sysroot.c b/src/resolve-in-sysroot.c
new file mode 100644
index 0000000000000000000000000000000000000000..8fe6cadb3b1a16f02f4e24442e8c6825b302811e
--- /dev/null
+++ b/src/resolve-in-sysroot.c
@@ -0,0 +1,327 @@
+/*
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "resolve-in-sysroot.h"
+
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <gio/gio.h>
+
+#include "libglnx/libglnx.h"
+
+#include "glib-backports.h"
+
+/* Enabling debug logging for this is rather too verbose, so only
+ * enable it when actively debugging this module */
+#if 0
+#define trace(...) g_debug (__VA_ARGS__)
+#else
+#define trace(...) do { } while (0)
+#endif
+
+/*
+ * clear_fd:
+ * @p: A pointer into the data array underlying a #GArray.
+ *
+ * Close the fd pointed to by @p, and set `*(int *) p` to -1.
+ *
+ * This wraps glnx_close_fd() with the signature required by
+ * g_array_set_clear_func().
+ */
+static void
+clear_fd (void *p)
+{
+  glnx_close_fd (p);
+}
+
+/*
+ * Steal `*fdp` and append it to @fds.
+ *
+ * We can't just use g_array_append_val (fds, glnx_steal_fd (&fd))
+ * because g_array_append_val is a macro that takes a pointer to its
+ * argument.
+ */
+static inline void
+fd_array_take (GArray *fds,
+               int *fdp)
+{
+  int fd = glnx_steal_fd (fdp);
+
+  g_array_append_val (fds, fd);
+}
+
+/*
+ * pv_resolve_in_sysroot:
+ * @sysroot: (transfer none): A file descriptor representing the root
+ * @descendant: (type filename): A path below the root directory, either
+ *  absolute or relative (to the root)
+ * @flags: Flags affecting how we resolve the path
+ * @real_path_out: (optional) (out) (type filename): If not %NULL, used to
+ *  return the path to @descendant below @sysroot
+ * @error: Used to raise an error on failure
+ *
+ * Open @descendant as though @sysroot was the root directory.
+ *
+ * If %PV_RESOLVE_FLAGS_MKDIR_P is in @flags, each path segment in
+ * @descendant must be a directory, a symbolic link to a directory,
+ * or nonexistent (in which case a directory will be created, currently
+ * with hard-coded 0700 permissions).
+ *
+ * Returns: An `O_PATH` file descriptor pointing to @descendant,
+ *  or -1 on error
+ */
+int
+pv_resolve_in_sysroot (int sysroot,
+                       const char *descendant,
+                       PvResolveFlags flags,
+                       gchar **real_path_out,
+                       GError **error)
+{
+  g_autoptr(GString) current_path = g_string_new ("");
+  /* Array of fds pointing to directories beneath @sysroot.
+   * The 0'th element is sysroot itself, the 1st element is a direct
+   * child of sysroot and so on. The last element can be a
+   * non-directory. */
+  g_autoptr(GArray) fds = NULL;
+  /* @buffer contains parts of @descendant. We edit it in-place to replace
+   * each directory separator we have dealt with by \0. */
+  g_autofree gchar *buffer = g_strdup (descendant);
+  /* @remaining points to the remaining path to traverse. For example,
+   * if we are trying to resolve a/b/c/d, we have already opened a, and we
+   * will open b next, then @buffer contains "a\0b/c" and remaining
+   * points to b. */
+  gchar *remaining;
+
+  g_return_val_if_fail (sysroot > 0, -1);
+  g_return_val_if_fail (descendant != NULL, -1);
+  g_return_val_if_fail (real_path_out == NULL || *real_path_out == NULL, -1);
+  g_return_val_if_fail (error == NULL || *error == NULL, -1);
+
+    {
+      glnx_autofd int fd = -1;
+
+      fd = TEMP_FAILURE_RETRY (fcntl (sysroot, F_DUPFD_CLOEXEC, 0));
+
+      if (fd < 0)
+        {
+          glnx_throw_errno_prefix (error, "Unable to duplicate fd \"%d\"",
+                                   sysroot);
+          return -1;
+        }
+
+      fds = g_array_new (FALSE, FALSE, sizeof (int));
+      g_array_set_clear_func (fds, clear_fd);
+      fd_array_take (fds, &fd);
+    }
+
+  remaining = buffer;
+
+  while (remaining != NULL)
+    {
+      g_autofree gchar *target = NULL;
+      glnx_autofd int fd = -1;
+      const gchar *next;
+      gchar *slash;   /* Points into @buffer */
+      int open_flags;
+
+      /* Ignore excess slashes */
+      while (remaining[0] == '/')
+        remaining++;
+
+      next = remaining;
+
+      if (next[0] == '\0')
+        break;
+
+      slash = strchr (remaining, '/');
+
+      if (slash == NULL)
+        {
+          trace ("Done so far: \"%s\"; next: \"%s\"; remaining: nothing",
+                 current_path->str, next);
+          remaining = NULL;
+        }
+      else
+        {
+          *slash = '\0';
+          remaining = slash + 1;
+          trace ("Done so far: \"%s\"; next: \"%s\"; remaining: \"%s\"",
+                 current_path->str, next, remaining);
+        }
+
+      /* Ignore ./ path segments */
+      if (strcmp (next, ".") == 0)
+        continue;
+
+      /* Implement ../ by going up a level - unless we would escape
+       * from the sysroot, in which case do nothing */
+      if (strcmp (next, "..") == 0)
+        {
+          const gchar *last_slash;
+
+          if (fds->len >= 2)
+            g_array_set_size (fds, fds->len - 1);
+          /* else silently ignore ../ when already at the root, the same
+           * as the kernel would */
+
+          last_slash = strrchr (current_path->str, '/');
+
+          if (last_slash != NULL)
+            g_string_truncate (current_path, last_slash - current_path->str);
+          else
+            g_string_truncate (current_path, 0);
+
+          continue;
+        }
+
+      /* Open @next with O_NOFOLLOW, so that if it's a symbolic link,
+       * we open the symbolic link itself and not whatever it points to */
+      open_flags = O_CLOEXEC | O_NOFOLLOW | O_PATH;
+      fd = TEMP_FAILURE_RETRY (openat (g_array_index (fds, int, fds->len - 1),
+                                       next, open_flags));
+
+      if (fd < 0 && errno == ENOENT && (flags & PV_RESOLVE_FLAGS_MKDIR_P) != 0)
+        {
+          if (TEMP_FAILURE_RETRY (mkdirat (g_array_index (fds, int, fds->len - 1),
+                                           next, 0700)) != 0)
+            {
+              glnx_throw_errno_prefix (error, "Unable to create \"%s/%s\"",
+                                       current_path->str, next);
+              return -1;
+            }
+
+          g_debug ("Created \"%s/%s\" in /proc/self/fd/%d",
+                   current_path->str, next, sysroot);
+
+          fd = TEMP_FAILURE_RETRY (openat (g_array_index (fds, int, fds->len - 1),
+                                           next, open_flags | O_DIRECTORY));
+        }
+
+      if (fd < 0)
+        {
+          glnx_throw_errno_prefix (error, "Unable to open \"%s/%s\"",
+                                   current_path->str, next);
+          return -1;
+        }
+
+      /* Maybe it's a symlink? */
+      target = glnx_readlinkat_malloc (fd, "", NULL, NULL);
+
+      if (target != NULL)   /* Yes, it's a symlink */
+        {
+          if (flags & PV_RESOLVE_FLAGS_REJECT_SYMLINKS)
+            {
+              g_set_error (error, G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS,
+                           "\"%s/%s\" is a symlink", current_path->str, next);
+              return -1;
+            }
+          else if ((flags & PV_RESOLVE_FLAGS_KEEP_FINAL_SYMLINK) != 0 &&
+                   remaining == NULL)
+            {
+              /* Treat as though not a symlink. */
+              g_clear_pointer (&target, g_free);
+            }
+        }
+
+      if (target != NULL)
+        {
+          /* This isn't g_autofree because clang would warn about it
+           * being unused. We need to keep it alive until we change
+           * @remaining to point into the new buffer. */
+          gchar *old_buffer = NULL;
+
+          if (target[0] == '/')
+            {
+              /* For example if we were asked to resolve foo/bar/a/b,
+               * but bar is a symlink to /x/y, we restart from the beginning as though
+               * we had been asked to resolve x/y/a/b */
+              trace ("Absolute symlink to \"%s\"", target);
+              g_string_set_size (current_path, 0);
+              g_array_set_size (fds, 1);
+            }
+          else
+            {
+              /* For example if we were asked to resolve foo/bar/a/b,
+               * but bar is a symlink to ../x/y, we continue as though
+               * we had been asked to resolve foo/../x/y/baz */
+              trace ("Relative symlink to \"%s\"/\"%s\"",
+                       current_path->str, target);
+            }
+
+          old_buffer = g_steal_pointer (&buffer);
+          buffer = g_build_filename (target, remaining, NULL);
+          remaining = buffer;
+          g_free (old_buffer);
+        }
+      else  /* Not a symlink, or a symlink but we are returning it anyway. */
+        {
+          /* If we are emulating mkdir -p, or if we will go on to open
+           * a member of @fd, then it had better be a directory. */
+          if ((flags & PV_RESOLVE_FLAGS_MKDIR_P) != 0 ||
+              remaining != NULL)
+            {
+              struct stat stat_buf;
+
+              if (!glnx_fstatat (fd, "", &stat_buf, AT_EMPTY_PATH, error))
+                {
+                  g_prefix_error (error,
+                                  "Unable to determine whether \"%s\" "
+                                  "is a directory",
+                                  current_path->str);
+                  return -1;
+                }
+
+              if (!S_ISDIR (stat_buf.st_mode))
+                {
+                  g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY,
+                               "\"%s\" is not a directory",
+                               current_path->str);
+                }
+            }
+
+          if (current_path->len != 0)
+            g_string_append_c (current_path, '/');
+
+          g_string_append (current_path, next);
+          fd_array_take (fds, &fd);
+        }
+    }
+
+  if (real_path_out != NULL)
+    *real_path_out = g_string_free (g_steal_pointer (&current_path), FALSE);
+
+  if (flags & PV_RESOLVE_FLAGS_READABLE)
+    {
+      g_autofree char *proc_fd_name = g_strdup_printf ("/proc/self/fd/%d",
+                                                       g_array_index (fds, int,
+                                                                      fds->len - 1));
+      glnx_autofd int fd = -1;
+
+      if (!glnx_openat_rdonly (-1, proc_fd_name, TRUE, &fd, error))
+        return -1;
+
+      return glnx_steal_fd (&fd);
+    }
+
+  /* Taking the address might look like nonsense here, but it's
+   * documented to work: g_array_index expands to fds->data[some_offset].
+   * We need to steal ownership of the fd back from @fds so it won't be
+   * closed with the rest of them when @fds is freed. */
+  return glnx_steal_fd (&g_array_index (fds, int, fds->len - 1));
+}
diff --git a/src/resolve-in-sysroot.h b/src/resolve-in-sysroot.h
new file mode 100644
index 0000000000000000000000000000000000000000..2941030c63d2328301b0e1df4fe80541d3bcf17a
--- /dev/null
+++ b/src/resolve-in-sysroot.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <glib.h>
+
+/*
+ * PvResolveFlags:
+ * @PV_RESOLVE_FLAGS_MKDIR_P: Create the filename to be resolved and
+ *  all of its ancestors as directories. If any already exist, they
+ *  must be directories or symlinks to directories.
+ * @PV_RESOLVE_FLAGS_KEEP_FINAL_SYMLINK: If the last component of
+ *  the path is a symlink, return a fd pointing to the symlink itself.
+ * @PV_RESOLVE_FLAGS_REJECT_SYMLINKS: If any component of
+ *  the path is a symlink, fail with %G_IO_ERROR_TOO_MANY_LINKS.
+ * @PV_RESOLVE_FLAGS_READABLE: Open the last component of the path
+ *  for reading, instead of just as `O_PATH`.
+ * @PV_RESOLVE_FLAGS_NONE: No special behaviour.
+ *
+ * Flags affecting how pv_resolve_in_sysroot() behaves.
+ */
+typedef enum
+{
+  PV_RESOLVE_FLAGS_MKDIR_P = (1 << 0),
+  PV_RESOLVE_FLAGS_KEEP_FINAL_SYMLINK = (1 << 1),
+  PV_RESOLVE_FLAGS_REJECT_SYMLINKS = (1 << 2),
+  PV_RESOLVE_FLAGS_READABLE = (1 << 3),
+  PV_RESOLVE_FLAGS_NONE = 0
+} PvResolveFlags;
+
+int pv_resolve_in_sysroot (int sysroot,
+                           const char *descendant,
+                           PvResolveFlags flags,
+                           gchar **real_path_out,
+                           GError **error) G_GNUC_WARN_UNUSED_RESULT;
diff --git a/src/runtime.c b/src/runtime.c
index b4def4e1de4e7f1808360b230de1ef201f902c4c..71a62fc7b4f8c210bb1006f7f3184f8c3241a0a6 100644
--- a/src/runtime.c
+++ b/src/runtime.c
@@ -61,6 +61,7 @@ struct _PvRuntime
   PvRuntimeFlags flags;
   gboolean any_libc_from_host;
   gboolean all_libc_from_host;
+  gboolean runtime_is_just_usr;
 };
 
 struct _PvRuntimeClass
@@ -337,9 +338,14 @@ pv_runtime_initable_init (GInitable *initable,
 
   self->runtime_usr = g_build_filename (self->source_files, "usr", NULL);
 
-  if (!g_file_test (self->runtime_usr, G_FILE_TEST_IS_DIR))
+  if (g_file_test (self->runtime_usr, G_FILE_TEST_IS_DIR))
+    {
+      self->runtime_is_just_usr = FALSE;
+    }
+  else
     {
       /* source_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);
     }
@@ -516,22 +522,65 @@ static gboolean
 pv_runtime_provide_container_access (PvRuntime *self,
                                      GError **error)
 {
-  /* TODO: Avoid using bwrap if we don't need to: when run from inside
-   * a Flatpak, it won't work.
-   *
-   * If we are working with a non-merged-/usr runtime, we can just
-   * set self->container_access to its path.
-   *
-   * Similarly, if we are working with a writeable copy of a runtime
-   * that we are editing in-place, we can set self->container_access to
-   * that. */
-
-  if (self->container_access_adverb == NULL)
+  if (self->container_access_adverb != NULL)
+    return TRUE;
+
+  if (!self->runtime_is_just_usr)
     {
-      self->container_access = g_build_filename (self->tmpdir, "mnt", NULL);
+      static const char * const need_top_level[] =
+      {
+        "bin",
+        "etc",
+        "lib",
+        "sbin",
+      };
+      gsize i;
+
+      /* If we are working with a runtime that has a root directory containing
+       * /etc and /usr, we can just access it via its path - that's "the same
+       * 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. */
+      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);
+
+      /* 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,
+       * but that doesn't guarantee that it has /bin, /lib, /sbin (either
+       * in the form of real directories or symlinks into /usr) and /etc
+       * (for at least /etc/alternatives and /etc/ld.so.cache).
+       *
+       * This check is not intended to be exhaustive, merely something
+       * that will catch obvious mistakes like completely forgetting to
+       * add the merged-/usr symlinks.
+       *
+       * In practice we also need /lib64 for 64-bit-capable runtimes,
+       * but a pure 32-bit runtime would legitimately not have that,
+       * 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,
+                                                     need_top_level[i],
+                                                     NULL);
 
+          if (!g_file_test (path, G_FILE_TEST_IS_DIR))
+            g_warning ("%s does not exist, this probably won't work",
+                       path);
+        }
+    }
+  else
+    {
+      /* Otherwise, will we need to use bwrap to build a directory hierarchy
+       * that is the same shape as the final system. */
+      g_debug ("%s: Using bwrap to set up runtime that is just /usr",
+               G_STRFUNC);
+
+      self->container_access = g_build_filename (self->tmpdir, "mnt", NULL);
       g_mkdir (self->container_access, 0700);
-      self->container_access = self->container_access;
 
       self->container_access_adverb = flatpak_bwrap_new (NULL);
       flatpak_bwrap_add_args (self->container_access_adverb,
@@ -564,9 +613,10 @@ try_bind_dri (PvRuntime *self,
     {
       g_autoptr(FlatpakBwrap) temp_bwrap = NULL;
       g_autofree gchar *expr = NULL;
-      g_autofree gchar *host_dri = NULL;
-      g_autofree gchar *dest_dri = NULL;
+      g_autoptr(GDir) dir = NULL;
+      const char *member;
 
+      g_debug ("Collecting dependencies of DRI drivers in \"%s\"...", dri);
       expr = g_strdup_printf ("only-dependencies:if-exists:path-match:%s/dri/*.so",
                               libdir);
 
@@ -589,28 +639,34 @@ try_bind_dri (PvRuntime *self,
 
       g_clear_pointer (&temp_bwrap, flatpak_bwrap_free);
 
-      /* TODO: If we're already in a container, rely on /run/host
-       * already being mounted, so we don't need to re-enter a container
-       * here. */
-      host_dri = g_build_filename ("/run/host", libdir, "dri", NULL);
-      dest_dri = g_build_filename (arch->libdir_on_host, "dri", NULL);
-      temp_bwrap = flatpak_bwrap_new (NULL);
-      flatpak_bwrap_add_args (temp_bwrap,
-                              self->bubblewrap,
-                              "--ro-bind", "/", "/",
-                              "--tmpfs", "/run",
-                              "--ro-bind", "/", "/run/host",
-                              "--bind", self->overrides, self->overrides,
-                              "sh", "-c",
-                              "ln -fns \"$1\"/* \"$2\"",
-                              "sh",   /* $0 */
-                              host_dri,
-                              dest_dri,
-                              NULL);
-      flatpak_bwrap_finish (temp_bwrap);
+      dir = g_dir_open (dri, 0, error);
 
-      if (!pv_bwrap_run_sync (temp_bwrap, NULL, error))
+      if (dir == NULL)
         return FALSE;
+
+      for (member = g_dir_read_name (dir);
+           member != NULL;
+           member = g_dir_read_name (dir))
+        {
+          g_autofree gchar *target = g_build_filename ("/run/host", dri,
+                                                      member, NULL);
+          g_autofree gchar *dest = g_build_filename (arch->libdir_on_host,
+                                                     "dri", member, NULL);
+
+          g_debug ("Creating symbolic link \"%s\" -> \"%s\" for \"%s\" DRI driver",
+                   dest, target, arch->tuple);
+
+          /* Delete an existing symlink if any, like ln -f */
+          if (unlink (dest) != 0 && errno != ENOENT)
+            return glnx_throw_errno_prefix (error,
+                                            "Unable to remove \"%s\"",
+                                            dest);
+
+          if (symlink (target, dest) != 0)
+            return glnx_throw_errno_prefix (error,
+                                            "Unable to create symlink \"%s\" -> \"%s\"",
+                                            dest, target);
+        }
     }
 
   if (g_file_test (s2tc, G_FILE_TEST_EXISTS))
@@ -618,6 +674,7 @@ try_bind_dri (PvRuntime *self,
       g_autoptr(FlatpakBwrap) temp_bwrap = NULL;
       g_autofree gchar *expr = NULL;
 
+      g_debug ("Collecting s2tc \"%s\" and its dependencies...", s2tc);
       expr = g_strdup_printf ("path-match:%s", s2tc);
 
       if (!pv_runtime_provide_container_access (self, error))
@@ -674,16 +731,11 @@ ensure_locales (PvRuntime *self,
       locale_gen = g_build_filename (self->tools_dir,
                                      "pressure-vessel-locale-gen",
                                      NULL);
-
-      flatpak_bwrap_add_args (run_locale_gen,
-                              self->bubblewrap,
-                              "--ro-bind", "/", "/",
-                              NULL);
-      pv_bwrap_add_api_filesystems (run_locale_gen);
+      /* We don't actually need to use bwrap when we're just running on
+       * the host system. */
       flatpak_bwrap_add_args (run_locale_gen,
-                              "--bind", locales, locales,
-                              "--chdir", locales,
                               locale_gen,
+                              "--output-dir", locales,
                               "--verbose",
                               NULL);
     }
@@ -707,8 +759,8 @@ ensure_locales (PvRuntime *self,
       flatpak_bwrap_add_args (run_locale_gen,
                               "--ro-bind", self->tools_dir, "/run/host/tools",
                               "--bind", locales, "/overrides/locales",
-                              "--chdir", "/overrides/locales",
                               locale_gen,
+                              "--output-dir", "/overrides/locales",
                               "--verbose",
                               NULL);
     }
@@ -1284,6 +1336,7 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self,
             return FALSE;
 
           flatpak_bwrap_add_args (temp_bwrap,
+                                  "env", "PATH=/usr/bin:/bin",
                                   "readlink", "-e", arch->ld_so,
                                   NULL);
           flatpak_bwrap_finish (temp_bwrap);
@@ -1469,7 +1522,13 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self,
 
               g_debug ("Making host ld.so visible in container");
 
-              ld_so_in_host = flatpak_canonicalize_filename (arch->ld_so);
+              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,
@@ -1596,13 +1655,18 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self,
               all_libdrm_from_host = FALSE;
             }
 
+          /* Order matters: drivers from a later entry will overwrite
+           * drivers from an earlier entry. Because we don't know whether
+           * /lib and /usr/lib are 32- or 64-bit, we need to prioritize
+           * libQUAL higher. Prioritize Debian-style multiarch higher
+           * still, because it's completely unambiguous. */
           dirs = g_new0 (gchar *, 7);
-          dirs[0] = g_build_filename ("/lib", arch->tuple, NULL);
-          dirs[1] = g_build_filename ("/usr", "lib", arch->tuple, NULL);
-          dirs[2] = g_strdup ("/lib");
-          dirs[3] = g_strdup ("/usr/lib");
-          dirs[4] = g_build_filename ("/", arch->libqual, NULL);
-          dirs[5] = g_build_filename ("/usr", arch->libqual, NULL);
+          dirs[0] = g_strdup ("/lib");
+          dirs[1] = g_strdup ("/usr/lib");
+          dirs[2] = g_build_filename ("/", arch->libqual, NULL);
+          dirs[3] = g_build_filename ("/usr", arch->libqual, NULL);
+          dirs[4] = g_build_filename ("/lib", arch->tuple, NULL);
+          dirs[5] = g_build_filename ("/usr", "lib", arch->tuple, NULL);
 
           for (j = 0; j < 6; j++)
             {
@@ -2078,16 +2142,8 @@ pv_runtime_set_search_paths (PvRuntime *self,
                              FlatpakBwrap *bwrap)
 {
   g_autoptr(GString) ld_library_path = g_string_new ("");
-  g_autoptr(GString) bin_path = g_string_new ("");
   gsize i;
 
-  pv_search_path_append (bin_path, "/overrides/bin");
-  pv_search_path_append (bin_path, g_getenv ("PATH"));
-  flatpak_bwrap_add_args (bwrap,
-                          "--setenv", "PATH",
-                          bin_path->str,
-                          NULL);
-
   /* TODO: Adapt the use_ld_so_cache code from Flatpak instead
    * of setting LD_LIBRARY_PATH, for better robustness against
    * games that set their own LD_LIBRARY_PATH ignoring what they
@@ -2107,6 +2163,11 @@ pv_runtime_set_search_paths (PvRuntime *self,
   /* This would be filtered out by a setuid bwrap, so we have to go
    * via --setenv. */
   flatpak_bwrap_add_args (bwrap,
+                          /* The PATH from outside the container doesn't
+                           * really make sense inside the container:
+                           * in principle the layout could be totally
+                           * different. */
+                          "--setenv", "PATH", "/overrides/bin:/usr/bin:/bin",
                           "--setenv", "LD_LIBRARY_PATH",
                           ld_library_path->str,
                           NULL);
diff --git a/sysroot/run-in-sysroot.py b/sysroot/run-in-sysroot.py
index eb875f4270adecabf7cd09f2b69f7751fe9137d0..64a712de7772d4c5eb051a92f8169dca181e274e 100755
--- a/sysroot/run-in-sysroot.py
+++ b/sysroot/run-in-sysroot.py
@@ -46,6 +46,7 @@ def main():
     parser = argparse.ArgumentParser()
     parser.add_argument('--srcdir', default='.')
     parser.add_argument('--builddir', default='_build')
+    parser.add_argument('--rw', action='append', default=[])
     parser.add_argument('--sysroot', default=None)
     parser.add_argument('--tarball', default=None)
     parser.add_argument('command')
@@ -94,27 +95,48 @@ def main():
     os.makedirs(
         os.path.join(abs_sysroot, './' + abs_builddir), exist_ok=True)
 
-    os.execvp(
+    argv = [
         'bwrap',
-        [
-            'bwrap',
-            '--ro-bind', abs_sysroot, '/',
-            '--bind',
-            os.path.join(abs_sysroot, 'var', 'lib', 'apt'),
-            '/var/lib/apt',
-            '--dev-bind', '/dev', '/dev',
-            '--ro-bind', '/etc/resolv.conf', '/etc/resolv.conf',
-            '--proc', '/proc',
-            '--tmpfs', '/tmp',
-            '--tmpfs', '/var/tmp',
-            '--tmpfs', '/home',
-            '--bind', abs_srcdir, abs_srcdir,
-            '--bind', abs_builddir, abs_builddir,
-            '--chdir', os.getcwd(),
-            '--setenv', 'LC_ALL', 'C.UTF-8',
-            args.command,
-        ] + args.args,
-    )
+        '--ro-bind', abs_sysroot, '/',
+        '--bind',
+        os.path.join(abs_sysroot, 'var', 'lib', 'apt'),
+        '/var/lib/apt',
+        '--dev-bind', '/dev', '/dev',
+        '--ro-bind', '/etc/resolv.conf', '/etc/resolv.conf',
+        '--proc', '/proc',
+        '--tmpfs', '/tmp',
+        '--tmpfs', '/var/tmp',
+        '--tmpfs', '/home',
+        '--bind', abs_srcdir, abs_srcdir,
+        '--bind', abs_builddir, abs_builddir,
+    ]
+
+    for var in (
+        'AUTOPKGTEST_ARTIFACTS',
+        'DESTDIR',
+        'G_TEST_BUILDDIR',
+        'G_TEST_SRCDIR',
+        'PRESSURE_VESSEL_TEST_CONTAINERS',
+    ):
+        if var in os.environ:
+            val = os.environ[var]
+            argv.extend([
+                '--bind', val, val,
+            ])
+
+    for rw in args.rw:
+        argv.extend([
+            '--bind', rw, rw,
+        ])
+
+    argv.extend([
+        '--chdir', os.getcwd(),
+        '--setenv', 'LC_ALL', 'C.UTF-8',
+        args.command,
+    ])
+    argv.extend(args.args)
+
+    os.execvp('bwrap', argv)
 
 
 if __name__ == '__main__':
diff --git a/tests/containers.py b/tests/containers.py
index 7be681096170991204666dce2e9c774117e04f29..ae474cfc45dbd9a589ed7489afbc1dc96c07e952 100755
--- a/tests/containers.py
+++ b/tests/containers.py
@@ -117,7 +117,7 @@ class TestContainers(BaseTest):
 
         bwrap = os.environ.get('BWRAP', shutil.which('bwrap'))
 
-        if bwrap is not None and subprocess.run(
+        if bwrap is not None and run_subprocess(
             [bwrap, '--dev-bind', '/', '/', 'sh', '-c', 'true'],
             stdout=2,
             stderr=2,
@@ -256,7 +256,7 @@ class TestContainers(BaseTest):
                 os.path.join(cls.artifacts, 'host-srsi.json'),
                 'w',
             ) as writer:
-                subprocess.run(
+                run_subprocess(
                     [
                         host_srsi,
                         '--verbose',
@@ -369,7 +369,7 @@ class TestContainers(BaseTest):
         with tee_file_and_stderr(
             os.path.join(artifacts, 'inside-scout.log')
         ) as tee:
-            completed = subprocess.run(
+            completed = self.run_subprocess(
                 argv,
                 cwd=self.artifacts,
                 stdout=tee.stdin,
diff --git a/tests/elf-get-soname.c b/tests/elf-get-soname.c
new file mode 100644
index 0000000000000000000000000000000000000000..218623ed8cdd2bf5ab04c2504c9a54041b876120
--- /dev/null
+++ b/tests/elf-get-soname.c
@@ -0,0 +1,114 @@
+/*
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include "subprojects/libglnx/config.h"
+
+#include <locale.h>
+#include <sysexits.h>
+
+#include "libglnx/libglnx.h"
+
+#include "glib-backports.h"
+
+#include "elf-utils.h"
+#include "utils.h"
+
+static GOptionEntry options[] =
+{
+  { NULL }
+};
+
+int
+main (int argc,
+      char *argv[])
+{
+  g_autoptr(GOptionContext) context = NULL;
+  g_autoptr(GError) local_error = NULL;
+  GError **error = &local_error;
+  int ret = EX_USAGE;
+  int i;
+
+  setlocale (LC_ALL, "");
+  pv_avoid_gvfs ();
+
+  context = g_option_context_new ("LIBRARY...");
+  g_option_context_add_main_entries (context, options, NULL);
+
+  if (!g_option_context_parse (context, &argc, &argv, error))
+    goto out;
+
+  if (argc >= 2 && strcmp (argv[1], "--") == 0)
+    {
+      argv++;
+      argc--;
+    }
+
+  if (argc < 2)
+    {
+      g_printerr ("A library to open is required\n");
+      goto out;
+    }
+
+  ret = 0;
+
+  for (i = 1; i < argc; i++)
+    {
+      int fd = open (argv[i], O_RDONLY | O_CLOEXEC);
+      g_autoptr(Elf) elf = NULL;
+      g_autofree gchar *soname = NULL;
+
+      if (fd < 0)
+        {
+          g_printerr ("Cannot open %s: %s\n", argv[i], g_strerror (errno));
+          ret = 1;
+          continue;
+        }
+
+      elf = pv_elf_open_fd (fd, error);
+
+      if (elf == NULL)
+        {
+          g_printerr ("Unable to open %s: %s\n",
+                      argv[i], local_error->message);
+          g_clear_error (error);
+          ret = 1;
+          continue;
+        }
+
+      soname = pv_elf_get_soname (elf, error);
+
+      if (soname == NULL)
+        {
+          g_printerr ("Unable to get SONAME of %s: %s\n",
+                      argv[i], local_error->message);
+          g_clear_error (error);
+          ret = 1;
+          continue;
+        }
+
+      g_print ("%s DT_SONAME: %s\n", argv[i], soname);
+    }
+
+out:
+  if (local_error != NULL)
+    g_warning ("%s", local_error->message);
+
+  return ret;
+}
diff --git a/tests/inside-scout.py b/tests/inside-scout.py
index 3b9f4885358c7de050a1a3d4eefaffaac4e41d63..b7e19585ee2db2c8ae6fbc3dbe9f68ad5733f211 100755
--- a/tests/inside-scout.py
+++ b/tests/inside-scout.py
@@ -363,13 +363,14 @@ class TestInsideScout(BaseTest):
                 arch=multiarch,
             ):
                 self.assertTrue(arch_info['can-run'])
-                self.assertEqual([], arch_info['library-issues-summary'])
                 # Graphics driver support depends on the host system, so we
                 # don't assert that everything is fine, only that we have
                 # the information.
                 self.assertIn('graphics-details', arch_info)
                 self.assertIn('glx/gl', arch_info['graphics-details'])
 
+            expect_library_issues = set()
+
             for soname, details in arch_info['library-details'].items():
                 with self.catch(
                     'per-library information',
@@ -377,6 +378,15 @@ class TestInsideScout(BaseTest):
                     arch=multiarch,
                     soname=soname,
                 ):
+                    if soname == 'libldap-2.4.so.2':
+                        # On Debian, libldap-2.4.so.2 is really an alias
+                        # for libldap_r-2.4.so.2; but on Arch Linux they
+                        # are separate libraries, and this causes trouble
+                        # for our library-loading. Ignore failure to load
+                        # the former.
+                        expect_library_issues |= set(details.get('issues', []))
+                        continue
+
                     self.assertIn('path', details)
                     self.assertEqual(
                         [],
@@ -521,6 +531,16 @@ class TestInsideScout(BaseTest):
                                         details.get(key),
                                     )
 
+            with self.catch(
+                'per-architecture information',
+                diagnostic=arch_info,
+                arch=multiarch,
+            ):
+                self.assertEqual(
+                    expect_library_issues,
+                    set(arch_info['library-issues-summary']),
+                )
+
 
 if __name__ == '__main__':
     assert sys.version_info >= (3, 5), 'Python 3.5+ is required'
diff --git a/tests/meson.build b/tests/meson.build
index 8b2bc8d6f9905cd7ba3f4998550998c3ed42d82c..b122e5e4850b367b3d7c400aa9bbfcd103eb9e03 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -43,6 +43,7 @@ tests = [
 
 compiled_tests = [
   'bwrap-lock',
+  'resolve-in-sysroot',
   'utils',
 ]
 
@@ -101,20 +102,28 @@ foreach test_name : tests
   endif
 endforeach
 
-executable(
-  'test-cheap-copy',
-  sources : [
-    'cheap-copy.c',
-  ],
-  dependencies : [
-    gio_unix,
-    libglnx.get_variable('libglnx_dep'),
-  ],
-  link_with : [
-    pressure_vessel_utils,
-  ],
-  include_directories : project_include_dirs,
-  install : false,
-)
+# Helper programs and manual tests
+helpers = [
+  'cheap-copy',
+  'elf-get-soname',
+]
+
+foreach helper : helpers
+  executable(
+    'test-' + helper,
+    sources : [
+      helper + '.c',
+    ],
+    dependencies : [
+      gio_unix,
+      libglnx.get_variable('libglnx_dep'),
+    ],
+    link_with : [
+      pressure_vessel_utils,
+    ],
+    include_directories : project_include_dirs,
+    install : false,
+  )
+endforeach
 
 # vim:set sw=2 sts=2 et:
diff --git a/tests/resolve-in-sysroot.c b/tests/resolve-in-sysroot.c
new file mode 100644
index 0000000000000000000000000000000000000000..7256764fc52b179c53d23805120401b020017add
--- /dev/null
+++ b/tests/resolve-in-sysroot.c
@@ -0,0 +1,273 @@
+/*
+ * Copyright © 2019-2020 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 "glib-backports.h"
+#include "libglnx/libglnx.h"
+
+#include "resolve-in-sysroot.h"
+#include "test-utils.h"
+#include "utils.h"
+
+typedef struct
+{
+  TestsOpenFdSet old_fds;
+} Fixture;
+
+static void
+setup (Fixture *f,
+       gconstpointer context)
+{
+  f->old_fds = tests_check_fd_leaks_enter ();
+}
+
+static void
+teardown (Fixture *f,
+          gconstpointer context)
+{
+  tests_check_fd_leaks_leave (f->old_fds);
+}
+
+static gboolean
+fd_same_as_rel_path_nofollow (int fd,
+                              int dfd,
+                              const gchar *path)
+{
+  GStatBuf fd_buffer, path_buffer;
+
+  return (fstat (fd, &fd_buffer) == 0
+          && fstatat (dfd, path, &path_buffer, AT_SYMLINK_NOFOLLOW) == 0
+          && fd_buffer.st_dev == path_buffer.st_dev
+          && fd_buffer.st_ino == path_buffer.st_ino);
+}
+
+typedef struct
+{
+  const char *name;
+  const char *target;
+} Symlink;
+
+typedef enum
+{
+  RESOLVE_ERROR_DOMAIN_NONE,
+  RESOLVE_ERROR_DOMAIN_GIO,
+} ResolveErrorDomain;
+
+typedef enum
+{
+  RESOLVE_CALL_FLAGS_IGNORE_PATH = (1 << 0),
+  RESOLVE_CALL_FLAGS_NONE = 0
+} ResolveCallFlags;
+
+typedef struct
+{
+  struct
+  {
+    const char *path;
+    PvResolveFlags flags;
+    ResolveCallFlags test_flags;
+  } call;
+  struct
+  {
+    const char *path;
+    int code;
+  } expect;
+} ResolveTest;
+
+static void
+test_resolve_in_sysroot (Fixture *f,
+                         gconstpointer context)
+{
+  static const char * const prepare_dirs[] =
+  {
+    "a/b/c/d/e",
+    "a/b2/c2/d2/e2",
+  };
+  static const Symlink prepare_symlinks[] =
+  {
+    { "a/b/symlink_to_c", "c" },
+    { "a/b/symlink_to_b2", "../b2" },
+    { "a/b/symlink_to_c2", "../../a/b2/c2" },
+    { "a/b/abs_symlink_to_run", "/run" },
+    { "a/b/long_symlink_to_dev", "../../../../../../../../../../../dev" },
+    { "x", "create_me" },
+  };
+  static const ResolveTest tests[] =
+  {
+    { { "a/b/c/d" }, { "a/b/c/d" } },
+    {
+      { "a/b/c/d", PV_RESOLVE_FLAGS_NONE, RESOLVE_CALL_FLAGS_IGNORE_PATH },
+      { "a/b/c/d" },
+    },
+    { { "a/b/c/d", PV_RESOLVE_FLAGS_MKDIR_P }, { "a/b/c/d" } },
+    {
+      { "a/b/c/d", PV_RESOLVE_FLAGS_MKDIR_P, RESOLVE_CALL_FLAGS_IGNORE_PATH },
+      { "a/b/c/d" },
+    },
+    { { "create_me" }, { NULL, G_IO_ERROR_NOT_FOUND } },
+    {
+      { "create_me", PV_RESOLVE_FLAGS_NONE, RESOLVE_CALL_FLAGS_IGNORE_PATH },
+      { NULL, G_IO_ERROR_NOT_FOUND }
+    },
+    { { "a/b/c/d", PV_RESOLVE_FLAGS_MKDIR_P }, { "a/b/c/d" } },
+    { { "a/b///////.////./././///././c/d" }, { "a/b/c/d" } },
+    { { "/a/b///////.////././../b2////././c2/d2" }, { "a/b2/c2/d2" } },
+    { { "a/b/c/d/e/f" }, { NULL, G_IO_ERROR_NOT_FOUND } },
+    { { "a/b/c/d/e/f", PV_RESOLVE_FLAGS_MKDIR_P }, { "a/b/c/d/e/f" } },
+    { { "a/b/c/d/e/f" }, { "a/b/c/d/e/f" } },
+    { { "a/b/c/d/e/f", PV_RESOLVE_FLAGS_MKDIR_P }, { "a/b/c/d/e/f" } },
+    { { "a3/b3/c3" }, { NULL, G_IO_ERROR_NOT_FOUND } },
+    { { "a3/b3/c3", PV_RESOLVE_FLAGS_MKDIR_P }, { "a3/b3/c3" } },
+    { { "a/b/symlink_to_c" }, { "a/b/c" } },
+    { { "a/b/symlink_to_c/d" }, { "a/b/c/d" } },
+    {
+      { "a/b/symlink_to_c/d", PV_RESOLVE_FLAGS_KEEP_FINAL_SYMLINK },
+      { "a/b/c/d" }
+    },
+    {
+      { "a/b/symlink_to_c/d", PV_RESOLVE_FLAGS_REJECT_SYMLINKS },
+      { NULL, G_IO_ERROR_TOO_MANY_LINKS }
+    },
+    { { "a/b/symlink_to_b2" }, { "a/b2" } },
+    { { "a/b/symlink_to_c2" }, { "a/b2/c2" } },
+    { { "a/b/abs_symlink_to_run" }, { NULL, G_IO_ERROR_NOT_FOUND } },
+    {
+      { "a/b/abs_symlink_to_run", PV_RESOLVE_FLAGS_KEEP_FINAL_SYMLINK },
+      { "a/b/abs_symlink_to_run" }
+    },
+    { { "run" }, { NULL, G_IO_ERROR_NOT_FOUND } },    /* Wasn't created yet */
+    { { "a/b/abs_symlink_to_run", PV_RESOLVE_FLAGS_MKDIR_P }, { "run" } },
+    { { "a/b/abs_symlink_to_run/host" }, { NULL, G_IO_ERROR_NOT_FOUND } },
+    { { "a/b/abs_symlink_to_run/host", PV_RESOLVE_FLAGS_MKDIR_P }, { "run/host" } },
+    { { "a/b/long_symlink_to_dev" }, { NULL, G_IO_ERROR_NOT_FOUND } },
+    { { "a/b/long_symlink_to_dev/shm" }, { NULL, G_IO_ERROR_NOT_FOUND } },
+    { { "a/b/long_symlink_to_dev/shm", PV_RESOLVE_FLAGS_MKDIR_P }, { "dev/shm" } },
+    { { "a/b/../b2/c2/../c3", PV_RESOLVE_FLAGS_MKDIR_P }, { "a/b2/c3" } },
+    { { "x" }, { NULL, G_IO_ERROR_NOT_FOUND } },
+    { { "x", PV_RESOLVE_FLAGS_KEEP_FINAL_SYMLINK }, { "x" } },
+    /* This is a bit odd: unlike mkdir -p, we create targets for dangling
+     * symlinks. It's easier to do this than not, and for pressure-vessel's
+     * use-case it probably even makes more sense than not. */
+    { { "x/y" }, { NULL, G_IO_ERROR_NOT_FOUND } },
+    { { "x/y", PV_RESOLVE_FLAGS_MKDIR_P }, { "create_me/y" } },
+  };
+  g_autoptr(GError) error = NULL;
+  g_auto(GLnxTmpDir) tmpdir = { FALSE };
+  gsize i;
+
+  glnx_mkdtemp ("test-XXXXXX", 0700, &tmpdir, &error);
+  g_assert_no_error (error);
+
+  for (i = 0; i < G_N_ELEMENTS (prepare_dirs); i++)
+    {
+      const char *it = prepare_dirs[i];
+
+      glnx_shutil_mkdir_p_at (tmpdir.fd, it, 0700, NULL, &error);
+      g_assert_no_error (error);
+    }
+
+  for (i = 0; i < G_N_ELEMENTS (prepare_symlinks); i++)
+    {
+      const Symlink *it = &prepare_symlinks[i];
+
+      if (symlinkat (it->target, tmpdir.fd, it->name) != 0)
+        g_error ("symlinkat %s: %s", it->name, g_strerror (errno));
+    }
+
+  for (i = 0; i < G_N_ELEMENTS (tests); i++)
+    {
+      const ResolveTest *it = &tests[i];
+      glnx_autofd int fd = -1;
+      g_autofree gchar *path = NULL;
+      gchar **out_path;
+      g_autoptr(GString) description = g_string_new ("");
+      TestsOpenFdSet old_fds;
+
+      old_fds = tests_check_fd_leaks_enter ();
+
+      if (it->call.flags & PV_RESOLVE_FLAGS_MKDIR_P)
+        g_string_append (description, " (creating directories)");
+
+      if (it->call.flags & PV_RESOLVE_FLAGS_KEEP_FINAL_SYMLINK)
+        g_string_append (description, " (not following final symlink)");
+
+      if (it->call.flags & PV_RESOLVE_FLAGS_REJECT_SYMLINKS)
+        g_string_append (description, " (not following any symlink)");
+
+      g_test_message ("%" G_GSIZE_FORMAT ": Resolving %s%s",
+                      i, it->call.path, description->str);
+
+      if (it->call.test_flags & RESOLVE_CALL_FLAGS_IGNORE_PATH)
+        out_path = NULL;
+      else
+        out_path = &path;
+
+      fd = pv_resolve_in_sysroot (tmpdir.fd, it->call.path,
+                                  it->call.flags, out_path, &error);
+
+      if (it->expect.path != NULL)
+        {
+          g_assert_no_error (error);
+          g_assert_cmpint (fd, >=, 0);
+
+          if (out_path != NULL)
+            g_assert_cmpstr (*out_path, ==, it->expect.path);
+
+          g_assert_true (fd_same_as_rel_path_nofollow (fd, tmpdir.fd,
+                                                       it->expect.path));
+        }
+      else
+        {
+          g_assert_error (error, G_IO_ERROR, it->expect.code);
+          g_assert_cmpint (fd, ==, -1);
+
+          if (out_path != NULL)
+            g_assert_cmpstr (*out_path, ==, NULL);
+
+          g_clear_error (&error);
+        }
+
+      glnx_close_fd (&fd);
+      tests_check_fd_leaks_leave (old_fds);
+    }
+}
+
+int
+main (int argc,
+      char **argv)
+{
+  pv_avoid_gvfs ();
+
+  g_test_init (&argc, &argv, NULL);
+  g_test_add ("/resolve-in-sysroot", Fixture, NULL,
+              setup, test_resolve_in_sysroot, teardown);
+
+  return g_test_run ();
+}