diff --git a/tests/meson.build b/tests/meson.build
index 8b2bc8d6f9905cd7ba3f4998550998c3ed42d82c..64bceaefd0983658ee251789ad4d9aceed75c43f 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -43,6 +43,7 @@ tests = [
 
 compiled_tests = [
   'bwrap-lock',
+  'resolve-in-sysroot',
   'utils',
 ]
 
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 ();
+}