debdiff flatpak_1.16.{3,6}-1~deb13u1.dsc | filterdiff -p1 -x'po/*.po' >| flatpak_1.16.6-1~deb13u1.diff

diffstat for flatpak-1.16.3 flatpak-1.16.6

 NEWS                                           |   50 +
 app/flatpak-builtins-build.c                   |    8 
 app/flatpak-builtins-run.c                     |  161 +++++
 common/flatpak-bwrap-private.h                 |    5 
 common/flatpak-bwrap.c                         |   33 -
 common/flatpak-context.c                       |   17 
 common/flatpak-dir.c                           |   12 
 common/flatpak-installation.c                  |    4 
 common/flatpak-oci-registry.c                  |    5 
 common/flatpak-run-private.h                   |   13 
 common/flatpak-run.c                           |  523 +++++++++++-----
 common/flatpak-utils-private.h                 |   12 
 common/flatpak-utils.c                         |  172 +++++
 debian/changelog                               |   91 ++
 debian/control                                 |    2 
 meson.build                                    |    4 
 po/bg.po                                       |  443 +++++++-------
 po/cs.po                                       |  441 +++++++------
 po/da.po                                       |  446 +++++++-------
 po/de.po                                       |  441 +++++++------
 po/en_GB.po                                    |  441 +++++++------
 po/es.po                                       |  443 +++++++-------
 po/fr.po                                       |  441 +++++++------
 po/gl.po                                       |  441 +++++++------
 po/hi.po                                       |  443 +++++++-------
 po/hr.po                                       |  443 +++++++-------
 po/hu.po                                       |  441 +++++++------
 po/id.po                                       |  443 +++++++-------
 po/ka.po                                       |  443 +++++++-------
 po/nl.po                                       |  441 +++++++------
 po/oc.po                                       |  441 +++++++------
 po/pl.po                                       |  443 +++++++-------
 po/pt.po                                       |  443 +++++++-------
 po/pt_BR.po                                    |  443 +++++++-------
 po/ro.po                                       |  441 +++++++------
 po/ru.po                                       |  443 +++++++-------
 po/sk.po                                       |  441 +++++++------
 po/sl.po                                       |  441 +++++++------
 po/sv.po                                       |  445 +++++++-------
 po/tr.po                                       |  445 +++++++-------
 po/uk.po                                       |  443 +++++++-------
 po/zh_CN.po                                    |  443 +++++++-------
 po/zh_TW.po                                    |  441 +++++++------
 portal/flatpak-portal.c                        |  461 ++++++--------
 subprojects/libglnx/Makefile-libglnx.am        |    2 
 subprojects/libglnx/glnx-backports.c           |   72 ++
 subprojects/libglnx/glnx-backports.h           |   29 
 subprojects/libglnx/glnx-chase.c               |  789 +++++++++++++++++++++++++
 subprojects/libglnx/glnx-chase.h               |   51 +
 subprojects/libglnx/glnx-errors.h              |   14 
 subprojects/libglnx/glnx-fdio.c                |   82 ++
 subprojects/libglnx/glnx-fdio.h                |   36 +
 subprojects/libglnx/glnx-local-alloc.h         |    1 
 subprojects/libglnx/glnx-lockfile.c            |    2 
 subprojects/libglnx/glnx-missing-syscall.h     |  353 +++++++++++
 subprojects/libglnx/libglnx.h                  |    1 
 subprojects/libglnx/meson.build                |    2 
 subprojects/libglnx/tests/meson.build          |    1 
 subprojects/libglnx/tests/test-libglnx-chase.c |  609 +++++++++++++++++++
 subprojects/libglnx/tests/test-libglnx-fdio.c  |  153 ++++
 system-helper/flatpak-system-helper.c          |   85 +-
 tests/apply-extra-static.c                     |   15 
 tests/libtest.sh                               |    2 
 tests/meson.build                              |   16 
 tests/test-extra-data.sh                       |  104 +++
 tests/test-matrix/meson.build                  |    4 
 tests/test-run-custom.sh                       |  200 ++++++
 tests/update-test-matrix                       |    1 
 68 files changed, 10045 insertions(+), 6096 deletions(-)

diff -Nru flatpak-1.16.3/app/flatpak-builtins-build.c flatpak-1.16.6/app/flatpak-builtins-build.c
--- flatpak-1.16.3/app/flatpak-builtins-build.c	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/app/flatpak-builtins-build.c	2026-04-10 18:28:42.000000000 +0100
@@ -508,7 +508,13 @@
   /* Never set up an a11y bus for builds */
   run_flags |= FLATPAK_RUN_FLAG_NO_A11Y_BUS_PROXY;
 
-  if (!flatpak_run_setup_base_argv (bwrap, runtime_files, app_id_dir, arch,
+  glnx_autofd int usr_fd = -1;
+  usr_fd = open (flatpak_file_get_path_cached (runtime_files),
+                 O_PATH | O_CLOEXEC | O_NOFOLLOW);
+  if (usr_fd < 0)
+    return glnx_throw_errno_prefix (error, "Failed to open runtime files");
+
+  if (!flatpak_run_setup_base_argv (bwrap, usr_fd, app_id_dir, arch,
                                     run_flags, error))
     return FALSE;
 
diff -Nru flatpak-1.16.3/app/flatpak-builtins-run.c flatpak-1.16.6/app/flatpak-builtins-run.c
--- flatpak-1.16.3/app/flatpak-builtins-run.c	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/app/flatpak-builtins-run.c	2026-04-10 18:28:42.000000000 +0100
@@ -60,7 +60,98 @@
 static gboolean opt_parent_share_pids;
 static int opt_instance_id_fd = -1;
 static char *opt_app_path;
+static int opt_app_fd = -1;
 static char *opt_usr_path;
+static int opt_usr_fd = -1;
+static GArray *opt_bind_fds = NULL;
+static GArray *opt_ro_bind_fds = NULL;
+
+static gboolean
+option_bind_fd_cb (const char  *option_name,
+                   const char  *value,
+                   gpointer     data,
+                   GError     **error)
+{
+  glnx_autofd int fd = -1;
+
+  fd = flatpak_accept_fd_argument (option_name, value, error);
+
+  if (fd < 0)
+    return FALSE;
+
+  g_array_append_val (opt_bind_fds, fd);
+  fd = -1; /* ownership transferred to GArray */
+  return TRUE;
+}
+
+static gboolean
+option_ro_bind_fd_cb (const char  *option_name,
+                      const char  *value,
+                      gpointer     data,
+                      GError     **error)
+{
+  glnx_autofd int fd = -1;
+
+  fd = flatpak_accept_fd_argument (option_name, value, error);
+
+  if (fd < 0)
+    return FALSE;
+
+  g_array_append_val (opt_ro_bind_fds, fd);
+  fd = -1; /* ownership transferred to GArray */
+  return TRUE;
+}
+
+static gboolean
+opt_instance_id_fd_cb (const char  *option_name,
+                       const char  *value,
+                       gpointer     data,
+                       GError     **error)
+{
+  glnx_autofd int fd = -1;
+
+  fd = flatpak_accept_fd_argument (option_name, value, error);
+
+  if (fd < 0)
+    return FALSE;
+
+  opt_instance_id_fd = g_steal_fd (&fd);
+  return TRUE;
+}
+
+static gboolean
+opt_app_fd_cb (const char  *option_name,
+               const char  *value,
+               gpointer     data,
+               GError     **error)
+{
+  glnx_autofd int fd = -1;
+
+  fd = flatpak_accept_fd_argument (option_name, value, error);
+
+  if (fd < 0)
+    return FALSE;
+
+  opt_app_fd = g_steal_fd (&fd);
+  return TRUE;
+}
+
+static gboolean
+opt_usr_fd_cb (const char  *option_name,
+               const char  *value,
+               gpointer     data,
+               GError     **error)
+{
+  glnx_autofd int fd = -1;
+
+  fd = flatpak_accept_fd_argument (option_name, value, error);
+
+  if (fd < 0)
+    return FALSE;
+
+  opt_usr_fd = g_steal_fd (&fd);
+  return TRUE;
+}
 
 static GOptionEntry options[] = {
   { "arch", 0, 0, G_OPTION_ARG_STRING, &opt_arch, N_("Arch to use"), N_("ARCH") },
@@ -86,9 +177,13 @@
   { "parent-pid", 0, 0, G_OPTION_ARG_INT, &opt_parent_pid, N_("Use PID as parent pid for sharing namespaces"), N_("PID") },
   { "parent-expose-pids", 0, 0, G_OPTION_ARG_NONE, &opt_parent_expose_pids, N_("Make processes visible in parent namespace"), NULL },
   { "parent-share-pids", 0, 0, G_OPTION_ARG_NONE, &opt_parent_share_pids, N_("Share process ID namespace with parent"), NULL },
-  { "instance-id-fd", 0, 0, G_OPTION_ARG_INT, &opt_instance_id_fd, N_("Write the instance ID to the given file descriptor"), NULL },
+  { "instance-id-fd", 0, 0, G_OPTION_ARG_CALLBACK, &opt_instance_id_fd_cb, N_("Write the instance ID to the given file descriptor"), NULL },
   { "app-path", 0, 0, G_OPTION_ARG_FILENAME, &opt_app_path, N_("Use PATH instead of the app's /app"), N_("PATH") },
+  { "app-fd", 0, 0, G_OPTION_ARG_CALLBACK, &opt_app_fd_cb, N_("Use FD instead of the app's /app"), N_("FD") },
   { "usr-path", 0, 0, G_OPTION_ARG_FILENAME, &opt_usr_path, N_("Use PATH instead of the runtime's /usr"), N_("PATH") },
+  { "usr-fd", 0, 0, G_OPTION_ARG_CALLBACK, &opt_usr_fd_cb, N_("Use FD instead of the runtime's /usr"), N_("FD") },
+  { "bind-fd", 0, 0, G_OPTION_ARG_CALLBACK | G_OPTION_FLAG_HIDDEN, &option_bind_fd_cb, N_("Bind mount the file or directory referred to by FD to its canonicalized path"), N_("FD") },
+  { "ro-bind-fd", 0, 0, G_OPTION_ARG_CALLBACK | G_OPTION_FLAG_HIDDEN, &option_ro_bind_fd_cb, N_("Bind mount the file or directory referred to by FD read-only to its canonicalized path"), N_("FD") },
   { NULL }
 };
 
@@ -111,9 +206,14 @@
   g_autoptr(GError) local_error = NULL;
   g_autoptr(GPtrArray) dirs = NULL;
   FlatpakRunFlags flags = 0;
+  glnx_autofd int app_fd = -1;
+  glnx_autofd int usr_fd = -1;
 
   run_environ = g_get_environ ();
 
+  opt_bind_fds = g_array_new (FALSE, FALSE, sizeof (int));
+  opt_ro_bind_fds = g_array_new (FALSE, FALSE, sizeof (int));
+
   context = g_option_context_new (_("APP [ARGUMENT…] - Run an app"));
   g_option_context_set_translation_domain (context, GETTEXT_PACKAGE);
 
@@ -309,14 +409,67 @@
   if (!opt_session_bus)
     flags |= FLATPAK_RUN_FLAG_NO_SESSION_BUS_PROXY;
 
+  if (opt_app_fd >= 0 && opt_app_path != NULL)
+    {
+      flatpak_fail_error (error, FLATPAK_ERROR,
+                          _("app-fd and app-path cannot both be used"));
+      return FALSE;
+    }
+
+  if (opt_app_fd >= 0)
+    {
+      app_fd = opt_app_fd;
+    }
+  else if (opt_app_path != NULL)
+    {
+      if (g_strcmp0 (opt_app_path, "") == 0)
+        {
+          app_fd = FLATPAK_RUN_APP_DEPLOY_APP_EMPTY;
+        }
+      else
+        {
+          app_fd = open (opt_app_path, O_PATH | O_CLOEXEC | O_NOFOLLOW);
+
+          if (app_fd < 0)
+            return glnx_throw_errno_prefix (error, "Failed to open app-path");
+        }
+    }
+  else
+    {
+      app_fd = FLATPAK_RUN_APP_DEPLOY_APP_ORIGINAL;
+    }
+
+  if (opt_usr_fd >= 0 && opt_usr_path != NULL)
+    {
+      flatpak_fail_error (error, FLATPAK_ERROR,
+                          _("usr-fd and usr-path cannot both be used"));
+      return FALSE;
+    }
+
+  if (opt_usr_fd >= 0)
+    {
+      usr_fd = opt_usr_fd;
+    }
+  else if (opt_usr_path != NULL)
+    {
+      usr_fd = open (opt_usr_path, O_PATH | O_CLOEXEC | O_NOFOLLOW);
+
+      if (usr_fd < 0)
+        return glnx_throw_errno_prefix (error, "Failed to open usr-path");
+    }
+  else
+    {
+      usr_fd = FLATPAK_RUN_APP_DEPLOY_USR_ORIGINAL;
+    }
+
   if (!flatpak_run_app (app_deploy ? app_ref : runtime_ref,
                         app_deploy,
-                        opt_app_path,
+                        app_fd,
                         arg_context,
                         opt_runtime,
                         opt_runtime_version,
                         opt_runtime_commit,
-                        opt_usr_path,
+                        usr_fd,
                         opt_parent_pid,
                         flags,
                         opt_cwd,
@@ -326,6 +479,8 @@
                         opt_instance_id_fd,
                         (const char * const *) run_environ,
                         NULL,
+                        opt_bind_fds,
+                        opt_ro_bind_fds,
                         cancellable,
                         error))
     return FALSE;
diff -Nru flatpak-1.16.3/common/flatpak-bwrap.c flatpak-1.16.6/common/flatpak-bwrap.c
--- flatpak-1.16.3/common/flatpak-bwrap.c	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/common/flatpak-bwrap.c	2026-04-10 18:28:42.000000000 +0100
@@ -42,15 +42,6 @@
 #include "flatpak-utils-private.h"
 #include "flatpak-utils-base-private.h"
 
-static void
-clear_fd (gpointer data)
-{
-  int *fd_p = data;
-
-  if (fd_p != NULL && *fd_p != -1)
-    close (*fd_p);
-}
-
 char *flatpak_bwrap_empty_env[] = { NULL };
 
 FlatpakBwrap *
@@ -60,9 +51,9 @@
 
   bwrap->argv = g_ptr_array_new_with_free_func (g_free);
   bwrap->noinherit_fds = g_array_new (FALSE, TRUE, sizeof (int));
-  g_array_set_clear_func (bwrap->noinherit_fds, clear_fd);
+  g_array_set_clear_func (bwrap->noinherit_fds, (GDestroyNotify) glnx_close_fd);
   bwrap->fds = g_array_new (FALSE, TRUE, sizeof (int));
-  g_array_set_clear_func (bwrap->fds, clear_fd);
+  g_array_set_clear_func (bwrap->fds, (GDestroyNotify) glnx_close_fd);
 
   if (env)
     bwrap->envp = g_strdupv (env);
@@ -146,6 +137,26 @@
   g_array_append_val (bwrap->fds, fd);
 }
 
+gboolean
+flatpak_bwrap_add_args_data_fd_dup (FlatpakBwrap  *bwrap,
+                                    const char    *op,
+                                    int            fd,
+                                    const char    *path_optional,
+                                    GError       **error)
+{
+  glnx_autofd int fd_dup = -1;
+
+  fd_dup = fcntl (fd, F_DUPFD_CLOEXEC, 3);
+  if (fd_dup < 0)
+    return glnx_throw_errno_prefix (error, "Failed to dup fd %d", fd);
+
+  flatpak_bwrap_add_args_data_fd (bwrap,
+                                  op,
+                                  g_steal_fd (&fd_dup),
+                                  path_optional);
+  return TRUE;
+}
+
 void
 flatpak_bwrap_add_arg_printf (FlatpakBwrap *bwrap, const char *format, ...)
 {
diff -Nru flatpak-1.16.3/common/flatpak-bwrap-private.h flatpak-1.16.6/common/flatpak-bwrap-private.h
--- flatpak-1.16.3/common/flatpak-bwrap-private.h	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/common/flatpak-bwrap-private.h	2026-04-10 18:28:42.000000000 +0100
@@ -63,6 +63,11 @@
                                           FlatpakBwrap *other);       /* Steals the fds */
 void          flatpak_bwrap_append_args (FlatpakBwrap *bwrap,
                                          GPtrArray    *other_array);
+gboolean      flatpak_bwrap_add_args_data_fd_dup (FlatpakBwrap  *bwrap,
+                                                  const char    *op,
+                                                  int            fd,
+                                                  const char    *path_optional,
+                                                  GError       **error);
 void          flatpak_bwrap_add_args_data_fd (FlatpakBwrap *bwrap,
                                               const char   *op,
                                               int           fd,
diff -Nru flatpak-1.16.3/common/flatpak-context.c flatpak-1.16.6/common/flatpak-context.c
--- flatpak-1.16.3/common/flatpak-context.c	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/common/flatpak-context.c	2026-04-10 18:28:42.000000000 +0100
@@ -1382,21 +1382,14 @@
                   GError     **error)
 {
   FlatpakContext *context = data;
-  guint64 fd;
-  gchar *endptr;
-  gboolean ret;
+  glnx_autofd int fd = -1;
 
-  fd = g_ascii_strtoull (value, &endptr, 10);
+  fd = flatpak_accept_fd_argument (option_name, value, error);
 
-  if (endptr == NULL || *endptr != '\0' || fd > G_MAXINT)
-    return glnx_throw (error, "Not a valid file descriptor: %s", value);
+  if (fd < 0)
+    return FALSE;
 
-  ret = flatpak_context_parse_env_fd (context, (int) fd, error);
-
-  if (fd >= 3)
-    close (fd);
-
-  return ret;
+  return flatpak_context_parse_env_fd (context, fd, error);
 }
 
 static gboolean
diff -Nru flatpak-1.16.3/common/flatpak-dir.c flatpak-1.16.6/common/flatpak-dir.c
--- flatpak-1.16.3/common/flatpak-dir.c	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/common/flatpak-dir.c	2026-04-10 18:28:42.000000000 +0100
@@ -8475,7 +8475,17 @@
    * Disable /proc entirely in this context. */
   run_flags |= FLATPAK_RUN_FLAG_NO_PROC;
 
-  if (!flatpak_run_setup_base_argv (bwrap, runtime_files, NULL, runtime_arch,
+  glnx_autofd int usr_fd = -1;
+
+  if (runtime_files != NULL)
+    {
+      usr_fd = open (flatpak_file_get_path_cached (runtime_files),
+                     O_PATH | O_CLOEXEC | O_NOFOLLOW);
+      if (usr_fd < 0)
+        return glnx_throw_errno_prefix (error, "Failed to open runtime files");
+    }
+
+  if (!flatpak_run_setup_base_argv (bwrap, usr_fd, NULL, runtime_arch,
                                     run_flags, error))
     return FALSE;
 
diff -Nru flatpak-1.16.3/common/flatpak-installation.c flatpak-1.16.6/common/flatpak-installation.c
--- flatpak-1.16.3/common/flatpak-installation.c	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/common/flatpak-installation.c	2026-04-10 18:28:42.000000000 +0100
@@ -703,9 +703,10 @@
 
   if (!flatpak_run_app (app_ref,
                         app_deploy,
+                        FLATPAK_RUN_APP_DEPLOY_APP_ORIGINAL,
                         NULL,
-                        NULL, NULL,
                         NULL, NULL, NULL,
+                        FLATPAK_RUN_APP_DEPLOY_USR_ORIGINAL,
                         0,
                         run_flags,
                         NULL,
@@ -713,6 +714,7 @@
                         NULL, 0, -1,
                         (const char * const *) run_environ,
                         &instance_dir,
+                        NULL, NULL,
                         cancellable, error))
     return FALSE;
 
diff -Nru flatpak-1.16.3/common/flatpak-oci-registry.c flatpak-1.16.6/common/flatpak-oci-registry.c
--- flatpak-1.16.3/common/flatpak-oci-registry.c	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/common/flatpak-oci-registry.c	2026-04-10 18:28:42.000000000 +0100
@@ -275,6 +275,9 @@
   return oci_registry;
 }
 
+/* Carefully opens a file from a base directory and subpath,
+ * making sure that its not a symlink, pipe, etc.
+ */
 static int
 local_open_file (int           dfd,
                  const char   *subpath,
@@ -286,7 +289,7 @@
   struct stat tmp_st_buf;
 
   do
-    fd = openat (dfd, subpath, O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY);
+    fd = openat (dfd, subpath, O_NOFOLLOW | O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY);
   while (G_UNLIKELY (fd == -1 && errno == EINTR));
   if (fd == -1)
     {
diff -Nru flatpak-1.16.3/common/flatpak-run.c flatpak-1.16.6/common/flatpak-run.c
--- flatpak-1.16.3/common/flatpak-run.c	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/common/flatpak-run.c	2026-04-10 18:28:42.000000000 +0100
@@ -1366,7 +1366,7 @@
                                gboolean            build,
                                gboolean            devel,
                                char              **app_info_path_out,
-                               int                 instance_id_fd,
+                               int                 instance_id_fd_arg,
                                char              **instance_id_host_dir_out,
                                char              **instance_id_host_private_dir_out,
                                char              **instance_id_out,
@@ -1374,7 +1374,11 @@
 {
   g_autofree char *info_path = NULL;
   g_autofree char *bwrapinfo_path = NULL;
-  int fd, fd2, fd3;
+  glnx_autofd int fd1 = -1;
+  glnx_autofd int fd2 = -1;
+  glnx_autofd int fd3 = -1;
+  int info_fd;
+  glnx_autofd int instance_id_fd = instance_id_fd_arg;
   g_autoptr(GKeyFile) keyfile = NULL;
   g_autofree char *runtime_path = NULL;
   const char *group;
@@ -1523,8 +1527,8 @@
      This way even if the bind-mount is unmounted we can find the real data.
    */
 
-  fd = open (info_path, O_RDONLY);
-  if (fd == -1)
+  fd1 = info_fd = open (info_path, O_RDONLY);
+  if (fd1 == -1)
     {
       int errsv = errno;
       g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errsv),
@@ -1535,7 +1539,6 @@
   fd2 = open (info_path, O_RDONLY);
   if (fd2 == -1)
     {
-      close (fd);
       int errsv = errno;
       g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errsv),
                    _("Failed to open flatpak-info file: %s"), g_strerror (errsv));
@@ -1544,9 +1547,9 @@
 
   flatpak_bwrap_add_args (bwrap, "--perms", "0600", NULL);
   flatpak_bwrap_add_args_data_fd (bwrap,
-                                  "--file", fd, "/.flatpak-info");
+                                  "--file", g_steal_fd (&fd1), "/.flatpak-info");
   flatpak_bwrap_add_args_data_fd (bwrap,
-                                  "--ro-bind-data", fd2, "/.flatpak-info");
+                                  "--ro-bind-data", g_steal_fd (&fd2), "/.flatpak-info");
 
   /* Tell the application that it's running under Flatpak in a generic way. */
   flatpak_bwrap_add_args (bwrap,
@@ -1563,8 +1566,6 @@
   fd3 = open (bwrapinfo_path, O_RDWR | O_CREAT, 0644);
   if (fd3 == -1)
     {
-      close (fd);
-      close (fd2);
       int errsv = errno;
       g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errsv),
                    _("Failed to open bwrapinfo.json file: %s"), g_strerror (errsv));
@@ -1587,10 +1588,6 @@
               if (errsv == EINTR)
                 continue;
 
-              close (fd);
-              close (fd2);
-              close (fd3);
-
               g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errsv),
                            _("Failed to write to instance id fd: %s"), g_strerror (errsv));
               return FALSE;
@@ -1600,13 +1597,14 @@
           instance_id_size -= bytes_written;
         }
 
-      close (instance_id_fd);
+      /* explicitly close this as soon as we're done to notify the other side */
+      g_clear_fd (&instance_id_fd, NULL);
     }
 
-  flatpak_bwrap_add_args_data_fd (bwrap, "--info-fd", fd3, NULL);
+  flatpak_bwrap_add_args_data_fd (bwrap, "--info-fd", g_steal_fd (&fd3), NULL);
 
   if (app_info_path_out != NULL)
-    *app_info_path_out = g_strdup_printf ("/proc/self/fd/%d", fd);
+    *app_info_path_out = g_strdup_printf ("/proc/self/fd/%d", info_fd);
 
   if (instance_id_host_dir_out != NULL)
     *instance_id_host_dir_out = g_steal_pointer (&instance_id_host_dir);
@@ -1620,42 +1618,65 @@
   return TRUE;
 }
 
+/*
+ * @runtime_fd: the /usr for the runtime, or -1 if running with no runtime,
+ *  perhaps to unpack extra-data
+ */
 static void
 add_tzdata_args (FlatpakBwrap *bwrap,
-                 GFile *runtime_files)
+                 int           runtime_fd)
 {
-  g_autofree char *raw_timezone = flatpak_get_timezone ();
-  g_autofree char *timezone_content = g_strdup_printf ("%s\n", raw_timezone);
-  g_autofree char *localtime_content = g_strconcat ("../usr/share/zoneinfo/", raw_timezone, NULL);
-  g_autoptr(GFile) runtime_zoneinfo = NULL;
-
-  if (runtime_files)
-    runtime_zoneinfo = g_file_resolve_relative_path (runtime_files, "share/zoneinfo");
+  g_autofree char *raw_timezone = NULL;
+  g_autofree char *timezone_content = NULL;
+  g_autofree char *localtime_content = NULL;
+  const char *tzdir;
+  glnx_autofd int tzdir_fd = -1;
+  glnx_autofd int zoneinfo_fd = -1;
+  g_autoptr(GError) error = NULL;
+
+  g_return_if_fail (runtime_fd >= -1);
+
+  raw_timezone = flatpak_get_timezone ();
+  timezone_content = g_strdup_printf ("%s\n", raw_timezone);
+  localtime_content = g_strconcat ("../usr/share/zoneinfo/", raw_timezone, NULL);
+
+  tzdir = flatpak_get_tzdir ();
+
+  tzdir_fd = glnx_chaseat (AT_FDCWD, tzdir, GLNX_CHASE_MUST_BE_DIRECTORY, NULL);
+
+  if (runtime_fd >= 0)
+    zoneinfo_fd = glnx_chaseat (runtime_fd, "share/zoneinfo",
+                                GLNX_CHASE_RESOLVE_BENEATH |
+                                GLNX_CHASE_MUST_BE_DIRECTORY,
+                                NULL);
 
-  /* Check for runtime /usr/share/zoneinfo */
-  if (runtime_zoneinfo != NULL && g_file_query_exists (runtime_zoneinfo, NULL))
+  /* Check for host /usr/share/zoneinfo */
+  if (tzdir_fd >= 0 && zoneinfo_fd >= 0)
+    {
+      /* Here we assume the host timezone file exist in the host data */
+      flatpak_bwrap_add_args (bwrap,
+                              "--ro-bind", tzdir, "/usr/share/zoneinfo",
+                              "--symlink", localtime_content, "/etc/localtime",
+                              NULL);
+    }
+  else if (runtime_fd >= 0)
     {
-      const char *tzdir = flatpak_get_tzdir ();
+      g_autofree char *runtime_zoneinfo = NULL;
+      glnx_autofd int runtime_zoneinfo_fd = -1;
+
+      runtime_zoneinfo = g_strconcat ("share/zoneinfo/", raw_timezone, NULL);
 
-      /* Check for host /usr/share/zoneinfo */
-      if (g_file_test (tzdir, G_FILE_TEST_IS_DIR))
+      /* Check for runtime /usr/share/zoneinfo */
+      runtime_zoneinfo_fd = glnx_chaseat (runtime_fd, runtime_zoneinfo,
+                                          GLNX_CHASE_RESOLVE_BENEATH |
+                                          GLNX_CHASE_MUST_BE_REGULAR,
+                                          NULL);
+      if (runtime_zoneinfo_fd >= 0)
         {
-          /* Here we assume the host timezone file exist in the host data */
           flatpak_bwrap_add_args (bwrap,
-                                  "--ro-bind", tzdir, "/usr/share/zoneinfo",
                                   "--symlink", localtime_content, "/etc/localtime",
                                   NULL);
         }
-      else
-        {
-          g_autoptr(GFile) runtime_tzfile = g_file_resolve_relative_path (runtime_zoneinfo, raw_timezone);
-
-          /* Check if host timezone file exist in the runtime tzdata */
-          if (g_file_query_exists (runtime_tzfile, NULL))
-            flatpak_bwrap_add_args (bwrap,
-                                    "--symlink", localtime_content, "/etc/localtime",
-                                    NULL);
-        }
     }
 
   flatpak_bwrap_add_args_data (bwrap, "timezone",
@@ -2149,26 +2170,47 @@
 }
 #endif
 
+/*
+ * @runtime_fd: the /usr for the runtime, or -1 if running with no runtime,
+ *  perhaps to unpack extra-data
+ */
 static void
 flatpak_run_setup_usr_links (FlatpakBwrap *bwrap,
-                             GFile        *runtime_files,
+                             int          runtime_fd,
                              const char   *sysroot)
 {
   int i;
 
-  if (runtime_files == NULL)
+  g_return_if_fail (runtime_fd >= -1);
+
+  if (runtime_fd < 0)
     return;
 
   for (i = 0; flatpak_abs_usrmerged_dirs[i] != NULL; i++)
     {
       const char *subdir = flatpak_abs_usrmerged_dirs[i];
-      g_autoptr(GFile) runtime_subdir = NULL;
+      glnx_autofd int runtime_subdir_fd = -1;
+      g_autoptr(GError) local_error = NULL;
 
       g_assert (subdir[0] == '/');
+
       /* Skip the '/' when using as a subdirectory of the runtime */
-      runtime_subdir = g_file_get_child (runtime_files, subdir + 1);
+      runtime_subdir_fd = glnx_chaseat (runtime_fd, subdir + 1,
+                                        GLNX_CHASE_RESOLVE_BENEATH |
+                                        GLNX_CHASE_NOFOLLOW,
+                                        &local_error);
 
-      if (g_file_query_exists (runtime_subdir, NULL))
+      if (runtime_subdir_fd < 0 &&
+          !g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+        {
+          g_warning ("Checking for usrmerged dir %s failed: %s",
+                     subdir, local_error->message);
+        }
+      else if (runtime_subdir_fd < 0)
+        {
+          g_info ("%s does not exist in runtime", subdir);
+        }
+      else
         {
           g_autofree char *link = g_strconcat ("usr", subdir, NULL);
           g_autofree char *create = NULL;
@@ -2182,11 +2224,6 @@
                                   "--symlink", link, create,
                                   NULL);
         }
-      else
-        {
-          g_info ("%s does not exist",
-                  flatpak_file_get_path_cached (runtime_subdir));
-        }
     }
 }
 
@@ -2200,9 +2237,13 @@
   "/sys/devices"
 };
 
+/*
+ * @runtime_fd: the /usr for the runtime, or -1 if running with no runtime,
+ *  perhaps to unpack extra-data
+ */
 gboolean
 flatpak_run_setup_base_argv (FlatpakBwrap   *bwrap,
-                             GFile          *runtime_files,
+                             int             runtime_fd,
                              GFile          *app_id_dir,
                              const char     *arch,
                              FlatpakRunFlags flags,
@@ -2215,12 +2256,13 @@
   struct group *g;
   gulong pers;
   gid_t gid = getgid ();
-  g_autoptr(GFile) etc = NULL;
   gboolean parent_expose_pids = (flags & FLATPAK_RUN_FLAG_PARENT_EXPOSE_PIDS) != 0;
   gboolean parent_share_pids = (flags & FLATPAK_RUN_FLAG_PARENT_SHARE_PIDS) != 0;
   gboolean bwrap_unprivileged = flatpak_bwrap_is_unprivileged ();
   gsize i;
 
+  g_return_val_if_fail (runtime_fd >= -1, FALSE);
+
   /* Disable recursive userns for all flatpak processes, as we need this
    * to guarantee that the sandbox can't restructure the filesystem.
    * Allowing to change e.g. /.flatpak-info would allow sandbox escape
@@ -2328,22 +2370,26 @@
   else if (g_file_test ("/var/lib/dbus/machine-id", G_FILE_TEST_EXISTS))
     flatpak_bwrap_add_args (bwrap, "--ro-bind", "/var/lib/dbus/machine-id", "/etc/machine-id", NULL);
 
-  if (runtime_files)
-    etc = g_file_get_child (runtime_files, "etc");
-  if (etc != NULL &&
-      (flags & FLATPAK_RUN_FLAG_WRITABLE_ETC) == 0 &&
-      g_file_query_exists (etc, NULL))
+  if (runtime_fd >= 0
+      && (flags & FLATPAK_RUN_FLAG_WRITABLE_ETC) == 0)
     {
       g_auto(GLnxDirFdIterator) dfd_iter = { 0, };
       struct dirent *dent;
       gboolean inited;
+      g_autoptr(GError) local_error = NULL;
 
-      inited = glnx_dirfd_iterator_init_at (AT_FDCWD, flatpak_file_get_path_cached (etc), FALSE, &dfd_iter, NULL);
+      inited = glnx_dirfd_iterator_init_at (runtime_fd, "etc", FALSE, &dfd_iter, &local_error);
+      if (!inited && !g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+        {
+          g_propagate_error (error, g_steal_pointer (&local_error));
+          return FALSE;
+        }
 
       while (inited)
         {
-          g_autofree char *src = NULL;
           g_autofree char *dest = NULL;
+          glnx_autofd int src_fd = -1;
+          struct stat statbuf;
 
           if (!glnx_dirfd_iterator_next_dent_ensure_dtype (&dfd_iter, &dent, NULL, NULL) || dent == NULL)
             break;
@@ -2360,9 +2406,19 @@
               strcmp (dent->d_name, "pkcs11") == 0)
             continue;
 
-          src = g_build_filename (flatpak_file_get_path_cached (etc), dent->d_name, NULL);
           dest = g_build_filename ("/etc", dent->d_name, NULL);
-          if (dent->d_type == DT_LNK)
+
+          src_fd = glnx_chaseat (dfd_iter.fd, dent->d_name,
+                                 GLNX_CHASE_NOFOLLOW |
+                                 GLNX_CHASE_RESOLVE_BENEATH,
+                                 error);
+          if (src_fd < 0)
+            return FALSE;
+
+          if (!glnx_fstat (src_fd, &statbuf, error))
+            return FALSE;
+
+          if (S_ISLNK (statbuf.st_mode))
             {
               g_autofree char *target = NULL;
 
@@ -2373,9 +2429,12 @@
 
               flatpak_bwrap_add_args (bwrap, "--symlink", target, dest, NULL);
             }
-          else
+          else if (src_fd >= 0)
             {
-              flatpak_bwrap_add_args (bwrap, "--ro-bind", src, dest, NULL);
+              flatpak_bwrap_add_args_data_fd (bwrap,
+                                              "--ro-bind-fd",
+                                              g_steal_fd (&src_fd),
+                                              dest);
             }
         }
     }
@@ -2396,9 +2455,9 @@
                               NULL);
     }
 
-  flatpak_run_setup_usr_links (bwrap, runtime_files, NULL);
+  flatpak_run_setup_usr_links (bwrap, runtime_fd, NULL);
 
-  add_tzdata_args (bwrap, runtime_files);
+  add_tzdata_args (bwrap, runtime_fd);
 
   pers = PER_LINUX;
 
@@ -2625,7 +2684,7 @@
                      GArray       *base_fd_array,
                      GFile        *app_id_dir,
                      const char   *checksum,
-                     GFile        *runtime_files,
+                     int           runtime_fd,
                      gboolean      generate_ld_so_conf,
                      GCancellable *cancellable,
                      GError      **error)
@@ -2665,7 +2724,7 @@
 
   flatpak_bwrap_append_args (bwrap, base_argv_array);
 
-  flatpak_run_setup_usr_links (bwrap, runtime_files, NULL);
+  flatpak_run_setup_usr_links (bwrap, runtime_fd, NULL);
 
   if (generate_ld_so_conf)
     {
@@ -2883,12 +2942,12 @@
 gboolean
 flatpak_run_app (FlatpakDecomposed   *app_ref,
                  FlatpakDeploy       *app_deploy,
-                 const char          *custom_app_path,
+                 int                  custom_app_fd,
                  FlatpakContext      *extra_context,
                  const char          *custom_runtime,
                  const char          *custom_runtime_version,
                  const char          *custom_runtime_commit,
-                 const char          *custom_usr_path,
+                 int                  custom_runtime_fd,
                  int                  parent_pid,
                  FlatpakRunFlags      flags,
                  const char          *cwd,
@@ -2898,17 +2957,14 @@
                  int                  instance_id_fd,
                  const char * const  *run_environ,
                  char               **instance_dir_out,
+                 GArray              *bind_fds,
+                 GArray              *ro_bind_fds,
                  GCancellable        *cancellable,
                  GError             **error)
 {
   g_autoptr(FlatpakDeploy) runtime_deploy = NULL;
   g_autoptr(GBytes) runtime_deploy_data = NULL;
   g_autoptr(GBytes) app_deploy_data = NULL;
-  g_autoptr(GFile) app_files = NULL;
-  g_autoptr(GFile) original_app_files = NULL;
-  g_autoptr(GFile) runtime_files = NULL;
-  g_autoptr(GFile) original_runtime_files = NULL;
-  g_autoptr(GFile) bin_ldconfig = NULL;
   g_autoptr(GFile) app_id_dir = NULL;
   g_autoptr(GFile) real_app_id_dir = NULL;
   g_autofree char *default_runtime_pref = NULL;
@@ -2942,20 +2998,41 @@
   g_autofree char *per_app_dir_lock_path = NULL;
   g_autofree char *shared_xdg_runtime_dir = NULL;
   int ld_so_fd = -1;
-  g_autoptr(GFile) runtime_ld_so_conf = NULL;
   gboolean generate_ld_so_conf = TRUE;
   gboolean use_ld_so_cache = TRUE;
   gboolean sandboxed = (flags & FLATPAK_RUN_FLAG_SANDBOX) != 0;
   gboolean parent_expose_pids = (flags & FLATPAK_RUN_FLAG_PARENT_EXPOSE_PIDS) != 0;
   gboolean parent_share_pids = (flags & FLATPAK_RUN_FLAG_PARENT_SHARE_PIDS) != 0;
-  const char *app_target_path = "/app";
-  const char *runtime_target_path = "/usr";
-  struct stat s;
+  glnx_autofd int original_runtime_fd = -1;
+  g_autoptr(GFile) original_runtime_files = NULL;
+  g_autoptr(GFile) custom_runtime_files = NULL;
+  /* borrows from either original_runtime_fd or custom_runtime_fd */
+  int runtime_fd = -1;
+  /* borrows from either original_runtime_files or custom_runtime_files */
+  GFile *runtime_files = NULL;
+  const char *original_runtime_target_path = NULL;
+  glnx_autofd int original_app_fd = -1;
+  g_autoptr(GFile) original_app_files = NULL;
+  g_autoptr(GFile) custom_app_files = NULL;
+  /* borrows from either original_app_fd or custom_app_fd */
+  int app_fd = -1;
+  /* borrows from either original_app_files or custom_app_files */
+  GFile *app_files = NULL;
+  const char *original_app_target_path = NULL;
 
   g_assert (run_environ != NULL);
 
   g_return_val_if_fail (app_ref != NULL, FALSE);
 
+  g_return_val_if_fail (custom_app_fd == FLATPAK_RUN_APP_DEPLOY_APP_ORIGINAL ||
+                        custom_app_fd == FLATPAK_RUN_APP_DEPLOY_APP_EMPTY ||
+                        custom_app_fd >= 0,
+                        FALSE);
+
+  g_return_val_if_fail (custom_runtime_fd == FLATPAK_RUN_APP_DEPLOY_USR_ORIGINAL ||
+                        custom_runtime_fd >= 0,
+                        FALSE);
+
   /* This check exists to stop accidental usage of `sudo flatpak run`
      and is not to prevent running as root.
    */
@@ -3081,38 +3158,53 @@
 
   flatpak_context_dump (app_context, "Final context");
   original_runtime_files = flatpak_deploy_get_files (runtime_deploy);
+  original_runtime_fd = open (flatpak_file_get_path_cached (original_runtime_files),
+                              O_PATH | O_CLOEXEC);
+  if (original_runtime_fd < 0)
+    return glnx_throw_errno_prefix (error, "Failed to open original runtime");
 
-  if (custom_usr_path != NULL)
+  if (custom_runtime_fd >= 0)
     {
-      runtime_files = g_file_new_for_path (custom_usr_path);
-      /* Mount the original runtime below here instead of /usr */
-      runtime_target_path = "/run/parent/usr";
+      g_autofree char *path = NULL;
+
+      path = flatpak_get_path_for_fd (custom_runtime_fd, &my_error);
+      if (path == NULL)
+        {
+          return flatpak_fail_error (error, FLATPAK_ERROR,
+                                     "Cannot convert custom usr fd to path: %s",
+                                     my_error->message);
+        }
+
+      custom_runtime_files = g_file_new_for_path (path);
+
+      original_runtime_target_path = "/run/parent/usr";
+      runtime_fd = custom_runtime_fd;
+      runtime_files = custom_runtime_files;
+    }
+  else if (custom_runtime_fd == FLATPAK_RUN_APP_DEPLOY_USR_ORIGINAL)
+    {
+      original_runtime_target_path = "/usr";
+      runtime_fd = original_runtime_fd;
+      runtime_files = original_runtime_files;
     }
   else
     {
-      runtime_files = g_object_ref (original_runtime_files);
+      g_assert_not_reached ();
     }
 
-  bin_ldconfig = g_file_resolve_relative_path (runtime_files, "bin/ldconfig");
-  if (!g_file_query_exists (bin_ldconfig, NULL))
-    use_ld_so_cache = FALSE;
-
-  /* We can't use the ld.so cache if we are using a custom /usr or /app,
-   * because we don't have a unique ID for the /usr or /app, so we can't
-   * do cache-invalidation correctly. The caller can either build their
-   * own ld.so.cache before supplying us with the runtime, or supply
-   * their own LD_LIBRARY_PATH. */
-  if (custom_usr_path != NULL || custom_app_path != NULL)
-    use_ld_so_cache = FALSE;
-
   if (app_deploy != NULL)
     {
       g_autofree const char **previous_ids = NULL;
       gsize len = 0;
       gboolean do_migrate;
 
-      real_app_id_dir = flatpak_get_data_dir (app_id);
       original_app_files = flatpak_deploy_get_files (app_deploy);
+      original_app_fd = open (flatpak_file_get_path_cached (original_app_files),
+                              O_PATH | O_CLOEXEC | O_NOFOLLOW);
+      if (original_app_fd < 0)
+        return glnx_throw_errno_prefix (error, "Failed to open original runtime");
+
+      real_app_id_dir = flatpak_get_data_dir (app_id);
 
       previous_app_id_dirs = g_ptr_array_new_with_free_func (g_object_unref);
       previous_ids = flatpak_deploy_data_get_previous_ids (app_deploy_data, &len);
@@ -3199,19 +3291,61 @@
         app_id_dir = g_object_ref (real_app_id_dir);
     }
 
-  if (custom_app_path != NULL)
+  if (custom_app_fd >= 0)
     {
-      if (strcmp (custom_app_path, "") == 0)
-        app_files = NULL;
-      else
-        app_files = g_file_new_for_path (custom_app_path);
+      g_autofree char *path = NULL;
+
+      path = flatpak_get_path_for_fd (custom_app_fd, error);
+      if (path == NULL)
+        return glnx_prefix_error (error, "Cannot convert custom app fd to path");
 
-      /* Mount the original app below here */
-      app_target_path = "/run/parent/app";
+      custom_app_files = g_file_new_for_path (path);
+
+      original_app_target_path = "/run/parent/app";
+      app_fd = custom_app_fd;
+      app_files = custom_app_files;
+    }
+  else if (custom_app_fd == FLATPAK_RUN_APP_DEPLOY_APP_ORIGINAL)
+    {
+      original_app_target_path = "/app";
+      app_fd = original_app_fd;
+      app_files = original_app_files;
+    }
+  else if (custom_app_fd == FLATPAK_RUN_APP_DEPLOY_APP_EMPTY)
+    {
+      original_app_target_path = "/run/parent/app";
+      app_fd = -1;
+      app_files = NULL;
     }
-  else if (original_app_files != NULL)
+  else
     {
-      app_files = g_object_ref (original_app_files);
+      g_assert_not_reached ();
+    }
+
+  /* We can't use the ld.so cache if we are using a custom /usr or /app,
+   * because we don't have a unique ID for the /usr or /app, so we can't
+   * do cache-invalidation correctly. The caller can either build their
+   * own ld.so.cache before supplying us with the runtime, or supply
+   * their own LD_LIBRARY_PATH. */
+  if (runtime_fd == custom_runtime_fd || app_fd == custom_app_fd)
+    {
+      use_ld_so_cache = FALSE;
+    }
+  else
+    {
+      glnx_autofd int ldconfig_fd = -1;
+
+      ldconfig_fd = glnx_chaseat (runtime_fd, "bin/ldconfig",
+                                  GLNX_CHASE_RESOLVE_BENEATH |
+                                  GLNX_CHASE_MUST_BE_REGULAR,
+                                  &my_error);
+      if (ldconfig_fd < 0)
+        {
+          use_ld_so_cache = FALSE;
+          g_debug ("bin/ldconfig not found in runtime: %s", my_error->message);
+        }
+
+      g_clear_error (&my_error);
     }
 
   flatpak_run_apply_env_default (bwrap, use_ld_so_cache);
@@ -3224,75 +3358,86 @@
       flatpak_bwrap_set_env (bwrap, "FLATPAK_SANDBOX_DIR", flatpak_file_get_path_cached (sandbox_dir), TRUE);
     }
 
-  flatpak_bwrap_add_args (bwrap,
-                          "--ro-bind", flatpak_file_get_path_cached (runtime_files), "/usr",
-                          NULL);
-
-  if (runtime_files == original_runtime_files)
-    {
-      /* All true Flatpak runtimes have files/.ref */
-      flatpak_bwrap_add_args (bwrap,
-                              "--lock-file", "/usr/.ref",
-                              NULL);
-    }
-  else
-    {
-      g_autoptr(GFile) runtime_child = NULL;
+  if (!flatpak_bwrap_add_args_data_fd_dup (bwrap,
+                                           "--ro-bind-fd", runtime_fd, "/usr",
+                                           error))
+    return FALSE;
 
-      runtime_child = g_file_get_child (runtime_files, ".ref");
+  {
+    glnx_autofd int runtime_ref_fd = -1;
 
-      /* Lock ${usr}/.ref if it exists */
-      if (g_file_query_exists (runtime_child, NULL))
+    runtime_ref_fd = glnx_chaseat (runtime_fd, ".ref",
+                                   GLNX_CHASE_RESOLVE_BENEATH |
+                                   GLNX_CHASE_MUST_BE_REGULAR,
+                                   NULL);
+    if (runtime_ref_fd >= 0)
+      {
         flatpak_bwrap_add_args (bwrap,
                                 "--lock-file", "/usr/.ref",
                                 NULL);
+      }
+  }
+
+  if (runtime_fd == custom_runtime_fd)
+    {
+      glnx_autofd int original_runtime_ref_fd = -1;
+      glnx_autofd int original_runtime_etc_fd = -1;
 
       /* Put the real Flatpak runtime in /run/parent, so that the
        * replacement /usr can have symlinks into /run/parent in order
        * to use the Flatpak runtime's graphics drivers etc. if desired */
-      flatpak_bwrap_add_args (bwrap,
-                              "--ro-bind",
-                              flatpak_file_get_path_cached (original_runtime_files),
-                              "/run/parent/usr",
-                              "--lock-file", "/run/parent/usr/.ref",
-                              NULL);
-      flatpak_run_setup_usr_links (bwrap, original_runtime_files,
-                                   "/run/parent");
+      if (!flatpak_bwrap_add_args_data_fd_dup (bwrap,
+                                               "--ro-bind-fd",
+                                               original_runtime_fd,
+                                               "/run/parent/usr",
+                                               error))
+        return FALSE;
+
+      original_runtime_ref_fd = glnx_chaseat (original_runtime_fd, ".ref",
+                                              GLNX_CHASE_RESOLVE_BENEATH |
+                                              GLNX_CHASE_MUST_BE_REGULAR,
+                                              NULL);
+      if (original_runtime_ref_fd >= 0)
+        {
+          flatpak_bwrap_add_args (bwrap,
+                                  "--lock-file", "/run/parent/usr/.ref",
+                                  NULL);
+        }
 
-      g_clear_object (&runtime_child);
-      runtime_child = g_file_get_child (original_runtime_files, "etc");
+      original_runtime_etc_fd = glnx_chaseat (original_runtime_fd, "etc",
+                                              GLNX_CHASE_RESOLVE_BENEATH |
+                                              GLNX_CHASE_MUST_BE_REGULAR,
+                                              NULL);
+      if (original_runtime_etc_fd >= 0)
+        {
+          flatpak_bwrap_add_args (bwrap,
+                                  "--symlink", "usr/etc", "/run/parent/etc",
+                                  NULL);
+        }
 
-      if (g_file_query_exists (runtime_child, NULL))
-        flatpak_bwrap_add_args (bwrap,
-                                "--symlink", "usr/etc", "/run/parent/etc",
-                                NULL);
+      flatpak_run_setup_usr_links (bwrap, original_runtime_fd,
+                                   "/run/parent");
     }
 
-  if (app_files != NULL)
+  if (app_fd >= 0)
     {
-      flatpak_bwrap_add_args (bwrap,
-                              "--ro-bind", flatpak_file_get_path_cached (app_files), "/app",
-                              NULL);
+      glnx_autofd int app_ref_fd = -1;
+
+      if (!flatpak_bwrap_add_args_data_fd_dup (bwrap,
+                                               "--ro-bind-fd", app_fd, "/app",
+                                               error))
+        return FALSE;
 
-      if (app_files == original_app_files)
+      app_ref_fd = glnx_chaseat (app_fd, ".ref",
+                                 GLNX_CHASE_RESOLVE_BENEATH |
+                                 GLNX_CHASE_MUST_BE_REGULAR,
+                                 NULL);
+      if (app_ref_fd >= 0)
         {
-          /* All true Flatpak apps have files/.ref */
           flatpak_bwrap_add_args (bwrap,
                                   "--lock-file", "/app/.ref",
                                   NULL);
         }
-      else
-        {
-          g_autoptr(GFile) app_child = NULL;
-
-          app_child = g_file_get_child (app_files, ".ref");
-
-          /* Lock ${app}/.ref if it exists */
-          if (g_file_query_exists (app_child, NULL))
-            flatpak_bwrap_add_args (bwrap,
-                                    "--lock-file", "/app/.ref",
-                                    NULL);
-        }
     }
   else
     {
@@ -3301,7 +3446,7 @@
                               NULL);
     }
 
-  if (original_app_files != NULL && app_files != original_app_files)
+  if (original_app_fd >= 0 && original_app_fd != app_fd)
     {
       /* Put the real Flatpak app in /run/parent/app */
       flatpak_bwrap_add_args (bwrap,
@@ -3314,26 +3459,37 @@
 
   if (metakey != NULL &&
       !flatpak_run_add_extension_args (bwrap, metakey, app_ref,
-                                       use_ld_so_cache, app_target_path,
+                                       use_ld_so_cache, original_app_target_path,
                                        &app_extensions, &app_ld_path,
                                        cancellable, error))
     return FALSE;
 
   if (!flatpak_run_add_extension_args (bwrap, runtime_metakey, runtime_ref,
-                                       use_ld_so_cache, runtime_target_path,
+                                       use_ld_so_cache, original_runtime_target_path,
                                        &runtime_extensions, &runtime_ld_path,
                                        cancellable, error))
     return FALSE;
 
-  if (custom_usr_path == NULL)
+  if (runtime_fd == original_runtime_fd)
     flatpak_run_extend_ld_path (bwrap, NULL, runtime_ld_path);
 
-  if (custom_app_path == NULL)
+  if (app_fd == original_app_fd)
     flatpak_run_extend_ld_path (bwrap, app_ld_path, NULL);
 
-  runtime_ld_so_conf = g_file_resolve_relative_path (runtime_files, "etc/ld.so.conf");
-  if (lstat (flatpak_file_get_path_cached (runtime_ld_so_conf), &s) == 0)
-    generate_ld_so_conf = S_ISREG (s.st_mode) && s.st_size == 0;
+  {
+    glnx_autofd int ld_so_conf_fd = -1;
+    struct glnx_statx stx;
+
+    ld_so_conf_fd = glnx_chase_and_statxat (runtime_fd, "etc/ld.so.conf",
+                                            GLNX_CHASE_RESOLVE_BENEATH |
+                                            GLNX_CHASE_MUST_BE_REGULAR,
+                                            GLNX_STATX_SIZE,
+                                            &stx, NULL);
+    if (ld_so_conf_fd < 0 ||
+        !(stx.stx_mask & GLNX_STATX_SIZE) ||
+        stx.stx_size != 0)
+      generate_ld_so_conf = FALSE;
+  }
 
   /* At this point we have the minimal argv set up, with just the app, runtime and extensions.
      We can reuse this to generate the ld.so.cache (if needed) */
@@ -3345,7 +3501,7 @@
                                       bwrap->fds,
                                       app_id_dir,
                                       checksum,
-                                      runtime_files,
+                                      runtime_fd,
                                       generate_ld_so_conf,
                                       cancellable, error);
       if (ld_so_fd == -1)
@@ -3355,7 +3511,7 @@
 
   flags |= flatpak_context_get_run_flags (app_context);
 
-  if (!flatpak_run_setup_base_argv (bwrap, runtime_files, app_id_dir, app_arch, flags, error))
+  if (!flatpak_run_setup_base_argv (bwrap, runtime_fd, app_id_dir, app_arch, flags, error))
     return FALSE;
 
   if (generate_ld_so_conf)
@@ -3378,7 +3534,8 @@
                                       app_id, flatpak_decomposed_get_branch (app_ref),
                                       runtime_ref, app_id_dir, app_context, extra_context,
                                       sandboxed, FALSE, flags & FLATPAK_RUN_FLAG_DEVEL,
-                                      &app_info_path, instance_id_fd,
+                                      &app_info_path,
+                                      g_steal_fd (&instance_id_fd),
                                       &instance_id_host_dir, &instance_id_host_private_dir,
                                       &instance_id, error))
     return FALSE;
@@ -3440,6 +3597,40 @@
                           "--symlink", "/usr/lib/debug/source", "/run/build-runtime",
                           NULL);
 
+  for (i = 0; bind_fds && i < bind_fds->len; i++)
+    {
+      int fd = g_array_index (bind_fds, int, i);
+      g_autofree char *path = NULL;
+
+      /* We get the path the fd refers to, to determine to mount point
+       * destination inside the sandbox */
+      path = flatpak_get_path_for_fd (fd, error);
+      if (!path)
+        return FALSE;
+
+      if (!flatpak_bwrap_add_args_data_fd_dup (bwrap,
+                                               "--bind-fd", fd, path,
+                                               error))
+        return FALSE;
+    }
+
+  for (i = 0; ro_bind_fds && i < ro_bind_fds->len; i++)
+    {
+      int fd = g_array_index (ro_bind_fds, int, i);
+      g_autofree char *path = NULL;
+
+      /* We get the path the fd refers to, to determine to mount point
+       * destination inside the sandbox */
+      path = flatpak_get_path_for_fd (fd, error);
+      if (!path)
+        return FALSE;
+
+      if (!flatpak_bwrap_add_args_data_fd_dup (bwrap,
+                                               "--ro-bind-fd", fd, path,
+                                               error))
+        return FALSE;
+    }
+
   if (cwd)
     flatpak_bwrap_add_args (bwrap, "--chdir", cwd, NULL);
 
diff -Nru flatpak-1.16.3/common/flatpak-run-private.h flatpak-1.16.6/common/flatpak-run-private.h
--- flatpak-1.16.3/common/flatpak-run-private.h	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/common/flatpak-run-private.h	2026-04-10 18:28:42.000000000 +0100
@@ -30,6 +30,11 @@
 #include "flatpak-utils-private.h"
 #include "flatpak-exports-private.h"
 
+#define FLATPAK_RUN_APP_DEPLOY_APP_ORIGINAL (-2)
+#define FLATPAK_RUN_APP_DEPLOY_APP_EMPTY (-3)
+
+#define FLATPAK_RUN_APP_DEPLOY_USR_ORIGINAL (-2)
+
 gboolean flatpak_run_in_transient_unit (const char *app_id,
                                         const char *instance_id,
                                         GError    **error);
@@ -72,7 +77,7 @@
                                   GError      **error);
 
 gboolean flatpak_run_setup_base_argv (FlatpakBwrap   *bwrap,
-                                      GFile          *runtime_files,
+                                      int             runtime_fd,
                                       GFile          *app_id_dir,
                                       const char     *arch,
                                       FlatpakRunFlags flags,
@@ -104,12 +109,12 @@
 
 gboolean flatpak_run_app (FlatpakDecomposed   *app_ref,
                           FlatpakDeploy       *app_deploy,
-                          const char          *custom_app_path,
+                          int                  custom_app_fd,
                           FlatpakContext      *extra_context,
                           const char          *custom_runtime,
                           const char          *custom_runtime_version,
                           const char          *custom_runtime_commit,
-                          const char          *custom_usr_path,
+                          int                  custom_usr_fd,
                           int                  parent_pid,
                           FlatpakRunFlags      flags,
                           const char          *cwd,
@@ -119,6 +124,8 @@
                           int                  instance_id_fd,
                           const char * const  *run_environ,
                           char               **instance_dir_out,
+                          GArray              *bind_fds,
+                          GArray              *ro_bind_fds,
                           GCancellable        *cancellable,
                           GError             **error);
 
diff -Nru flatpak-1.16.3/common/flatpak-utils.c flatpak-1.16.6/common/flatpak-utils.c
--- flatpak-1.16.3/common/flatpak-utils.c	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/common/flatpak-utils.c	2026-04-10 18:28:42.000000000 +0100
@@ -922,6 +922,22 @@
   return ret;
 }
 
+static gboolean
+flatpak_str_is_alphanumeric (const char *arg)
+{
+  while (*arg != '\0')
+    {
+      char c = *arg;
+
+      if (!g_ascii_isalnum (c))
+        return FALSE;
+
+      arg++;
+    }
+
+  return TRUE;
+}
+
 /* This atomically replaces a symlink with a new value, removing the
  * existing symlink target, if it exstis and is different from
  * @target. This is atomic in the sense that we're guaranteed to
@@ -931,6 +947,9 @@
  * symlink for some reason, ending up with neither the old or the new
  * target. That is fine if the reason for the symlink is keeping a
  * cache though.
+ * The target shall only be a file in the same directory as the symlink, and
+ * shall only contain the characters a-zA-Z0-9. This is so that the target of
+ * the symlink that gets removed is in the same directory as the link.
  */
 gboolean
 flatpak_switch_symlink_and_remove (const char *symlink_path,
@@ -974,10 +993,21 @@
           g_autofree char *old_target = flatpak_readlink (tmp_path, error);
           if (old_target == NULL)
             return FALSE;
-          if (strcmp (old_target, target) != 0) /* Don't remove old file if its the same as the new one */
+
+          /* Don't remove old file if its the same as the new one */
+          if (strcmp (old_target, target) != 0)
             {
-              g_autofree char *old_target_path = g_build_filename (symlink_dir, old_target, NULL);
-              unlink (old_target_path);
+              if (flatpak_str_is_alphanumeric (old_target))
+                {
+                  g_autofree char *old_target_path = NULL;
+
+                  old_target_path = g_build_filename (symlink_dir, old_target, NULL);
+                  unlink (old_target_path);
+                }
+              else
+                {
+                  g_warning ("Refusing to delete old link target %s", old_target);
+                }
             }
         }
       else if (errno != ENOENT)
@@ -2473,3 +2503,139 @@
 
   return is_debugging;
 }
+
+int
+flatpak_parse_fd (const char  *fd_string,
+                  GError     **error)
+{
+  guint64 parsed;
+  char *endptr;
+  int fd;
+  struct stat stbuf;
+
+  parsed = g_ascii_strtoull (fd_string, &endptr, 10);
+
+  if (endptr == NULL || *endptr != '\0' || parsed > G_MAXINT)
+    return glnx_fd_throw (error, "Not a valid file descriptor: %s", fd_string);
+
+  fd = (int) parsed;
+
+  if (!glnx_fstat (fd, &stbuf, NULL))
+    return glnx_fd_throw (error, "Not an open file descriptor: %d", fd);
+
+  return fd;
+}
+
+/* Sets errno on failure. */
+gboolean
+flatpak_set_cloexec (int fd)
+{
+  int flags = fcntl (fd, F_GETFD);
+
+  if (flags == -1)
+    return FALSE;
+
+  flags |= FD_CLOEXEC;
+
+  if (fcntl (fd, F_SETFD, flags) < 0)
+    return FALSE;
+
+  return TRUE;
+}
+
+/*
+ * flatpak_accept_fd_argument:
+ * @option_name: Name of a command-line option such as `--env-fd`
+ * @value: Value of the command-line option
+ *
+ * Parse a command-line argument whose value is a file descriptor to be
+ * used internally by Flatpak.
+ *
+ * The file descriptor must be 3 or higher (cannot be stdin, stdout
+ * or stderr).
+ *
+ * The file descriptor is set to be close-on-execute (CLOEXEC).
+ * If child processes are meant to inherit it, the caller must clear the
+ * close-on-execute flag, or duplicate the fd.
+ *
+ * Returns: A file descriptor to be closed by the caller, or -1 on error
+ */
+int
+flatpak_accept_fd_argument (const char  *option_name,
+                            const char  *value,
+                            GError     **error)
+{
+  glnx_autofd int fd = -1;
+
+  fd = flatpak_parse_fd (value, error);
+
+  if (fd < 0)
+    {
+      g_prefix_error (error, "%s: ", option_name);
+      return -1;
+    }
+
+  if (fd < 3)
+    {
+      /* We don't want to close stdin, stdout or stderr */
+      fd = -1;
+      return glnx_fd_throw (error,
+                            "%s: Cannot use reserved file descriptor 0, 1 or 2",
+                            option_name);
+    }
+
+  if (!flatpak_set_cloexec (fd))
+    return glnx_fd_throw_errno_prefix (error, "%s", option_name);
+
+  return g_steal_fd (&fd);
+}
+
+/*
+ * Attempt to discover the filesystem path corresponding to @fd.
+ *
+ * If @fd points to an existing file, return the absolute path of that
+ * file in the environment where it was opened. Note that this is not
+ * necessarily a valid path in the current namespace, if it was
+ * transferred via fd-passing from a process in a different filesystem
+ * namespace.
+ *
+ * If @fd points to a deleted file, or to a socket, fifo, memfd or similar
+ * non-filesystem object, set an error and return %NULL.
+ *
+ * Returns: (type filename) (transfer full) (nullable):
+ */
+char *
+flatpak_get_path_for_fd (int        fd,
+                         GError   **error)
+{
+  g_autofree char *proc_path = NULL;
+  g_autofree char *path = NULL;
+
+  proc_path = g_strdup_printf ("/proc/self/fd/%d", fd);
+  path = glnx_readlinkat_malloc (AT_FDCWD, proc_path, NULL, error);
+  if (path == NULL)
+    return NULL;
+
+  /* All normal paths start with /, but some weird things
+     don't, such as socket:[27345] or anon_inode:[eventfd].
+     We don't support any of these */
+  if (path[0] != '/')
+    {
+      return glnx_null_throw (error, "%s resolves to non-absolute path %s",
+                              proc_path, path);
+    }
+
+  /* File descriptors to actually deleted files have " (deleted)"
+     appended to them. This also happens to some fake fd types
+     like shmem which are "/<name> (deleted)". All such
+     files are considered invalid. Unfortunately this also
+     matches files with filenames that actually end in " (deleted)",
+     but there is not much to do about this. */
+  if (g_str_has_suffix (path, " (deleted)"))
+    {
+      return glnx_null_throw (error, "%s resolves to deleted path %s",
+                              proc_path, path);
+    }
+
+  return g_steal_pointer (&path);
+}
diff -Nru flatpak-1.16.3/common/flatpak-utils-private.h flatpak-1.16.6/common/flatpak-utils-private.h
--- flatpak-1.16.3/common/flatpak-utils-private.h	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/common/flatpak-utils-private.h	2026-04-10 18:28:42.000000000 +0100
@@ -348,6 +348,18 @@
 void flatpak_set_debugging (gboolean debugging);
 gboolean flatpak_is_debugging (void);
 
+int flatpak_parse_fd (const char  *fd_string,
+                      GError     **error);
+
+char * flatpak_get_path_for_fd (int      fd,
+                                GError **error);
+
 #define FLATPAK_MESSAGE_ID "c7b39b1e006b464599465e105b361485"
 
+gboolean flatpak_set_cloexec (int fd);
+
+int flatpak_accept_fd_argument (const char  *option_name,
+                                const char  *value,
+                                GError     **error);
+
 #endif /* __FLATPAK_UTILS_H__ */
diff -Nru flatpak-1.16.3/debian/changelog flatpak-1.16.6/debian/changelog
--- flatpak-1.16.3/debian/changelog	2026-01-24 21:55:30.000000000 +0000
+++ flatpak-1.16.6/debian/changelog	2026-04-10 20:03:53.000000000 +0100
@@ -1,3 +1,94 @@
+flatpak (1.16.6-1~deb13u1) trixie-security; urgency=high
+
+  * Backport new upstream stable release for Debian 13
+    - Fix a sandbox escape involving symlinks passed to flatpak-portal.
+      A malicious or compromised Flatpak app could exploit this to achieve
+      arbitrary code execution on the host.
+      (CVE-2026-34078, GHSA-cc2q-qc34-jprg) (Closes: #1132943)
+    - Prevent arbitrary file deletion outside the sandbox by a malicious or
+      compromised Flatpak app
+      (CVE-2026-34079, GHSA-p29x-r292-46pp) (Closes: #1132944)
+    - Prevent a local user from reading any file that is readable by the
+      _flatpak system user. A mitigation is that it would be very unusual
+      for these files not to be readable by the original local user as well.
+      (No CVE ID, GHSA-2fxp-43j9-pwvc) (Closes: #1132946)
+    - Prevent a local user from making another local user unable to cancel
+      an ongoing download of apps or runtimes installed system-wide
+      via the system helper.
+      (No CVE ID, GHSA-89xm-3m96-w3jg) (Closes: #1132945)
+    - Various fixes for regressions caused when fixing CVE-2026-34078
+  * Revert changes that are not appropriate for a stable update:
+    - Revert "d/watch: Convert to v5 format, only watch stable
+      (even-numbered) releases"
+    - Revert "Standards-Version: 4.7.3"
+
+ -- Simon McVittie <smcv@debian.org>  Fri, 10 Apr 2026 20:03:53 +0100
+
+flatpak (1.16.6-1) unstable; urgency=high
+
+  * New upstream stable release
+    - Incorporates the patches from the previous upload, plus
+      additional bugfixes
+  * Drop patches that were included in the new upstream release
+
+ -- Simon McVittie <smcv@debian.org>  Fri, 10 Apr 2026 19:06:41 +0100
+
+flatpak (1.16.5-1) unstable; urgency=high
+
+  * New upstream release fixing some of the regressions in 1.16.4
+    (Closes: #1132960, #1132968)
+  * Drop patches that were applied upstream
+  * d/p/utils-Add-flatpak_set_cloexec.patch,
+    d/p/run-context-Mark-fd-arguments-as-close-on-exec.patch:
+    Add proposed patches to fix another regression in 1.16.4 for
+    Chromium-based browsers
+    (Closes: #1132968)
+  * d/p/run-Cope-with-an-empty-runtime.patch,
+    d/p/dir-In-apply_extra_data-don-t-assume-there-is-always-a-ru.patch:
+    Add proposed patches to fix regression in 1.16.4 when installing
+    apps/runtimes/extensions that use a statically-linked extra-data helper
+  * d/p/utils-Move-flatpak_get_path_for_fd-to-here.patch,
+    d/p/portal-Avoid-crash-if-sandbox-expose-ro-fd-is-out-of-rang.patch,
+    d/p/portal-Log-and-ignore-unusable-sandbox-expose-fds-instead.patch,
+    d/p/portal-Reinstate-flatpak_get_path_for_fd-checks.patch:
+    Add proposed patches to fix regression in 1.16.4 for Epiphany and
+    possibly other WebKitGTK-based apps
+  * d/p/libtest-Allow-adding-a-new-ref-to-an-existing-temporary-o.patch:
+    Add proposed patch to fix autopkgtest failure
+
+ -- Simon McVittie <smcv@debian.org>  Fri, 10 Apr 2026 12:47:08 +0100
+
+flatpak (1.16.4-2) unstable; urgency=medium
+
+  * d/p/run-Fix-checking-wrong-variable-in-runtime-fd-selection.patch,
+    d/p/run-Mount-original-app-on-run-parent-app-when-using-app-p.patch:
+    Add proposed patches fixing regression for unofficial Steam Flatpak app
+    (Closes: #1132960)
+  * Mention Debian bug numbers in previous changelog entry
+
+ -- Simon McVittie <smcv@debian.org>  Wed, 08 Apr 2026 10:55:53 +0100
+
+flatpak (1.16.4-1) unstable; urgency=high
+
+  * New upstream security release
+    - Fix a sandbox escape involving symlinks passed to flatpak-portal.
+      A malicious or compromised Flatpak app could exploit this to achieve
+      arbitrary code execution on the host.
+      (CVE-2026-34078, GHSA-cc2q-qc34-jprg) (Closes: #1132943)
+    - Prevent arbitrary file deletion outside the sandbox by a malicious or
+      compromised Flatpak app
+      (CVE-2026-34079, GHSA-p29x-r292-46pp) (Closes: #1132944)
+    - Prevent a local user from reading any file that is readable by the
+      _flatpak system user. A mitigation is that it would be very unusual
+      for these files not to be readable by the original local user as well.
+      (No CVE ID, GHSA-2fxp-43j9-pwvc) (Closes: #1132946)
+    - Prevent a local user from making another local user unable to cancel
+      an ongoing download of apps or runtimes installed system-wide
+      via the system helper.
+      (No CVE ID, GHSA-89xm-3m96-w3jg) (Closes: #1132945)
+
+ -- Simon McVittie <smcv@debian.org>  Tue, 07 Apr 2026 22:14:56 +0100
+
 flatpak (1.16.3-1~deb13u1) trixie; urgency=medium
 
   * Backport new upstream stable release for Debian 13
diff -Nru flatpak-1.16.3/debian/control flatpak-1.16.6/debian/control
--- flatpak-1.16.3/debian/control	2026-01-24 21:55:30.000000000 +0000
+++ flatpak-1.16.6/debian/control	2026-04-10 20:03:53.000000000 +0100
@@ -11,6 +11,7 @@
  bison,
  bubblewrap (>= 0.10.0~),
  ca-certificates <!nocheck>,
+ curl <!nocheck>,
  dbus-daemon,
  debhelper (>= 13.11.6~),
  debhelper-compat (= 13),
@@ -128,6 +129,7 @@
 Depends:
  attr,
  ca-certificates,
+ curl,
  dbus-daemon,
  desktop-file-utils,
  flatpak (= ${binary:Version}),
diff -Nru flatpak-1.16.3/meson.build flatpak-1.16.6/meson.build
--- flatpak-1.16.3/meson.build	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/meson.build	2026-04-10 18:28:42.000000000 +0100
@@ -4,7 +4,7 @@
 project(
   'flatpak',
   'c',
-  version : '1.16.3',
+  version : '1.16.6',
   default_options: [
     'warning_level=2',
   ],
@@ -13,7 +13,7 @@
 
 flatpak_major_version = 1
 flatpak_minor_version = 16
-flatpak_micro_version = 3
+flatpak_micro_version = 6
 flatpak_extra_version = ''
 
 flatpak_interface_age = 0
diff -Nru flatpak-1.16.3/NEWS flatpak-1.16.6/NEWS
--- flatpak-1.16.3/NEWS	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/NEWS	2026-04-10 18:28:42.000000000 +0100
@@ -1,3 +1,53 @@
+Changes in 1.16.6
+~~~~~~~~~~~~~~~~~~
+Released: 2026-04-10
+
+Bug fixes:
+
+* Fix the remaining regression for Chromium based browsers by not leaking file
+  descriptors down to wrapped command (#6589, #6594)
+
+* Fix a regression when installing extra-data without a runtime, which is the
+  case for openh264 (#6587)
+
+* Fix the remaining regression for Epiphany by ignoring unusable sandbox-expose
+  paths for sub-sandboxes in the portal (#6588)
+
+* Fix the installed tests by allowing to add a new ref to an existing temporary
+  ostree repo (#6592)
+
+* Avoid closing fds 0/1/2 when they are used as a bad argument to flatpak-run,
+  and reduce duplication in handling file descriptor arguments (#6598)
+
+Changes in 1.16.5
+~~~~~~~~~~~~~~~~~~
+Released: 2026-04-09
+
+Bug fixes:
+
+* Fix regressions caused by the sandbox escape security fix, which impact some
+  browsers, browser-based apps and Steam (#6577, #6569, #6576, #6574)
+
+Enhancements:
+
+* Expand test coverage of flatpak-run features used by flatpak-portal (#6573)
+
+Changes in 1.16.4
+~~~~~~~~~~~~~~~~~~
+Released: 2026-04-07
+
+Security fixes:
+
+* Fix a complete sandbox escape which leads to host file access and code
+  execution in the host context (CVE-2026-34078)
+
+* Prevent arbitrary file deletion on the host filesystem (CVE-2026-34079)
+
+* Prevent arbitrary read-access to files in the system-helper context
+  (GHSA-2fxp-43j9-pwvc)
+
+* Prevent orphaning cross-user pull operations (GHSA-89xm-3m96-w3jg)
+
 Changes in 1.16.3
 ~~~~~~~~~~~~~~~~~~
 Released: 2026-01-21
diff -Nru flatpak-1.16.3/portal/flatpak-portal.c flatpak-1.16.6/portal/flatpak-portal.c
--- flatpak-1.16.3/portal/flatpak-portal.c	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/portal/flatpak-portal.c	2026-04-10 18:28:42.000000000 +0100
@@ -552,195 +552,80 @@
 }
 
 static gboolean
-is_valid_expose (const char *expose,
-                 GError    **error)
+validate_opath_fd (int        fd,
+                   gboolean   needs_writable,
+                   GError   **error)
 {
-  /* No subdirs or absolute paths */
-  if (expose[0] == '/')
-    {
-      g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
-                   "Invalid sandbox expose: absolute paths not allowed");
-      return FALSE;
-    }
-  else if (strchr (expose, '/'))
-    {
-      g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
-                   "Invalid sandbox expose: subdirectories not allowed");
-      return FALSE;
-    }
-
-  return TRUE;
-}
-
-static char *
-filesystem_arg (const char *path,
-                gboolean    readonly)
-{
-  g_autoptr(GString) s = g_string_new ("--filesystem=");
-  const char *p;
-
-  for (p = path; *p != 0; p++)
-    {
-      if (*p == ':')
-        g_string_append (s, "\\:");
-      else
-        g_string_append_c (s, *p);
-    }
-
-  if (readonly)
-    g_string_append (s, ":ro");
-
-  return g_string_free (g_steal_pointer (&s), FALSE);
-}
-
-
-static char *
-filesystem_sandbox_arg (const char *path,
-                        const char *sandbox,
-                        gboolean    readonly)
-{
-  g_autoptr(GString) s = g_string_new ("--filesystem=");
-  const char *p;
-
-  for (p = path; *p != 0; p++)
-    {
-      if (*p == ':')
-        g_string_append (s, "\\:");
-      else
-        g_string_append_c (s, *p);
-    }
-
-  g_string_append (s, "/sandbox/");
-
-  for (p = sandbox; *p != 0; p++)
-    {
-      if (*p == ':')
-        g_string_append (s, "\\:");
-      else
-        g_string_append_c (s, *p);
-    }
-
-  if (readonly)
-    g_string_append (s, ":ro");
-
-  return g_string_free (g_steal_pointer (&s), FALSE);
-}
-
-static char *
-bubblewrap_remap_path (const char *path)
-{
-  if (g_str_has_prefix (path, "/newroot/"))
-    path = path + strlen ("/newroot");
-  return g_strdup (path);
-}
-
-static char *
-verify_proc_self_fd (const char *proc_path,
-                     GError **error)
-{
-  char path_buffer[PATH_MAX + 1];
-  ssize_t symlink_size;
-
-  symlink_size = readlink (proc_path, path_buffer, PATH_MAX);
-  if (symlink_size < 0)
-    return glnx_null_throw_errno_prefix (error, "readlink");
-
-  path_buffer[symlink_size] = 0;
-
-  /* All normal paths start with /, but some weird things
-     don't, such as socket:[27345] or anon_inode:[eventfd].
-     We don't support any of these */
-  if (path_buffer[0] != '/')
-    return glnx_null_throw (error, "%s resolves to non-absolute path %s",
-                            proc_path, path_buffer);
-
-  /* File descriptors to actually deleted files have " (deleted)"
-     appended to them. This also happens to some fake fd types
-     like shmem which are "/<name> (deleted)". All such
-     files are considered invalid. Unfortunatelly this also
-     matches files with filenames that actually end in " (deleted)",
-     but there is not much to do about this. */
-  if (g_str_has_suffix (path_buffer, " (deleted)"))
-    return glnx_null_throw (error, "%s resolves to deleted path %s",
-                            proc_path, path_buffer);
-
-  /* remap from sandbox to host if needed */
-  return bubblewrap_remap_path (path_buffer);
-}
-
-static char *
-get_path_for_fd (int fd,
-                 gboolean *writable_out,
-                 GError **error)
-{
-  g_autofree char *proc_path = NULL;
   int fd_flags;
   struct stat st_buf;
   struct stat real_st_buf;
+  int access_mode;
   g_autofree char *path = NULL;
-  gboolean writable = FALSE;
-  int read_access_mode;
 
   /* Must be able to get fd flags */
   fd_flags = fcntl (fd, F_GETFL);
-  if (fd_flags == -1)
-    return glnx_null_throw_errno_prefix (error, "fcntl F_GETFL");
+  if (fd_flags < 0)
+    return glnx_throw_errno_prefix (error, "Failed to get fd flags");
 
   /* Must be O_PATH */
   if ((fd_flags & O_PATH) != O_PATH)
-    return glnx_null_throw (error, "not opened with O_PATH");
-
-  /* We don't want to allow exposing symlinks, because if they are
-   * under the callers control they could be changed between now and
-   * starting the child allowing it to point anywhere, so enforce NOFOLLOW.
-   * and verify that stat is not a link.
-   */
-  if ((fd_flags & O_NOFOLLOW) != O_NOFOLLOW)
-    return glnx_null_throw (error, "not opened with O_NOFOLLOW");
+    {
+      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                           "File descriptor is not O_PATH");
+      return FALSE;
+    }
 
   /* Must be able to fstat */
   if (fstat (fd, &st_buf) < 0)
-    return glnx_null_throw_errno_prefix (error, "fstat");
-
-  /* As per above, no symlinks */
-  if (S_ISLNK (st_buf.st_mode))
-    return glnx_null_throw (error, "is a symbolic link");
-
-  proc_path = g_strdup_printf ("/proc/self/fd/%d", fd);
+    return glnx_throw_errno_prefix (error, "Failed to fstat");
 
-  /* Must be able to read valid path from /proc/self/fd */
-  /* This is an absolute and (at least at open time) symlink-expanded path */
-  path = verify_proc_self_fd (proc_path, error);
+  path = flatpak_get_path_for_fd (fd, error);
   if (path == NULL)
-    return NULL;
+    return FALSE;
 
-  /* Verify that this is the same file as the app opened */
+  /* Verify that this is the same file as the app opened.
+   * Note that this is not security relevant because flatpak-run/bwrap will
+   * check things and abort if something is off. We do this only for backwards
+   * compatibility reasons: we need to be able to ignore the issue instead of
+   * aborting the entire sandbox setup later. */
   if (stat (path, &real_st_buf) < 0 ||
       st_buf.st_dev != real_st_buf.st_dev ||
       st_buf.st_ino != real_st_buf.st_ino)
     {
       /* Different files on the inside and the outside, reject the request */
-      return glnx_null_throw (error,
-                              "different file inside and outside sandbox");
+      return glnx_throw (error,
+                         "different file inside and outside sandbox");
     }
 
-  read_access_mode = R_OK;
+  access_mode = R_OK;
   if (S_ISDIR (st_buf.st_mode))
-    read_access_mode |= X_OK;
+    access_mode |= X_OK;
 
-  /* Must be able to access the path via the sandbox supplied O_PATH fd,
-     which applies the sandbox side mount options (like readonly). */
-  if (access (proc_path, read_access_mode) != 0)
-    return glnx_null_throw (error, "not %s in sandbox",
-                            read_access_mode & X_OK ? "accessible" : "readable");
+  if (needs_writable)
+    access_mode |= W_OK;
 
-  if (access (proc_path, W_OK) == 0)
-    writable = TRUE;
+  /* Must be able to access readable and potentially writable */
+  if (faccessat (fd, "", access_mode, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW) != 0)
+    return glnx_throw_errno_prefix (error, "Bad access mode");
+
+  return TRUE;
+}
+
+static int
+fd_map_remap_fd (GArray *fd_map,
+                 int    *max_fd_in_out,
+                 int     fd)
+{
+  FdMapEntry fd_map_entry;
 
-  if (writable_out != NULL)
-    *writable_out = writable;
+  /* Use a fd that hasn't been used yet. We might have to reshuffle
+   * fd_map_entry.to, a bit later. */
+  fd_map_entry.from = fd;
+  fd_map_entry.to = ++(*max_fd_in_out);
+  fd_map_entry.final = fd_map_entry.to;
+  g_array_append_val (fd_map, fd_map_entry);
 
-  return g_steal_pointer (&path);
+  return fd_map_entry.final;
 }
 
 static gboolean
@@ -798,10 +683,13 @@
   gboolean devel;
   gboolean empty_app;
   g_autoptr(GString) env_string = g_string_new ("");
-  glnx_autofd int env_fd = -1;
   const char *flatpak;
   gboolean testing = FALSE;
   g_autofree char *app_id_prefix = NULL;
+  g_autoptr(GArray) owned_fds = NULL;
+  g_autoptr(GArray) expose_fds = NULL;
+  g_autoptr(GArray) expose_fds_ro = NULL;
+  glnx_autofd int instance_sandbox_fd = -1;
 
   child_setup_data.instance_id_fd = -1;
   child_setup_data.env_fd = -1;
@@ -925,29 +813,6 @@
       return G_DBUS_METHOD_INVOCATION_HANDLED;
     }
 
-  for (i = 0; sandbox_expose != NULL && sandbox_expose[i] != NULL; i++)
-    {
-      const char *expose = sandbox_expose[i];
-
-      g_info ("exposing %s", expose);
-      if (!is_valid_expose (expose, &error))
-        {
-          g_dbus_method_invocation_return_gerror (invocation, error);
-          return G_DBUS_METHOD_INVOCATION_HANDLED;
-        }
-    }
-
-  for (i = 0; sandbox_expose_ro != NULL && sandbox_expose_ro[i] != NULL; i++)
-    {
-      const char *expose = sandbox_expose_ro[i];
-      g_info ("exposing %s", expose);
-      if (!is_valid_expose (expose, &error))
-        {
-          g_dbus_method_invocation_return_gerror (invocation, error);
-          return G_DBUS_METHOD_INVOCATION_HANDLED;
-        }
-    }
-
   app_id_prefix = g_strdup_printf ("%s.", app_id);
   for (i = 0; sandbox_a11y_own_names != NULL && sandbox_a11y_own_names[i] != NULL; i++)
     {
@@ -1197,10 +1062,14 @@
       g_string_append_c (env_string, '\0');
     }
 
+  owned_fds = g_array_new (FALSE, FALSE, sizeof (int));
+  g_array_set_clear_func (owned_fds, (GDestroyNotify) glnx_close_fd);
+
   if (env_string->len > 0)
     {
-      FdMapEntry fd_map_entry;
       g_auto(GLnxTmpfile) env_tmpf  = { 0, };
+      int env_fd = -1;
+      int remapped_fd;
 
       if (!flatpak_buffer_to_sealed_memfd_or_tmpfile (&env_tmpf, "environ",
                                                       env_string->str,
@@ -1211,16 +1080,12 @@
         }
 
       env_fd = g_steal_fd (&env_tmpf.fd);
+      g_array_append_val (owned_fds, env_fd);
 
-      /* Use a fd that hasn't been used yet. We might have to reshuffle
-       * fd_map_entry.to, a bit later. */
-      fd_map_entry.from = env_fd;
-      fd_map_entry.to = ++max_fd;
-      fd_map_entry.final = fd_map_entry.to;
-      g_array_append_val (fd_map, fd_map_entry);
+      remapped_fd = fd_map_remap_fd (fd_map, &max_fd, env_fd);
 
       g_ptr_array_add (flatpak_argv,
-                       g_strdup_printf ("--env-fd=%d", fd_map_entry.final));
+                       g_strdup_printf ("--env-fd=%d", remapped_fd));
     }
 
   for (i = 0; unset_env != NULL && unset_env[i] != NULL; i++)
@@ -1305,6 +1170,7 @@
 
       g_ptr_array_add (flatpak_argv, g_strdup_printf ("--instance-id-fd=%d", pipe_fds[1]));
       child_setup_data.instance_id_fd = pipe_fds[1];
+      max_fd = MAX(max_fd, pipe_fds[1]);
     }
 
   if (devel)
@@ -1318,111 +1184,178 @@
   else
     g_ptr_array_add (flatpak_argv, g_strdup ("--unshare=network"));
 
+  expose_fds = g_array_new (FALSE, FALSE, sizeof (int));
+  expose_fds_ro = g_array_new (FALSE, FALSE, sizeof (int));
+
+  if (instance_path != NULL)
+    {
+      glnx_autofd int instance_fd = -1;
+
+      instance_fd = glnx_chaseat (AT_FDCWD, instance_path,
+                                  GLNX_CHASE_DEFAULT,
+                                  &error);
+      if (instance_fd < 0)
+        {
+          g_dbus_method_invocation_return_gerror (invocation, error);
+          return G_DBUS_METHOD_INVOCATION_HANDLED;
+        }
+
+      if (!glnx_ensure_dir (instance_fd, "sandbox", 0700, &error))
+        {
+          g_warning ("Unable to create %s/sandbox: %s", instance_path, error->message);
+          g_clear_error (&error);
+        }
+
+      instance_sandbox_fd = glnx_chaseat (instance_fd, "sandbox",
+                                          GLNX_CHASE_RESOLVE_NO_SYMLINKS,
+                                          &error);
+      if (instance_sandbox_fd < 0)
+        {
+          g_dbus_method_invocation_return_gerror (invocation, error);
+          return G_DBUS_METHOD_INVOCATION_HANDLED;
+        }
+    }
 
-  if (instance_path)
+  for (i = 0; sandbox_expose != NULL && sandbox_expose[i] != NULL; i++)
     {
-      for (i = 0; sandbox_expose != NULL && sandbox_expose[i] != NULL; i++)
-        g_ptr_array_add (flatpak_argv,
-                         filesystem_sandbox_arg (instance_path, sandbox_expose[i], FALSE));
-      for (i = 0; sandbox_expose_ro != NULL && sandbox_expose_ro[i] != NULL; i++)
-        g_ptr_array_add (flatpak_argv,
-                         filesystem_sandbox_arg (instance_path, sandbox_expose_ro[i], TRUE));
+      int expose_fd;
+
+      g_assert (instance_sandbox_fd >= 0);
+
+      expose_fd = glnx_chaseat (instance_sandbox_fd, sandbox_expose[i],
+                                GLNX_CHASE_RESOLVE_NO_SYMLINKS |
+                                GLNX_CHASE_RESOLVE_BENEATH,
+                                &error);
+      if (expose_fd < 0)
+        {
+          g_dbus_method_invocation_return_gerror (invocation, error);
+          return G_DBUS_METHOD_INVOCATION_HANDLED;
+        }
+
+      g_array_append_val (expose_fds, expose_fd);
+      /* transfers ownership, can't g_steal_fd with g_array_append_val */
+      g_array_append_val (owned_fds, expose_fd);
     }
 
   for (i = 0; sandbox_expose_ro != NULL && sandbox_expose_ro[i] != NULL; i++)
     {
-      const char *expose = sandbox_expose_ro[i];
-      g_info ("exposing %s", expose);
+      int expose_fd;
+
+      g_assert (instance_sandbox_fd >= 0);
+
+      expose_fd = glnx_chaseat (instance_sandbox_fd, sandbox_expose_ro[i],
+                                GLNX_CHASE_RESOLVE_NO_SYMLINKS |
+                                GLNX_CHASE_RESOLVE_BENEATH,
+                                &error);
+      if (expose_fd < 0)
+        {
+          g_dbus_method_invocation_return_gerror (invocation, error);
+          return G_DBUS_METHOD_INVOCATION_HANDLED;
+        }
+
+      g_array_append_val (expose_fds_ro, expose_fd);
+      /* transfers ownership, can't g_steal_fd with g_array_append_val */
+      g_array_append_val (owned_fds, expose_fd);
     }
 
   if (sandbox_expose_fd != NULL)
     {
       gsize len = g_variant_n_children (sandbox_expose_fd);
+
       for (i = 0; i < len; i++)
         {
           gint32 handle;
-          g_variant_get_child (sandbox_expose_fd, i, "h", &handle);
-          if (handle >= 0 && handle < fds_len)
-            {
-              int handle_fd = fds[handle];
-              g_autofree char *path = NULL;
-              gboolean writable = FALSE;
-
-              path = get_path_for_fd (handle_fd, &writable, &error);
 
-              if (path)
-                {
-                  g_ptr_array_add (flatpak_argv, filesystem_arg (path, !writable));
-                }
-              else
-                {
-                  g_info ("unable to get path for sandbox-exposed fd %d, ignoring: %s",
-                          handle_fd, error->message);
-                  g_clear_error (&error);
-                }
-            }
-          else
+          g_variant_get_child (sandbox_expose_fd, i, "h", &handle);
+          if (handle >= fds_len || handle < 0)
             {
+              g_debug ("Invalid sandbox-expose-fd handle %d", handle);
               g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                                      G_DBUS_ERROR_INVALID_ARGS,
                                                      "No file descriptor for handle %d",
                                                      handle);
               return G_DBUS_METHOD_INVOCATION_HANDLED;
             }
+
+          if (validate_opath_fd (fds[handle], TRUE, &error))
+            {
+              g_array_append_val (expose_fds, fds[handle]);
+            }
+          else
+            {
+              g_info ("unable to validate sandbox-expose-fd %d, ignoring: %s",
+                      fds[handle], error->message);
+              g_clear_error (&error);
+            }
         }
     }
 
   if (sandbox_expose_fd_ro != NULL)
     {
       gsize len = g_variant_n_children (sandbox_expose_fd_ro);
+
       for (i = 0; i < len; i++)
         {
           gint32 handle;
-          g_variant_get_child (sandbox_expose_fd_ro, i, "h", &handle);
-          if (handle >= 0 && handle < fds_len)
-            {
-              int handle_fd = fds[handle];
-              g_autofree char *path = NULL;
-              gboolean writable = FALSE;
 
-              path = get_path_for_fd (handle_fd, &writable, &error);
-
-              if (path)
-                {
-                  g_ptr_array_add (flatpak_argv, filesystem_arg (path, TRUE));
-                }
-              else
-                {
-                  g_info ("unable to get path for sandbox-exposed fd %d, ignoring: %s",
-                          handle_fd, error->message);
-                  g_clear_error (&error);
-                }
-            }
-          else
+          g_variant_get_child (sandbox_expose_fd_ro, i, "h", &handle);
+          if (handle >= fds_len || handle < 0)
             {
+              g_debug ("Invalid sandbox-expose-ro-fd handle %d", handle);
               g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                                      G_DBUS_ERROR_INVALID_ARGS,
                                                      "No file descriptor for handle %d",
                                                      handle);
               return G_DBUS_METHOD_INVOCATION_HANDLED;
             }
+
+          if (validate_opath_fd (fds[handle], FALSE, &error))
+            {
+              g_array_append_val (expose_fds_ro, fds[handle]);
+            }
+          else
+            {
+              g_info ("unable to validate sandbox-expose-ro-fd %d, ignoring: %s",
+                      fds[handle], error->message);
+              g_clear_error (&error);
+            }
         }
     }
 
+  for (i = 0; i < expose_fds->len; i++)
+    {
+      int remapped_fd;
+
+      remapped_fd = fd_map_remap_fd (fd_map, &max_fd, g_array_index (expose_fds, int, i));
+
+      g_ptr_array_add (flatpak_argv, g_strdup_printf ("--bind-fd=%d",
+                                                      remapped_fd));
+    }
+
+  for (i = 0; i < expose_fds_ro->len; i++)
+    {
+      int remapped_fd;
+
+      remapped_fd = fd_map_remap_fd (fd_map, &max_fd, g_array_index (expose_fds_ro, int, i));
+
+      g_ptr_array_add (flatpak_argv, g_strdup_printf ("--ro-bind-fd=%d",
+                                                      remapped_fd));
+    }
+
   empty_app = (arg_flags & FLATPAK_SPAWN_FLAGS_EMPTY_APP) != 0;
 
+  if (empty_app && app_fd != NULL)
+    {
+      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
+                                             G_DBUS_ERROR_INVALID_ARGS,
+                                             "app-fd and EMPTY_APP cannot both be used");
+      return G_DBUS_METHOD_INVOCATION_HANDLED;
+    }
+
   if (app_fd != NULL)
     {
+      int remapped_fd;
       gint32 handle = g_variant_get_handle (app_fd);
-      g_autofree char *path = NULL;
-
-      if (empty_app)
-        {
-          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
-                                                 G_DBUS_ERROR_INVALID_ARGS,
-                                                 "app-fd and EMPTY_APP cannot both be used");
-          return G_DBUS_METHOD_INVOCATION_HANDLED;
-        }
 
       if (handle >= fds_len || handle < 0)
         {
@@ -1434,18 +1367,11 @@
         }
 
       g_assert (fds != NULL);   /* otherwise fds_len would be 0 */
-      path = get_path_for_fd (fds[handle], NULL, &error);
 
-      if (path == NULL)
-        {
-          g_prefix_error (&error, "Unable to convert /app fd %d into path: ",
-                          fds[handle]);
-          g_dbus_method_invocation_return_gerror (invocation, error);
-          return G_DBUS_METHOD_INVOCATION_HANDLED;
-        }
+      remapped_fd = fd_map_remap_fd (fd_map, &max_fd, fds[handle]);
 
-      g_info ("Using %s as /app instead of app", path);
-      g_ptr_array_add (flatpak_argv, g_strdup_printf ("--app-path=%s", path));
+      g_ptr_array_add (flatpak_argv, g_strdup_printf ("--app-fd=%d",
+                                                      remapped_fd));
     }
   else if (empty_app)
     {
@@ -1454,8 +1380,8 @@
 
   if (usr_fd != NULL)
     {
+      int remapped_fd;
       gint32 handle = g_variant_get_handle (usr_fd);
-      g_autofree char *path = NULL;
 
       if (handle >= fds_len || handle < 0)
         {
@@ -1467,18 +1393,11 @@
         }
 
       g_assert (fds != NULL);   /* otherwise fds_len would be 0 */
-      path = get_path_for_fd (fds[handle], NULL, &error);
 
-      if (path == NULL)
-        {
-          g_prefix_error (&error, "Unable to convert /usr fd %d into path: ",
-                          fds[handle]);
-          g_dbus_method_invocation_return_gerror (invocation, error);
-          return G_DBUS_METHOD_INVOCATION_HANDLED;
-        }
+      remapped_fd = fd_map_remap_fd (fd_map, &max_fd, fds[handle]);
 
-      g_info ("Using %s as /usr instead of runtime", path);
-      g_ptr_array_add (flatpak_argv, g_strdup_printf ("--usr-path=%s", path));
+      g_ptr_array_add (flatpak_argv, g_strdup_printf ("--usr-fd=%d",
+                                                      remapped_fd));
     }
 
   g_ptr_array_add (flatpak_argv, g_strdup_printf ("--runtime=%s", runtime_parts[1]));
diff -Nru flatpak-1.16.3/subprojects/libglnx/glnx-backports.c flatpak-1.16.6/subprojects/libglnx/glnx-backports.c
--- flatpak-1.16.3/subprojects/libglnx/glnx-backports.c	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/subprojects/libglnx/glnx-backports.c	2026-04-10 18:28:42.000000000 +0100
@@ -106,6 +106,78 @@
 }
 #endif
 
+#if !GLIB_CHECK_VERSION(2, 76, 0)
+gboolean
+_glnx_close (gint     fd,
+             GError **error)
+{
+  int res;
+
+  /* Important: if @error is NULL, we must not do anything that is
+   * not async-signal-safe.
+   */
+  res = close (fd);
+
+  if (res == -1)
+    {
+      int errsv = errno;
+
+      if (errsv == EINTR)
+        {
+          /* Just ignore EINTR for now; a retry loop is the wrong thing to do
+           * on Linux at least.  Anyone who wants to add a conditional check
+           * for e.g. HP-UX is welcome to do so later...
+           *
+           * close_func_with_invalid_fds() in gspawn.c has similar logic.
+           *
+           * https://lwn.net/Articles/576478/
+           * http://lkml.indiana.edu/hypermail/linux/kernel/0509.1/0877.html
+           * https://bugzilla.gnome.org/show_bug.cgi?id=682819
+           * http://utcc.utoronto.ca/~cks/space/blog/unix/CloseEINTR
+           * https://sites.google.com/site/michaelsafyan/software-engineering/checkforeintrwheninvokingclosethinkagain
+           *
+           * `close$NOCANCEL()` in gstdioprivate.h, on macOS, ensures that the fd is
+           * closed even if it did return EINTR.
+           */
+          return TRUE;
+        }
+
+      if (error)
+        {
+          g_set_error_literal (error, G_FILE_ERROR,
+                               g_file_error_from_errno (errsv),
+                               g_strerror (errsv));
+        }
+
+      if (errsv == EBADF)
+        {
+          /* There is a bug. Fail an assertion. Note that this function is supposed to be
+           * async-signal-safe, but in case an assertion fails, all bets are already off. */
+          if (fd >= 0)
+            {
+              /* Closing an non-negative, invalid file descriptor is a bug. The bug is
+               * not necessarily in the caller of _glnx_close(), but somebody else
+               * might have wrongly closed fd. In any case, there is a serious bug
+               * somewhere. */
+              g_critical ("_glnx_close(fd:%d) failed with EBADF. The tracking of file descriptors got messed up", fd);
+            }
+          else
+            {
+              /* Closing a negative "file descriptor" is less problematic. It's still a nonsensical action
+               * from the caller. Assert against that too. */
+              g_critical ("_glnx_close(fd:%d) failed with EBADF. This is not a valid file descriptor", fd);
+            }
+        }
+
+      errno = errsv;
+
+      return FALSE;
+    }
+
+  return TRUE;
+}
+#endif
+
 #if !GLIB_CHECK_VERSION(2, 80, 0)
 /* This function is called between fork() and exec() and hence must be
  * async-signal-safe (see signal-safety(7)). */
diff -Nru flatpak-1.16.3/subprojects/libglnx/glnx-backports.h flatpak-1.16.6/subprojects/libglnx/glnx-backports.h
--- flatpak-1.16.3/subprojects/libglnx/glnx-backports.h	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/subprojects/libglnx/glnx-backports.h	2026-04-10 18:28:42.000000000 +0100
@@ -29,6 +29,7 @@
 #include <string.h>
 
 #include <glib-unix.h>
+#include <glib/gstdio.h>
 #include <gio/gio.h>
 
 G_BEGIN_DECLS
@@ -53,6 +54,34 @@
   } G_STMT_END
 #endif
 
+#if !GLIB_CHECK_VERSION(2, 76, 0)
+gboolean _glnx_close (gint     fd,
+                      GError **error);
+#else
+#define _glnx_close g_close
+#endif
+
+#if !GLIB_CHECK_VERSION(2, 76, 0)
+static inline gboolean
+g_clear_fd (int     *fd_ptr,
+            GError **error)
+{
+  int fd = *fd_ptr;
+
+  *fd_ptr = -1;
+
+  if (fd < 0)
+    return TRUE;
+
+  /* Suppress "Not available before" warning */
+  G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+  /* This importantly calls _glnx_close to always get async-signal-safe if
+   * error == NULL */
+  return _glnx_close (fd, error);
+  G_GNUC_END_IGNORE_DEPRECATIONS
+}
+#endif
+
 #if !GLIB_CHECK_VERSION(2, 40, 0)
 #define g_info(...) g_log (G_LOG_DOMAIN, G_LOG_LEVEL_INFO, __VA_ARGS__)
 #endif
diff -Nru flatpak-1.16.3/subprojects/libglnx/glnx-chase.c flatpak-1.16.6/subprojects/libglnx/glnx-chase.c
--- flatpak-1.16.3/subprojects/libglnx/glnx-chase.c	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/subprojects/libglnx/glnx-chase.c	2026-04-10 18:28:42.000000000 +0100
@@ -0,0 +1,789 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2026 Red Hat, Inc.
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * glnx_chaseat was inspired by systemd's chase
+ */
+
+#include "libglnx-config.h"
+
+#include <fcntl.h>
+#include <stdint.h>
+#include <sys/mount.h>
+#include <sys/statfs.h>
+#include <sys/syscall.h>
+#include <sys/vfs.h>
+#include <unistd.h>
+
+#include <glnx-backports.h>
+#include <glnx-errors.h>
+#include <glnx-fdio.h>
+#include <glnx-local-alloc.h>
+#include <glnx-missing.h>
+
+#include <glnx-chase.h>
+
+#define AUTOFS_SUPER_MAGIC 0x0187 /* man fstatfs */
+
+#define GLNX_CHASE_DEBUG_NO_OPENAT2 (1U << 31)
+#define GLNX_CHASE_DEBUG_NO_OPEN_TREE (1U << 30)
+
+#define GLNX_CHASE_ALL_DEBUG_FLAGS \
+  (GLNX_CHASE_DEBUG_NO_OPENAT2 | \
+   GLNX_CHASE_DEBUG_NO_OPEN_TREE)
+
+#define GLNX_CHASE_ALL_REGULAR_FLAGS \
+  (GLNX_CHASE_NO_AUTOMOUNT | \
+   GLNX_CHASE_NOFOLLOW | \
+   GLNX_CHASE_RESOLVE_BENEATH | \
+   GLNX_CHASE_RESOLVE_IN_ROOT | \
+   GLNX_CHASE_RESOLVE_NO_SYMLINKS | \
+   GLNX_CHASE_MUST_BE_REGULAR | \
+   GLNX_CHASE_MUST_BE_DIRECTORY | \
+   GLNX_CHASE_MUST_BE_SOCKET)
+
+#define GLNX_CHASE_ALL_FLAGS \
+  (GLNX_CHASE_ALL_DEBUG_FLAGS | GLNX_CHASE_ALL_REGULAR_FLAGS)
+
+typedef GQueue GlnxStatxQueue;
+
+static void
+glnx_statx_queue_push (GlnxStatxQueue          *queue,
+                       const struct glnx_statx *st)
+{
+  struct glnx_statx *copy;
+
+  copy = g_memdup2 (st, sizeof (*st));
+  g_queue_push_tail (queue, copy);
+}
+
+static void
+glnx_statx_queue_free_element (gpointer element,
+                               G_GNUC_UNUSED gpointer userdata)
+{
+  g_free (element);
+}
+
+static void
+glnx_statx_queue_free (GlnxStatxQueue *squeue)
+{
+  GQueue *queue = (GQueue *) squeue;
+
+  /* Same as g_queue_clear_full (queue, g_free), but works for <2.60 */
+  g_queue_foreach (queue, glnx_statx_queue_free_element, NULL);
+  g_queue_clear (queue);
+}
+
+G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(GlnxStatxQueue, glnx_statx_queue_free)
+
+static gboolean
+glnx_statx_inode_same (const struct glnx_statx *a,
+                       const struct glnx_statx *b)
+{
+  g_assert ((a->stx_mask & (GLNX_STATX_TYPE | GLNX_STATX_INO)) ==
+            (GLNX_STATX_TYPE | GLNX_STATX_INO));
+  g_assert ((b->stx_mask & (GLNX_STATX_TYPE | GLNX_STATX_INO)) ==
+            (GLNX_STATX_TYPE | GLNX_STATX_INO));
+
+  return ((a->stx_mode ^ b->stx_mode) & S_IFMT) == 0 &&
+         a->stx_dev_major == b->stx_dev_major &&
+         a->stx_dev_minor == b->stx_dev_minor &&
+         a->stx_ino == b->stx_ino;
+}
+
+static gboolean
+glnx_statx_mount_same (const struct glnx_statx *a,
+                       const struct glnx_statx *b)
+{
+  g_assert ((a->stx_mask & (GLNX_STATX_MNT_ID | GLNX_STATX_MNT_ID_UNIQUE)) != 0);
+  g_assert ((b->stx_mask & (GLNX_STATX_MNT_ID | GLNX_STATX_MNT_ID_UNIQUE)) != 0);
+
+  return a->stx_mnt_id == b->stx_mnt_id;
+}
+
+static gboolean
+glnx_chase_statx (int                 dfd,
+                  int                 additional_flags,
+                  struct glnx_statx  *buf,
+                  GError            **error)
+{
+  if (!glnx_statx (dfd, "",
+                   AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW | additional_flags,
+                   GLNX_STATX_TYPE | GLNX_STATX_INO |
+                   GLNX_STATX_MNT_ID | GLNX_STATX_MNT_ID_UNIQUE,
+                   buf,
+                   error))
+    return FALSE;
+
+  if ((buf->stx_mask & (GLNX_STATX_TYPE | GLNX_STATX_INO)) !=
+        (GLNX_STATX_TYPE | GLNX_STATX_INO) ||
+      (buf->stx_mask & (GLNX_STATX_MNT_ID | GLNX_STATX_MNT_ID_UNIQUE)) == 0)
+    {
+      errno = ENODATA;
+      return glnx_throw_errno_prefix (error,
+                                      "statx didn't return all required fields");
+    }
+
+  return TRUE;
+}
+
+/* TODO: procfs magiclinks handling */
+
+/* open_tree subset which transparently falls back to openat.
+ *
+ * Returned fd is always OPATH and CLOEXEC.
+ *
+ * With NO_AUTOMOUNT this function never triggers automounts. Otherwise, it only
+ * guarantees to trigger an automount which is on last segment of the path!
+ *
+ * flags can be a combinations of:
+ *  - GLNX_CHASE_NO_AUTOMOUNT
+ *  - GLNX_CHASE_NOFOLLOW
+ */
+static int
+chase_open_tree (int              dirfd,
+                 const char      *path,
+                 GlnxChaseFlags   flags,
+                 GError         **error)
+{
+  glnx_autofd int fd = -1;
+  static gboolean can_open_tree = TRUE;
+  unsigned int openat_flags = 0;
+
+  g_assert ((flags & ~(GLNX_CHASE_NO_AUTOMOUNT |
+                       GLNX_CHASE_NOFOLLOW |
+                       GLNX_CHASE_ALL_DEBUG_FLAGS)) == 0);
+
+  /* First we try to actually use open_tree, and then fall back to the impl
+   * using openat.
+   * Technically racy (static, not synced), but both paths work fine so it
+   * doesn't matter. */
+  if (can_open_tree && (flags & GLNX_CHASE_DEBUG_NO_OPEN_TREE) == 0)
+    {
+      unsigned int open_tree_flags = 0;
+
+      open_tree_flags = OPEN_TREE_CLOEXEC;
+      if ((flags & GLNX_CHASE_NOFOLLOW) != 0)
+        open_tree_flags |= AT_SYMLINK_NOFOLLOW;
+      if ((flags & GLNX_CHASE_NO_AUTOMOUNT) != 0)
+        open_tree_flags |= AT_NO_AUTOMOUNT;
+
+      fd = open_tree (dirfd, path, open_tree_flags);
+
+      /* If open_tree is not supported, or blocked (EPERM), we fall back to
+       * openat */
+      if (fd < 0 && G_IN_SET (errno,
+                              EOPNOTSUPP,
+                              ENOTTY,
+                              ENOSYS,
+                              EAFNOSUPPORT,
+                              EPFNOSUPPORT,
+                              EPROTONOSUPPORT,
+                              ESOCKTNOSUPPORT,
+                              ENOPROTOOPT,
+                              EPERM))
+        can_open_tree = FALSE;
+      else if (fd < 0)
+        return glnx_fd_throw_errno_prefix (error, "open_tree");
+      else
+        return g_steal_fd (&fd);
+    }
+
+  openat_flags = O_CLOEXEC | O_PATH;
+  if ((flags & GLNX_CHASE_NOFOLLOW) != 0)
+    openat_flags |= O_NOFOLLOW;
+
+  fd = openat (dirfd, path, openat_flags);
+  if (fd < 0)
+    return glnx_fd_throw_errno_prefix (error, "openat in open_tree fallback");
+
+  /* openat does not trigger automounts, so we have to manually do so
+   * unless NO_AUTOMOUNT was specified */
+  if ((flags & GLNX_CHASE_NO_AUTOMOUNT) == 0)
+    {
+      struct statfs stfs;
+
+      if (fstatfs (fd, &stfs) < 0)
+        return glnx_fd_throw_errno_prefix (error, "fstatfs in open_tree fallback");
+
+      /* fstatfs(2) can then be used to determine if it is, in fact, an
+       * untriggered automount point (.f_type == AUTOFS_SUPER_MAGIC). */
+      if (stfs.f_type == AUTOFS_SUPER_MAGIC)
+        {
+          glnx_autofd int new_fd = -1;
+
+          new_fd = openat (fd, ".", openat_flags | O_DIRECTORY);
+          /* For some reason, openat with O_PATH | O_DIRECTORY does trigger
+           * automounts, without us having to actually open the file, so let's
+           * use this here. It only works for directories though. */
+          if (new_fd >= 0)
+            return g_steal_fd (&new_fd);
+
+          if (errno != ENOTDIR)
+            return glnx_fd_throw_errno_prefix (error, "openat(O_DIRECTORY) in autofs mount open_tree fallback");
+
+          /* The automount is a directory, so let's try to open the file,
+           * which can fail because we are missing permissions, but that's
+           * okay, we only need to trigger automount. */
+          new_fd = openat (fd, ".", (openat_flags & ~O_PATH) |
+                                    O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY);
+          glnx_close_fd (&new_fd);
+
+          /* And try again with O_PATH */
+          new_fd = openat (dirfd, path, openat_flags);
+          if (new_fd < 0)
+            return glnx_fd_throw_errno_prefix (error, "reopening in autofs mount open_tree fallback");
+
+          if (fstatfs (new_fd, &stfs) < 0)
+            return glnx_fd_throw_errno_prefix (error, "fstatfs in autofs mount open_tree fallback");
+
+          /* bail if we didn't manage to trigger the automount */
+          if (stfs.f_type == AUTOFS_SUPER_MAGIC)
+            {
+              errno = EOPNOTSUPP;
+              return glnx_fd_throw_errno_prefix (error, "unable to trigger automount");
+            }
+
+          return g_steal_fd (&new_fd);
+        }
+    }
+
+  return g_steal_fd (&fd);
+}
+
+static int
+open_cwd (GlnxChaseFlags   flags,
+          GError         **error)
+{
+  GLNX_AUTO_PREFIX_ERROR ("cannot open working directory", error);
+
+  /* NO_AUTOMOUNT should be fine here because automount must have been
+   * triggered already for the CWD */
+  return chase_open_tree (AT_FDCWD, ".",
+                          (flags & GLNX_CHASE_ALL_DEBUG_FLAGS) |
+                          GLNX_CHASE_NO_AUTOMOUNT |
+                          GLNX_CHASE_NOFOLLOW,
+                          error);
+}
+
+static int
+open_root (GlnxChaseFlags   flags,
+           GError         **error)
+{
+  GLNX_AUTO_PREFIX_ERROR ("cannot open root directory", error);
+
+  /* NO_AUTOMOUNT should be fine here because automount must have been
+   * triggered already for the root */
+  return chase_open_tree (AT_FDCWD, "/",
+                          (flags & GLNX_CHASE_ALL_DEBUG_FLAGS) |
+                          GLNX_CHASE_NO_AUTOMOUNT |
+                          GLNX_CHASE_NOFOLLOW,
+                          error);
+}
+
+/* This returns the next segment of a path and tells us if it is the last
+ * segment.
+ *
+ * Importantly, a segment is anything after a "/", even if it is empty  or ".".
+ *
+ * For example:
+ *   "" -> ""
+ *   "/" -> ""
+ *   "////" -> ""
+ *   "foo/bar" -> "foo", "bar"
+ *   "foo//bar" -> "foo", "bar"
+ *   "///foo//bar" -> "foo", "bar"
+ *   "///foo//bar/" -> "foo", "bar", ""
+ *   "///foo//bar/." -> "foo", "bar", "."
+ */
+static char *
+extract_next_segment (const char **remaining,
+                      gboolean    *is_last)
+{
+  const char *r = *remaining;
+  const char *s;
+  size_t len = 0;
+
+  while (r[0] != '\0' && G_IS_DIR_SEPARATOR (r[0]))
+    r++;
+
+  s = r;
+
+  while (r[0] != '\0' && !G_IS_DIR_SEPARATOR (r[0]))
+    {
+      r++;
+      len++;
+    }
+
+  *is_last = (r[0] == '\0');
+  *remaining = r;
+  return g_strndup (s, len);
+}
+
+/* This iterates over the segments of path and opens the corresponding
+ * directories or files. This gives us the opportunity to implement openat2
+ * like RESOLVE_ semantics, without actually needing openat2.
+ * It also allows us to implement features which openat2 does not have because
+ * we're in full control over the resolving.
+ */
+static int
+chase_manual (int              dirfd,
+              const char      *path,
+              GlnxChaseFlags   flags,
+              GError         **error)
+{
+  gboolean is_absolute;
+  g_autofree char *buffer = NULL;
+  const char *remaining;
+  glnx_autofd int owned_root_fd = -1;
+  int root_fd;
+  glnx_autofd int owned_fd = -1;
+  int fd;
+  int remaining_follows = GLNX_CHASE_MAX;
+  struct glnx_statx st;
+  g_auto(GlnxStatxQueue) path_st = G_QUEUE_INIT;
+  int no_automount;
+
+  /* Take a shortcut if
+   * - none of the resolve flags are set (they would require work here)
+   * - NO_AUTOMOUNT is set (chase_open_tree only triggers the automount for
+   *   last component in some cases)
+   *
+   * TODO: if we have a guarantee that the open_tree syscall works, we can
+   * shortcut even without GLNX_CHASE_NO_AUTOMOUNT
+   */
+  if ((flags & (GLNX_CHASE_NO_AUTOMOUNT |
+                GLNX_CHASE_RESOLVE_BENEATH |
+                GLNX_CHASE_RESOLVE_IN_ROOT |
+                GLNX_CHASE_RESOLVE_NO_SYMLINKS)) == GLNX_CHASE_NO_AUTOMOUNT)
+    {
+      GlnxChaseFlags open_tree_flags =
+        (flags & (GLNX_CHASE_NOFOLLOW | GLNX_CHASE_ALL_DEBUG_FLAGS));
+
+      return chase_open_tree (dirfd, path, open_tree_flags, error);
+    }
+
+  no_automount = (flags & GLNX_CHASE_NO_AUTOMOUNT) != 0 ? AT_NO_AUTOMOUNT : 0;
+
+  is_absolute = g_path_is_absolute (path);
+
+  if (is_absolute && (flags & GLNX_CHASE_RESOLVE_BENEATH) != 0)
+    {
+      /* Absolute paths always get rejected with RESOLVE_BENEATH with errno
+       * EXDEV */
+
+      errno = EXDEV;
+      return glnx_fd_throw_errno_prefix (error, "absolute path not allowed for RESOLVE_BENEATH");
+    }
+  else if (!is_absolute ||
+           (is_absolute && (flags & GLNX_CHASE_RESOLVE_IN_ROOT) != 0))
+    {
+      /* The absolute path is relative to dirfd with GLNX_CHASE_RESOLVE_IN_ROOT,
+       * and a relative path is always relative. */
+
+      /* In both cases we use dirfd as our chase root */
+      if (dirfd == AT_FDCWD)
+        {
+          owned_root_fd = root_fd = open_cwd (flags, error);
+          if (root_fd < 0)
+            return -1;
+        }
+      else
+        {
+          root_fd = dirfd;
+        }
+    }
+  else
+    {
+      /* For absolute paths, we ignore dirfd, we use the actual root / for our
+       * chase root */
+      g_assert (is_absolute);
+
+      owned_root_fd = root_fd = open_root (flags, error);
+      if (root_fd < 0)
+        return -1;
+    }
+
+  /* At this point, we always have (a relative) path, relative to root_fd */
+  is_absolute = FALSE;
+  g_assert (root_fd >= 0);
+
+  /* Add root to path_st, so we can verify if we get back to it */
+  if (!glnx_chase_statx (root_fd, no_automount, &st, error))
+    return -1;
+
+  glnx_statx_queue_push (&path_st, &st);
+
+  /* Let's start walking the path! */
+  buffer = g_strdup (path);
+  remaining = buffer;
+  fd = root_fd;
+
+  for (;;)
+    {
+      g_autofree char *segment = NULL;
+      gboolean is_last;
+      glnx_autofd int next_fd = -1;
+
+      segment = extract_next_segment (&remaining, &is_last);
+
+      /* If we encounter an empty segment ("", "."), we stay where we are and
+       * ignore the segment, or just exit if it is the last segment. */
+      if (g_strcmp0 (segment, "") == 0 || g_strcmp0 (segment, ".") == 0)
+        {
+          if (is_last)
+            break;
+          continue;
+        }
+
+      /* Special handling for going down the tree with RESOLVE_ flags */
+      if (g_strcmp0 (segment, "..") == 0)
+        {
+          /* path_st contains the stat of the root if we're at root, so the
+           * length is 1 in that case, and going lower than the root is not
+           * allowed here! */
+
+          if (path_st.length <= 1 && (flags & GLNX_CHASE_RESOLVE_BENEATH) != 0)
+            {
+              /* With RESOLVE_BENEATH, error out if we would end up above the
+               * root fd */
+              errno = EXDEV;
+              return glnx_fd_throw_errno_prefix (error, "attempted to traverse above root path via \"..\"");
+            }
+          else if (path_st.length <= 1 && (flags & GLNX_CHASE_RESOLVE_IN_ROOT) != 0)
+            {
+              /* With RESOLVE_IN_ROOT, we pretend that we hit the real root,
+               * and stay there, just like the kernel does. */
+              continue;
+            }
+        }
+
+      {
+        /* Open the next segment. We always use GLNX_CHASE_NOFOLLOW here to be
+         * able to ensure the RESOLVE flags, and automount behavior. */
+
+        GlnxChaseFlags open_tree_flags =
+          GLNX_CHASE_NOFOLLOW |
+          (flags & (GLNX_CHASE_NO_AUTOMOUNT | GLNX_CHASE_ALL_DEBUG_FLAGS));
+
+        next_fd = chase_open_tree (fd, segment, open_tree_flags, error);
+        if (next_fd < 0)
+          return -1;
+      }
+
+      if (!glnx_chase_statx (next_fd, no_automount, &st, error))
+        return -1;
+
+      /* We resolve links if: they are not in the last component, or if they
+       * are the last component and NOFOLLOW is not set. */
+      if (S_ISLNK (st.stx_mode) &&
+          (!is_last || (flags & GLNX_CHASE_NOFOLLOW) == 0))
+        {
+          g_autofree char *link = NULL;
+          g_autofree char *new_buffer = NULL;
+
+          /* ...however, we do not resolve symlinks with NO_SYMLINKS, and use
+           * remaining_follows to ensure we don't loop forever. */
+          if ((flags & GLNX_CHASE_RESOLVE_NO_SYMLINKS) != 0 ||
+              --remaining_follows <= 0)
+            {
+              errno = ELOOP;
+              return glnx_fd_throw_errno_prefix (error, "followed too many symlinks");
+            }
+
+          /* AT_EMPTY_PATH is implied for readlinkat */
+          link = glnx_readlinkat_malloc (next_fd, "", NULL, error);
+          if (!link)
+            return -1;
+
+          if (g_path_is_absolute (link) &&
+              (flags & GLNX_CHASE_RESOLVE_BENEATH) != 0)
+            {
+              errno = EXDEV;
+              return glnx_fd_throw_errno_prefix (error, "absolute symlink not allowed for RESOLVE_BENEATH");
+            }
+
+          /* The link can be absolute, and we handle that below, by changing the
+           * dirfd. The path *remains* and absolute path internally, but that is
+           * okay because we always interpret any path (even absolute ones) as
+           * being relative to the dirfd */
+          new_buffer = g_strdup_printf ("%s/%s", link, remaining);
+          g_clear_pointer (&buffer, g_free);
+          buffer = g_steal_pointer (&new_buffer);
+          remaining = buffer;
+
+          if (g_path_is_absolute (link))
+            {
+              if ((flags & GLNX_CHASE_RESOLVE_IN_ROOT) != 0)
+                {
+                  /* If the path was absolute, and RESOLVE_IN_ROOT is set, we
+                   * will resolve the remaining path relative to root_fd */
+
+                  g_clear_fd (&owned_fd, NULL);
+                  fd = root_fd;
+                }
+              else
+                {
+                  /* If the path was absolute, we will resolve the remaining
+                   * path relative to the real root */
+
+                  g_clear_fd (&owned_fd, NULL);
+                  fd = owned_fd = open_root (flags, error);
+                  if (fd < 0)
+                    return -1;
+                }
+
+              /* path_st must only contain the new root at this point */
+              if (!glnx_chase_statx (fd, no_automount, &st, error))
+                return -1;
+
+              glnx_statx_queue_free (&path_st);
+              g_queue_init (&path_st);
+              glnx_statx_queue_push (&path_st, &st);
+            }
+
+          continue;
+        }
+
+      /* Either adds an element to path_st or removes one if we got down the
+       * tree. This also checks that going down the tree ends up at the inode
+       * we saw before (if we saw it before). */
+      if (g_strcmp0 (segment, "..") == 0)
+        {
+          g_autofree struct glnx_statx *old_tail = NULL;
+          struct glnx_statx *lower_st;
+
+          old_tail = g_queue_pop_tail (&path_st);
+
+          lower_st = g_queue_peek_tail (&path_st);
+          if (lower_st &&
+              (!glnx_statx_mount_same (&st, lower_st) ||
+               !glnx_statx_inode_same (&st, lower_st)))
+            {
+              errno = EXDEV;
+              return glnx_fd_throw_errno_prefix (error, "a parent directory changed while traversing");
+            }
+        }
+      else
+        {
+          glnx_statx_queue_push (&path_st, &st);
+        }
+
+      /* There is still another path component, but the next fd is not a
+       * a directory. We need the fd to be a directory though, to open the next
+       * segment from. So bail with the appropriate error. */
+      if (!is_last && !S_ISDIR (st.stx_mode))
+        {
+          errno = ENOTDIR;
+          return glnx_fd_throw_errno_prefix (error, "a non-final path segment is not a directory");
+        }
+
+      g_clear_fd (&owned_fd, NULL);
+      fd = owned_fd = g_steal_fd (&next_fd);
+
+      if (is_last)
+        break;
+    }
+
+  /* We need an owned fd to return. Only having fd and not owned_fd can happen
+   * if we never finished a single iteration, or if an absolute path with
+   * RESOLVE_IN_ROOT makes us point at root_fd.
+   * We just re-open fd to always get an owned fd.
+   * Note that this only works because in all cases where owned_fd does not
+   * exists, fd is a directory. */
+  if (owned_fd < 0)
+    {
+      owned_fd = openat (fd, ".", O_PATH | O_CLOEXEC | O_NOFOLLOW);
+      if (owned_fd < 0)
+        return glnx_fd_throw_errno_prefix (error, "reopening failed");
+    }
+
+  return g_steal_fd (&owned_fd);
+}
+
+/**
+ * glnx_chaseat:
+ * @dirfd: a directory file descriptor
+ * @path: a path
+ * @flags: combination of GlnxChaseFlags flags
+ * @error: a #GError
+ *
+ * Behaves similar to openat, but with a number of differences:
+ *
+ * - All file descriptors which get returned are O_PATH and O_CLOEXEC. If you
+ *   want to actually open the file for reading or writing, use glnx_fd_reopen,
+ *   openat, or other at-style functions.
+ * - By default, automounts get triggered and the O_PATH fd will point to inodes
+ *   in the newly mounted filesystem if an automount is encountered. This can be
+ *   turned off with GLNX_CHASE_NO_AUTOMOUNT.
+ * - The GLNX_CHASE_RESOLVE_ flags can be used to safely deal with symlinks.
+ *
+ * Returns: the chased file, or -1 with @error set on error
+ */
+int
+glnx_chaseat (int              dirfd,
+              const char      *path,
+              GlnxChaseFlags   flags,
+              GError         **error)
+{
+  static gboolean can_openat2 = TRUE;
+  glnx_autofd int fd = -1;
+
+  g_return_val_if_fail (dirfd >= 0 || dirfd == AT_FDCWD, -1);
+  g_return_val_if_fail (path != NULL, -1);
+  g_return_val_if_fail ((flags & ~(GLNX_CHASE_ALL_FLAGS)) == 0, -1);
+  g_return_val_if_fail (error == NULL || *error == NULL, -1);
+
+  {
+    int must_flags = flags & (GLNX_CHASE_MUST_BE_REGULAR |
+                              GLNX_CHASE_MUST_BE_DIRECTORY |
+                              GLNX_CHASE_MUST_BE_SOCKET);
+    /* check that no more than one bit is set (= power of two) */
+    g_return_val_if_fail ((must_flags & (must_flags - 1)) == 0, -1);
+  }
+
+  /* TODO: Add a callback which is called for every resolved path segment, to
+   * allow users to verify and expand the functionality safely. */
+
+  /* We need the manual impl for NO_AUTOMOUNT, and we can skip this, if we don't
+   * have openat2 at all.
+   * Technically racy (static, not synced), but both paths work fine so it
+   * doesn't matter. */
+  if (can_openat2 && (flags & GLNX_CHASE_NO_AUTOMOUNT) == 0 &&
+      (flags & GLNX_CHASE_DEBUG_NO_OPENAT2) == 0)
+    {
+      uint64_t openat2_flags = 0;
+      uint64_t openat2_resolve = 0;
+      struct open_how how;
+
+      openat2_flags = O_PATH | O_CLOEXEC;
+      if ((flags & GLNX_CHASE_NOFOLLOW) != 0)
+        openat2_flags |= O_NOFOLLOW;
+
+      openat2_resolve |= RESOLVE_NO_MAGICLINKS;
+      if ((flags & GLNX_CHASE_RESOLVE_BENEATH) != 0)
+        openat2_resolve |= RESOLVE_BENEATH;
+      if ((flags & GLNX_CHASE_RESOLVE_IN_ROOT) != 0)
+        openat2_resolve |= RESOLVE_IN_ROOT;
+      if ((flags & GLNX_CHASE_RESOLVE_NO_SYMLINKS) != 0)
+        openat2_resolve |= RESOLVE_NO_SYMLINKS;
+
+      how = (struct open_how) {
+        .flags = openat2_flags,
+        .mode = 0,
+        .resolve = openat2_resolve,
+      };
+
+      fd = openat2 (dirfd, path, &how, sizeof (how));
+      if (fd < 0)
+        {
+          /* If the syscall is not implemented (ENOSYS) or blocked by
+           * seccomp (EPERM), we need to fall back to the manual path chasing
+           * via open_tree. */
+          if (G_IN_SET (errno, ENOSYS, EPERM))
+            can_openat2 = FALSE;
+          else
+            return glnx_fd_throw_errno (error);
+        }
+    }
+
+  if (fd < 0)
+    {
+      fd = chase_manual (dirfd, path, flags, error);
+      if (fd < 0)
+        return -1;
+    }
+
+  if ((flags & (GLNX_CHASE_MUST_BE_REGULAR |
+                GLNX_CHASE_MUST_BE_DIRECTORY |
+                GLNX_CHASE_MUST_BE_SOCKET)) != 0)
+    {
+      struct glnx_statx st;
+
+      if (!glnx_statx (fd, "",
+                       AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW |
+                       ((flags & GLNX_CHASE_NO_AUTOMOUNT) ? AT_NO_AUTOMOUNT : 0),
+                       GLNX_STATX_TYPE,
+                       &st,
+                       error))
+        return -1;
+
+      if ((st.stx_mask & GLNX_STATX_TYPE) == 0)
+        {
+          errno = ENODATA;
+          return glnx_fd_throw_errno_prefix (error, "unable to get file type");
+        }
+
+      if ((flags & GLNX_CHASE_MUST_BE_REGULAR) != 0 &&
+          !S_ISREG (st.stx_mode))
+        {
+          if (S_ISDIR (st.stx_mode))
+            errno = EISDIR;
+          else
+            errno = EBADFD;
+
+          return glnx_fd_throw_errno_prefix (error, "not a regular file");
+        }
+
+      if ((flags & GLNX_CHASE_MUST_BE_DIRECTORY) != 0 &&
+          !S_ISDIR (st.stx_mode))
+        {
+          errno = ENOTDIR;
+          return glnx_fd_throw_errno_prefix (error, "not a directory");
+        }
+
+      if ((flags & GLNX_CHASE_MUST_BE_SOCKET) != 0 &&
+          !S_ISSOCK (st.stx_mode))
+        {
+          errno = ENOTSOCK;
+          return glnx_fd_throw_errno_prefix (error, "not a socket");
+        }
+    }
+
+  return g_steal_fd (&fd);
+}
+
+/**
+ * glnx_chase_and_statxat:
+ * @dirfd: a directory file descriptor
+ * @path: a path
+ * @flags: combination of GlnxChaseFlags flags
+ * @mask: combination of GLNX_STATX_ flags
+ * @statbuf: a pointer to a struct glnx_statx which will be filled out
+ * @error: a #GError
+ *
+ * Stats the file at @path relative to @dirfd and fills out @statbuf with the
+ * result according to the interest mask @mask.
+ *
+ * See glnx_chaseat for the meaning of @dirfd, @path, and @flags.
+ *
+ * Returns: the chased file, or -1 with @error set on error
+ */
+int
+glnx_chase_and_statxat (int                 dirfd,
+                        const char         *path,
+                        GlnxChaseFlags      flags,
+                        unsigned int        mask,
+                        struct glnx_statx  *statbuf,
+                        GError            **error)
+{
+  glnx_autofd int fd = -1;
+
+  /* other args are checked by glnx_chaseat */
+  g_return_val_if_fail (statbuf != NULL, FALSE);
+
+  fd = glnx_chaseat (dirfd, path, flags, error);
+  if (fd < 0)
+    return -1;
+
+  if (!glnx_statx (fd, "",
+                   AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW |
+                   ((flags & GLNX_CHASE_NO_AUTOMOUNT) ? AT_NO_AUTOMOUNT : 0),
+                   mask,
+                   statbuf,
+                   error))
+    return -1;
+
+  return g_steal_fd (&fd);
+}
diff -Nru flatpak-1.16.3/subprojects/libglnx/glnx-chase.h flatpak-1.16.6/subprojects/libglnx/glnx-chase.h
--- flatpak-1.16.3/subprojects/libglnx/glnx-chase.h	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/subprojects/libglnx/glnx-chase.h	2026-04-10 18:28:42.000000000 +0100
@@ -0,0 +1,51 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2026 Red Hat, Inc.
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include <glib.h>
+
+typedef enum _GlnxChaseFlags {
+  /* Default */
+  GLNX_CHASE_DEFAULT = 0,
+  /* Disable triggering of automounts */
+  GLNX_CHASE_NO_AUTOMOUNT = 1 << 1,
+  /* Do not follow the path's right-most component. When the path's right-most
+   * component refers to symlink, return O_PATH fd of the symlink. */
+  GLNX_CHASE_NOFOLLOW = 1 << 2,
+  /* Do not permit the path resolution to succeed if any component of the
+   * resolution is not a descendant of the directory indicated by dirfd. */
+  GLNX_CHASE_RESOLVE_BENEATH = 1 << 3,
+  /* Symlinks are resolved relative to the given dirfd instead of root. */
+  GLNX_CHASE_RESOLVE_IN_ROOT = 1 << 4,
+  /* Fail if any symlink is encountered. */
+  GLNX_CHASE_RESOLVE_NO_SYMLINKS = 1 << 5,
+  /* Fail if the path's right-most component is not a regular file */
+  GLNX_CHASE_MUST_BE_REGULAR = 1 << 6,
+  /* Fail if the path's right-most component is not a directory */
+  GLNX_CHASE_MUST_BE_DIRECTORY = 1 << 7,
+  /* Fail if the path's right-most component is not a socket */
+  GLNX_CHASE_MUST_BE_SOCKET = 1 << 8,
+} GlnxChaseFlags;
+
+/* How many iterations to execute before returning ELOOP */
+#define GLNX_CHASE_MAX 32
+
+G_BEGIN_DECLS
+
+int glnx_chaseat (int              dirfd,
+                  const char      *path,
+                  GlnxChaseFlags   flags,
+                  GError         **error);
+
+int glnx_chase_and_statxat (int                 dirfd,
+                            const char         *path,
+                            GlnxChaseFlags      flags,
+                            unsigned int        mask,
+                            struct glnx_statx  *statbuf,
+                            GError            **error);
+
+G_END_DECLS
diff -Nru flatpak-1.16.3/subprojects/libglnx/glnx-errors.h flatpak-1.16.6/subprojects/libglnx/glnx-errors.h
--- flatpak-1.16.3/subprojects/libglnx/glnx-errors.h	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/subprojects/libglnx/glnx-errors.h	2026-04-10 18:28:42.000000000 +0100
@@ -32,6 +32,10 @@
 #define glnx_null_throw(error, args...) \
   ({glnx_throw (error, args); NULL;})
 
+/* Like glnx_throw(), but yields -1 (invalid fd). */
+#define glnx_fd_throw(error, args...) \
+  ({glnx_throw (error, args); -1;})
+
 /* Implementation detail of glnx_throw_prefix() */
 void glnx_real_set_prefix_error_va (GError     *error,
                                     const char *format,
@@ -97,7 +101,7 @@
                        g_strerror (errsv));
   /* We also restore the value of errno, since that's
    * what was done in a long-ago libgsystem commit
-   * https://git.gnome.org/browse/libgsystem/commit/?id=ed106741f7a0596dc8b960b31fdae671d31d666d
+   * https://gitlab.gnome.org/Archive/libgsystem/-/commit/ed106741f7a0596dc8b960b31fdae671d31d666d
    * but I certainly can't remember now why I did that.
    */
   errno = errsv;
@@ -108,6 +112,10 @@
 #define glnx_null_throw_errno(error) \
   ({glnx_throw_errno (error); NULL;})
 
+/* Like glnx_throw_errno(), but yields -1 (invalid fd). */
+#define glnx_fd_throw_errno(error) \
+  ({glnx_throw_errno (error); -1;})
+
 /* Implementation detail of glnx_throw_errno_prefix() */
 void glnx_real_set_prefix_error_from_errno_va (GError     **error,
                                                gint         errsv,
@@ -120,6 +128,10 @@
 #define glnx_null_throw_errno_prefix(error, args...) \
   ({glnx_throw_errno_prefix (error, args); NULL;})
 
+/* Like glnx_throw_errno_prefix(), but yields -1 (invalid fd). */
+#define glnx_fd_throw_errno_prefix(error, args...) \
+  ({glnx_throw_errno_prefix (error, args); -1;})
+
 /* BEGIN LEGACY APIS */
 
 #define glnx_set_error_from_errno(error)                \
diff -Nru flatpak-1.16.3/subprojects/libglnx/glnx-fdio.c flatpak-1.16.6/subprojects/libglnx/glnx-fdio.c
--- flatpak-1.16.3/subprojects/libglnx/glnx-fdio.c	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/subprojects/libglnx/glnx-fdio.c	2026-04-10 18:28:42.000000000 +0100
@@ -321,6 +321,8 @@
                                            error);
 }
 
+static const char proc_self_fd_slash[] = "/proc/self/fd/";
+
 /* Use this after calling glnx_open_tmpfile_linkable_at() to give
  * the file its final name (link into place).
  */
@@ -367,8 +369,8 @@
   else
     {
       /* This case we have O_TMPFILE, so our reference to it is via /proc/self/fd */
-      char proc_fd_path[strlen("/proc/self/fd/") + DECIMAL_STR_MAX(tmpf->fd) + 1];
-      snprintf (proc_fd_path, sizeof (proc_fd_path), "/proc/self/fd/%i", tmpf->fd);
+      char proc_fd_path[sizeof (proc_self_fd_slash) + DECIMAL_STR_MAX(tmpf->fd)];
+      snprintf (proc_fd_path, sizeof (proc_fd_path), "%s%i", proc_self_fd_slash, tmpf->fd);
 
       if (replace)
         {
@@ -455,8 +457,8 @@
   else
     {
       /* This case we have O_TMPFILE, so our reference to it is via /proc/self/fd */
-      char proc_fd_path[strlen("/proc/self/fd/") + DECIMAL_STR_MAX(tmpf->fd) + 1];
-      snprintf (proc_fd_path, sizeof (proc_fd_path), "/proc/self/fd/%i", tmpf->fd);
+      char proc_fd_path[sizeof (proc_self_fd_slash) + DECIMAL_STR_MAX(tmpf->fd)];
+      snprintf (proc_fd_path, sizeof (proc_fd_path), "%s%i", proc_self_fd_slash, tmpf->fd);
 
       if (!glnx_openat_rdonly (AT_FDCWD, proc_fd_path, TRUE, &rdonly_fd, error))
         return FALSE;
@@ -1205,3 +1207,75 @@
 
   return TRUE;
 }
+
+/**
+ * glnx_fd_reopen:
+ * @fd: a file descriptor
+ * @flags: combination of openat flags
+ * @error: a #GError
+ *
+ * Reopens the specified fd with new flags. This is useful for converting an
+ * O_PATH fd into a regular one, or to turn O_RDWR fds into O_RDONLY fds.
+ *
+ * This implicitly sets `O_CLOEXEC | O_NOCTTY` in @flags.
+ *
+ * `O_CREAT` isn't allowed in @flags.
+ *
+ * This doesn't work on sockets (since they cannot be open()ed, ever).
+ *
+ * This implicitly resets the file read index to 0.
+ *
+ * If AT_FDCWD is specified as file descriptor, the function returns an fd to
+ * the current working directory.
+ *
+ * If the specified file descriptor refers to a symlink via O_PATH, then this
+ * function cannot be used to follow that symlink. Because we cannot have
+ * non-O_PATH fds to symlinks reopening it without O_PATH will always result in
+ * ELOOP. Or in other words: if you have an O_PATH fd to a symlink you can
+ * reopen it only if you pass O_PATH again.
+ */
+int
+glnx_fd_reopen (int      fd,
+                int      flags,
+                GError **error)
+{
+  glnx_autofd int new_fd = -1;
+
+  g_return_val_if_fail (fd >= 0 || fd == AT_FDCWD, -1);
+  g_return_val_if_fail ((flags & O_CREAT) == 0, -1);
+
+  /* */
+  flags |= O_CLOEXEC | O_NOCTTY;
+
+  /* O_NOFOLLOW is not allowed in fd_reopen(), because after all this is
+   * primarily implemented via a symlink-based interface in /proc/self/fd. Let's
+   * refuse this here early. Note that the kernel would generate ELOOP here too,
+   * hence this manual check is mostly redundant – the only reason we add it
+   * here is so that the O_DIRECTORY special case (see below) behaves the same
+   * way as the non-O_DIRECTORY case. */
+  if ((flags & O_NOFOLLOW) != 0)
+    {
+      errno = ELOOP;
+      return glnx_fd_throw_errno (error);
+    }
+
+  if ((flags & O_DIRECTORY) != 0 || fd == AT_FDCWD)
+    {
+      /* If we shall reopen the fd as directory we can just go via "." and thus
+       * bypass the whole magic /proc/ directory, and make ourselves independent
+       * of that being mounted. */
+      new_fd = openat (fd, ".", flags | O_DIRECTORY);
+    }
+  else
+    {
+      g_autofree char *proc_fd_path = NULL;
+
+      proc_fd_path = g_strdup_printf ("/proc/self/fd/%d", fd);
+      new_fd = open (proc_fd_path, flags);
+    }
+
+  if (new_fd < 0)
+    return glnx_fd_throw_errno (error);
+
+  return g_steal_fd (&new_fd);
+}
diff -Nru flatpak-1.16.3/subprojects/libglnx/glnx-fdio.h flatpak-1.16.6/subprojects/libglnx/glnx-fdio.h
--- flatpak-1.16.3/subprojects/libglnx/glnx-fdio.h	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/subprojects/libglnx/glnx-fdio.h	2026-04-10 18:28:42.000000000 +0100
@@ -22,6 +22,7 @@
 #pragma once
 
 #include <glnx-backport-autocleanups.h>
+#include <glnx-missing.h>
 #include <gio/gfiledescriptorbased.h>
 #include <limits.h>
 #include <dirent.h>
@@ -314,6 +315,37 @@
 }
 
 /**
+ * glnx_statx:
+ * @dfd: Directory FD to stat beneath
+ * @path: Path to stat beneath @dfd
+ * @flags: Flags to pass to statx()
+ * @mask: Mask to pass to statx()
+ * @buf: (out caller-allocates): Return location for statx details
+ * @error: Return location for a #GError, or %NULL
+ *
+ * Wrapper around statx() which adds #GError support and ensures that it
+ * retries on %EINTR.
+ *
+ * The mask to pass must be a combination of GLNX_STATX_* flags which are
+ * defined by glnx, which map up with the struct glnx_statx.
+ *
+ * Returns: %TRUE on success, or %FALSE setting both @error and `errno`
+ * Since: UNRELEASED
+ */
+static inline gboolean
+glnx_statx (int                 dfd,
+            const char         *path,
+            unsigned            flags,
+            unsigned int        mask,
+            struct glnx_statx  *buf,
+            GError            **error)
+{
+  if (TEMP_FAILURE_RETRY (glnx_statx_syscall (dfd, path, flags, mask, buf)) != 0)
+    return glnx_throw_errno_prefix (error, "statx(%s)", path);
+  return TRUE;
+}
+
+/**
  * glnx_fstatat_allow_noent:
  * @dfd: Directory FD to stat beneath
  * @path: Path to stat beneath @dfd
@@ -383,4 +415,8 @@
   return TRUE;
 }
 
+int glnx_fd_reopen (int      fd,
+                    int      flags,
+                    GError **error);
+
 G_END_DECLS
diff -Nru flatpak-1.16.3/subprojects/libglnx/glnx-local-alloc.h flatpak-1.16.6/subprojects/libglnx/glnx-local-alloc.h
--- flatpak-1.16.3/subprojects/libglnx/glnx-local-alloc.h	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/subprojects/libglnx/glnx-local-alloc.h	2026-04-10 18:28:42.000000000 +0100
@@ -43,7 +43,6 @@
   if (o)
     g_object_unref (o);
 }
-#define glnx_unref_object __attribute__ ((cleanup(glnx_local_obj_unref)))
 
 /* Backwards-compat with older libglnx */
 #define glnx_steal_fd g_steal_fd
diff -Nru flatpak-1.16.3/subprojects/libglnx/glnx-lockfile.c flatpak-1.16.6/subprojects/libglnx/glnx-lockfile.c
--- flatpak-1.16.3/subprojects/libglnx/glnx-lockfile.c	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/subprojects/libglnx/glnx-lockfile.c	2026-04-10 18:28:42.000000000 +0100
@@ -66,6 +66,8 @@
         g_autofree char *t = NULL;
         int r;
 
+        g_return_val_if_fail (p != NULL, FALSE);
+
         /*
          * We use UNPOSIX locks if they are available. They have nice
          * semantics, and are mostly compatible with NFS. However,
diff -Nru flatpak-1.16.3/subprojects/libglnx/glnx-missing-syscall.h flatpak-1.16.6/subprojects/libglnx/glnx-missing-syscall.h
--- flatpak-1.16.3/subprojects/libglnx/glnx-missing-syscall.h	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/subprojects/libglnx/glnx-missing-syscall.h	2026-04-10 18:28:42.000000000 +0100
@@ -33,6 +33,7 @@
 
 #include "libglnx-config.h"
 #include <glib.h>
+#include <stdint.h>
 
 #if !HAVE_DECL_RENAMEAT2
 #  ifndef __NR_renameat2
@@ -236,3 +237,355 @@
 #define close_range(low, high, flags) inline_close_range(low, high, flags)
 #define HAVE_CLOSE_RANGE
 #endif
+
+#ifndef __IGNORE_statx
+#  if defined(__aarch64__)
+#    define systemd_NR_statx 291
+#  elif defined(__alpha__)
+#    define systemd_NR_statx 522
+#  elif defined(__arc__) || defined(__tilegx__)
+#    define systemd_NR_statx 291
+#  elif defined(__arm__)
+#    define systemd_NR_statx 397
+#  elif defined(__i386__)
+#    define systemd_NR_statx 383
+#  elif defined(__ia64__)
+#    define systemd_NR_statx 1350
+#  elif defined(__loongarch_lp64)
+#    define systemd_NR_statx 291
+#  elif defined(__m68k__)
+#    define systemd_NR_statx 379
+#  elif defined(_MIPS_SIM)
+#    if _MIPS_SIM == _MIPS_SIM_ABI32
+#      define systemd_NR_statx 4366
+#    elif _MIPS_SIM == _MIPS_SIM_NABI32
+#      define systemd_NR_statx 6330
+#    elif _MIPS_SIM == _MIPS_SIM_ABI64
+#      define systemd_NR_statx 5326
+#    else
+#      error "Unknown MIPS ABI"
+#    endif
+#  elif defined(__hppa__)
+#    define systemd_NR_statx 349
+#  elif defined(__powerpc__)
+#    define systemd_NR_statx 383
+#  elif defined(__riscv)
+#    if __riscv_xlen == 32
+#      define systemd_NR_statx 291
+#    elif __riscv_xlen == 64
+#      define systemd_NR_statx 291
+#    else
+#      error "Unknown RISC-V ABI"
+#    endif
+#  elif defined(__s390__)
+#    define systemd_NR_statx 379
+#  elif defined(__sparc__)
+#    define systemd_NR_statx 360
+#  elif defined(__x86_64__)
+#    if defined(__ILP32__)
+#      define systemd_NR_statx (332 | /* __X32_SYSCALL_BIT */ 0x40000000)
+#    else
+#      define systemd_NR_statx 332
+#    endif
+#  elif !defined(missing_arch_template)
+#    warning "statx() syscall number is unknown for your architecture"
+#  endif
+
+/* may be an (invalid) negative number due to libseccomp, see PR 13319 */
+#  if defined __NR_statx && __NR_statx >= 0
+#    if defined systemd_NR_statx
+G_STATIC_ASSERT (__NR_statx == systemd_NR_statx);
+#    endif
+#  else
+#    if defined __NR_statx
+#      undef __NR_statx
+#    endif
+#    if defined systemd_NR_statx && systemd_NR_statx >= 0
+#      define __NR_statx systemd_NR_statx
+#    endif
+#  endif
+#endif
+
+#if !defined(HAVE_GLNX_STATX) && defined(__NR_statx)
+#define GLNX_STATX_TYPE              0x00000001U     /* Want/got stx_mode & S_IFMT */
+#define GLNX_STATX_MODE              0x00000002U     /* Want/got stx_mode & ~S_IFMT */
+#define GLNX_STATX_NLINK             0x00000004U     /* Want/got stx_nlink */
+#define GLNX_STATX_UID               0x00000008U     /* Want/got stx_uid */
+#define GLNX_STATX_GID               0x00000010U     /* Want/got stx_gid */
+#define GLNX_STATX_ATIME             0x00000020U     /* Want/got stx_atime */
+#define GLNX_STATX_MTIME             0x00000040U     /* Want/got stx_mtime */
+#define GLNX_STATX_CTIME             0x00000080U     /* Want/got stx_ctime */
+#define GLNX_STATX_INO               0x00000100U     /* Want/got stx_ino */
+#define GLNX_STATX_SIZE              0x00000200U     /* Want/got stx_size */
+#define GLNX_STATX_BLOCKS            0x00000400U     /* Want/got stx_blocks */
+#define GLNX_STATX_BASIC_STATS       0x000007ffU     /* The stuff in the normal stat struct */
+#define GLNX_STATX_BTIME             0x00000800U     /* Want/got stx_btime */
+#define GLNX_STATX_MNT_ID            0x00001000U     /* Got stx_mnt_id */
+#define GLNX_STATX_DIOALIGN          0x00002000U     /* Want/got direct I/O alignment info */
+#define GLNX_STATX_MNT_ID_UNIQUE     0x00004000U     /* Want/got extended stx_mount_id */
+#define GLNX_STATX_SUBVOL            0x00008000U     /* Want/got stx_subvol */
+#define GLNX_STATX_WRITE_ATOMIC      0x00010000U     /* Want/got atomic_write_* fields */
+#define GLNX_STATX_DIO_READ_ALIGN    0x00020000U     /* Want/got dio read alignment info */
+#define GLNX_STATX__RESERVED         0x80000000U     /* Reserved for future struct statx expansion */
+
+struct glnx_statx_timestamp
+{
+  int64_t tv_sec;
+  uint32_t tv_nsec;
+  int32_t __reserved;
+};
+
+struct glnx_statx
+{
+  uint32_t stx_mask;
+  uint32_t stx_blksize;
+  uint64_t stx_attributes;
+  uint32_t stx_nlink;
+  uint32_t stx_uid;
+  uint32_t stx_gid;
+  uint16_t stx_mode;
+  uint16_t __spare0[1];
+  uint64_t stx_ino;
+  uint64_t stx_size;
+  uint64_t stx_blocks;
+  uint64_t stx_attributes_mask;
+  struct glnx_statx_timestamp stx_atime;
+  struct glnx_statx_timestamp stx_btime;
+  struct glnx_statx_timestamp stx_ctime;
+  struct glnx_statx_timestamp stx_mtime;
+  uint32_t stx_rdev_major;
+  uint32_t stx_rdev_minor;
+  uint32_t stx_dev_major;
+  uint32_t stx_dev_minor;
+  uint64_t stx_mnt_id;
+  uint32_t stx_dio_mem_align;
+  uint32_t stx_dio_offset_align;
+  uint64_t stx_subvol;
+  uint32_t stx_atomic_write_unit_min;
+  uint32_t stx_atomic_write_unit_max;
+  uint32_t stx_atomic_write_segments_max;
+  uint32_t stx_dio_read_offset_align;
+  uint32_t stx_atomic_write_unit_max_opt;
+  uint32_t	__spare2[1];
+  uint64_t	__spare3[8];
+};
+
+static inline int
+glnx_statx_syscall (int                dfd,
+                    const char        *filename,
+                    unsigned           flags,
+                    unsigned int       mask,
+                    struct glnx_statx *buf)
+{
+	memset (buf, 0xbf, sizeof (*buf));
+	return syscall (__NR_statx, dfd, filename, flags, mask, buf);
+  return 0;
+}
+
+#define HAVE_GLNX_STATX
+#endif
+
+/* Copied from systemd git: ff83795469 ("boot: Improve log message")
+ * - open_tree
+ * - openat2
+ */
+
+#ifndef __IGNORE_open_tree
+#  if defined(__aarch64__)
+#    define systemd_NR_open_tree 428
+#  elif defined(__alpha__)
+#    define systemd_NR_open_tree 538
+#  elif defined(__arc__) || defined(__tilegx__)
+#    define systemd_NR_open_tree 428
+#  elif defined(__arm__)
+#    define systemd_NR_open_tree 428
+#  elif defined(__i386__)
+#    define systemd_NR_open_tree 428
+#  elif defined(__ia64__)
+#    define systemd_NR_open_tree 1452
+#  elif defined(__loongarch_lp64)
+#    define systemd_NR_open_tree 428
+#  elif defined(__m68k__)
+#    define systemd_NR_open_tree 428
+#  elif defined(_MIPS_SIM)
+#    if _MIPS_SIM == _MIPS_SIM_ABI32
+#      define systemd_NR_open_tree 4428
+#    elif _MIPS_SIM == _MIPS_SIM_NABI32
+#      define systemd_NR_open_tree 6428
+#    elif _MIPS_SIM == _MIPS_SIM_ABI64
+#      define systemd_NR_open_tree 5428
+#    else
+#      error "Unknown MIPS ABI"
+#    endif
+#  elif defined(__hppa__)
+#    define systemd_NR_open_tree 428
+#  elif defined(__powerpc__)
+#    define systemd_NR_open_tree 428
+#  elif defined(__riscv)
+#    if __riscv_xlen == 32
+#      define systemd_NR_open_tree 428
+#    elif __riscv_xlen == 64
+#      define systemd_NR_open_tree 428
+#    else
+#      error "Unknown RISC-V ABI"
+#    endif
+#  elif defined(__s390__)
+#    define systemd_NR_open_tree 428
+#  elif defined(__sparc__)
+#    define systemd_NR_open_tree 428
+#  elif defined(__x86_64__)
+#    if defined(__ILP32__)
+#      define systemd_NR_open_tree (428 | /* __X32_SYSCALL_BIT */ 0x40000000)
+#    else
+#      define systemd_NR_open_tree 428
+#    endif
+#  elif !defined(missing_arch_template)
+#    warning "open_tree() syscall number is unknown for your architecture"
+#  endif
+
+/* may be an (invalid) negative number due to libseccomp, see PR 13319 */
+#  if defined __NR_open_tree && __NR_open_tree >= 0
+#    if defined systemd_NR_open_tree
+G_STATIC_ASSERT (__NR_open_tree == systemd_NR_open_tree);
+#    endif
+#  else
+#    if defined __NR_open_tree
+#      undef __NR_open_tree
+#    endif
+#    if defined systemd_NR_open_tree && systemd_NR_open_tree >= 0
+#      define __NR_open_tree systemd_NR_open_tree
+#    endif
+#  endif
+#endif
+
+#if !defined(HAVE_OPEN_TREE) && defined(__NR_open_tree)
+#ifndef OPEN_TREE_CLONE
+#define OPEN_TREE_CLONE 1
+#endif
+
+#ifndef OPEN_TREE_CLOEXEC
+#define OPEN_TREE_CLOEXEC O_CLOEXEC
+#endif
+
+static inline int
+inline_open_tree (int         dfd,
+                  const char *filename,
+                  unsigned    flags)
+{
+  return syscall(__NR_open_tree, dfd, filename, flags);
+}
+#define open_tree inline_open_tree
+#define HAVE_OPEN_TREE
+#endif
+
+#ifndef __IGNORE_openat2
+#  if defined(__aarch64__)
+#    define systemd_NR_openat2 437
+#  elif defined(__alpha__)
+#    define systemd_NR_openat2 547
+#  elif defined(__arc__) || defined(__tilegx__)
+#    define systemd_NR_openat2 437
+#  elif defined(__arm__)
+#    define systemd_NR_openat2 437
+#  elif defined(__i386__)
+#    define systemd_NR_openat2 437
+#  elif defined(__ia64__)
+#    define systemd_NR_openat2 1461
+#  elif defined(__loongarch_lp64)
+#    define systemd_NR_openat2 437
+#  elif defined(__m68k__)
+#    define systemd_NR_openat2 437
+#  elif defined(_MIPS_SIM)
+#    if _MIPS_SIM == _MIPS_SIM_ABI32
+#      define systemd_NR_openat2 4437
+#    elif _MIPS_SIM == _MIPS_SIM_NABI32
+#      define systemd_NR_openat2 6437
+#    elif _MIPS_SIM == _MIPS_SIM_ABI64
+#      define systemd_NR_openat2 5437
+#    else
+#      error "Unknown MIPS ABI"
+#    endif
+#  elif defined(__hppa__)
+#    define systemd_NR_openat2 437
+#  elif defined(__powerpc__)
+#    define systemd_NR_openat2 437
+#  elif defined(__riscv)
+#    if __riscv_xlen == 32
+#      define systemd_NR_openat2 437
+#    elif __riscv_xlen == 64
+#      define systemd_NR_openat2 437
+#    else
+#      error "Unknown RISC-V ABI"
+#    endif
+#  elif defined(__s390__)
+#    define systemd_NR_openat2 437
+#  elif defined(__sparc__)
+#    define systemd_NR_openat2 437
+#  elif defined(__x86_64__)
+#    if defined(__ILP32__)
+#      define systemd_NR_openat2 (437 | /* __X32_SYSCALL_BIT */ 0x40000000)
+#    else
+#      define systemd_NR_openat2 437
+#    endif
+#  elif !defined(missing_arch_template)
+#    warning "openat2() syscall number is unknown for your architecture"
+#  endif
+
+/* may be an (invalid) negative number due to libseccomp, see PR 13319 */
+#  if defined __NR_openat2 && __NR_openat2 >= 0
+#    if defined systemd_NR_openat2
+G_STATIC_ASSERT (__NR_openat2 == systemd_NR_openat2);
+#    endif
+#  else
+#    if defined __NR_openat2
+#      undef __NR_openat2
+#    endif
+#    if defined systemd_NR_openat2 && systemd_NR_openat2 >= 0
+#      define __NR_openat2 systemd_NR_openat2
+#    endif
+#  endif
+#endif
+
+#if !defined(HAVE_OPENAT2) && defined(__NR_openat2)
+#ifndef RESOLVE_NO_XDEV
+#define RESOLVE_NO_XDEV 0x01
+#endif
+
+#ifndef RESOLVE_NO_MAGICLINKS
+#define RESOLVE_NO_MAGICLINKS 0x02
+#endif
+
+#ifndef RESOLVE_NO_SYMLINKS
+#define RESOLVE_NO_SYMLINKS 0x04
+#endif
+
+#ifndef RESOLVE_BENEATH
+#define RESOLVE_BENEATH 0x08
+#endif
+
+#ifndef RESOLVE_IN_ROOT
+#define RESOLVE_IN_ROOT 0x10
+#endif
+
+#ifndef RESOLVE_CACHED
+#define RESOLVE_CACHED 0x20
+#endif
+
+struct inline_open_how {
+        uint64_t flags;
+        uint64_t mode;
+        uint64_t resolve;
+};
+#define open_how inline_open_how
+
+static inline int
+inline_openat2 (int         dfd,
+                const char *filename,
+                void       *buffer,
+                size_t      size)
+{
+  return syscall(__NR_openat2, dfd, filename, buffer, size);
+}
+#define openat2 inline_openat2
+#define HAVE_OPENAT2
+#endif
diff -Nru flatpak-1.16.3/subprojects/libglnx/libglnx.h flatpak-1.16.6/subprojects/libglnx/libglnx.h
--- flatpak-1.16.3/subprojects/libglnx/libglnx.h	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/subprojects/libglnx/libglnx.h	2026-04-10 18:28:42.000000000 +0100
@@ -31,6 +31,7 @@
 #include <glnx-backport-autocleanups.h>
 #include <glnx-backport-testutils.h>
 #include <glnx-backports.h>
+#include <glnx-chase.h>
 #include <glnx-lockfile.h>
 #include <glnx-errors.h>
 #include <glnx-dirfd.h>
diff -Nru flatpak-1.16.3/subprojects/libglnx/Makefile-libglnx.am flatpak-1.16.6/subprojects/libglnx/Makefile-libglnx.am
--- flatpak-1.16.3/subprojects/libglnx/Makefile-libglnx.am	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/subprojects/libglnx/Makefile-libglnx.am	2026-04-10 18:28:42.000000000 +0100
@@ -37,6 +37,8 @@
 	$(libglnx_srcpath)/glnx-backport-testutils.c \
 	$(libglnx_srcpath)/glnx-backports.h \
 	$(libglnx_srcpath)/glnx-backports.c \
+	$(libglnx_srcpath)/glnx-chase.h \
+	$(libglnx_srcpath)/glnx-chase.c \
 	$(libglnx_srcpath)/glnx-local-alloc.h \
 	$(libglnx_srcpath)/glnx-local-alloc.c \
 	$(libglnx_srcpath)/glnx-errors.h \
diff -Nru flatpak-1.16.3/subprojects/libglnx/meson.build flatpak-1.16.6/subprojects/libglnx/meson.build
--- flatpak-1.16.3/subprojects/libglnx/meson.build	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/subprojects/libglnx/meson.build	2026-04-10 18:28:42.000000000 +0100
@@ -76,6 +76,8 @@
   'glnx-backport-testutils.h',
   'glnx-backports.c',
   'glnx-backports.h',
+  'glnx-chase.c',
+  'glnx-chase.h',
   'glnx-console.c',
   'glnx-console.h',
   'glnx-dirfd.c',
diff -Nru flatpak-1.16.3/subprojects/libglnx/tests/meson.build flatpak-1.16.6/subprojects/libglnx/tests/meson.build
--- flatpak-1.16.3/subprojects/libglnx/tests/meson.build	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/subprojects/libglnx/tests/meson.build	2026-04-10 18:28:42.000000000 +0100
@@ -34,6 +34,7 @@
 
   test_names = [
     'backports',
+    'chase',
     'errors',
     'fdio',
     'macros',
diff -Nru flatpak-1.16.3/subprojects/libglnx/tests/test-libglnx-chase.c flatpak-1.16.6/subprojects/libglnx/tests/test-libglnx-chase.c
--- flatpak-1.16.3/subprojects/libglnx/tests/test-libglnx-chase.c	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/subprojects/libglnx/tests/test-libglnx-chase.c	2026-04-10 18:28:42.000000000 +0100
@@ -0,0 +1,609 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2026 Red Hat, Inc.
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#include "libglnx-config.h"
+#include "libglnx.h"
+#include <glib.h>
+#include <stdlib.h>
+#include <gio/gio.h>
+#include <err.h>
+#include <string.h>
+
+#include "libglnx-testlib.h"
+
+#define GLNX_CHASE_DEBUG_NO_OPENAT2 (1U << 31)
+#define GLNX_CHASE_DEBUG_NO_OPEN_TREE (1U << 30)
+
+const char *test_paths[] = {
+  "file/baz",
+  "file/baz/",
+  "file/baz/.",
+  "file/baz/../baz",
+  "file////baz/..//baz",
+  "file////baz/..//../file/baz",
+};
+
+static ino_t
+get_ino (int fd)
+{
+  int r;
+  struct stat st;
+
+  r = fstatat (fd, "", &st, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW);
+  g_assert_cmpint (r, >=, 0);
+
+  return st.st_ino;
+}
+
+static ino_t
+path_get_ino (const char *path)
+{
+  int r;
+  struct stat st;
+
+  r = fstatat (AT_FDCWD, path, &st, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW);
+  g_assert_cmpint (r, >=, 0);
+
+  return st.st_ino;
+}
+
+static char *
+get_abspath (int         dfd,
+             const char *path)
+{
+  g_autofree char *proc_fd_path = NULL;
+  g_autofree char *abs = NULL;
+  g_autoptr(GError) error = NULL;
+
+  proc_fd_path = g_strdup_printf ("/proc/self/fd/%d", dfd);
+  abs = glnx_readlinkat_malloc (AT_FDCWD, proc_fd_path, NULL, &error);
+  g_assert_no_error (error);
+  g_assert_nonnull (abs);
+
+  return g_strdup_printf ("%s/%s", abs, path);
+}
+
+static void
+check_chase (int             dfd,
+             const char     *path,
+             GlnxChaseFlags  flags,
+             int             expected_ino)
+{
+  g_autoptr(GError) error = NULL;
+  glnx_autofd int chase_fd = -1;
+
+  /* let's try to test the openat2 impl */
+  chase_fd = glnx_chaseat (dfd, path, flags, &error);
+  g_assert_no_error (error);
+  g_assert_cmpint (chase_fd, >=, 0);
+  g_assert_cmpint (get_ino (chase_fd), ==, expected_ino);
+  g_clear_fd (&chase_fd, NULL);
+
+  /* let's try to test the open_tree impl */
+  chase_fd = glnx_chaseat (dfd, path,
+                           flags | GLNX_CHASE_DEBUG_NO_OPENAT2,
+                           &error);
+  g_assert_no_error (error);
+  g_assert_cmpint (chase_fd, >=, 0);
+  g_assert_cmpint (get_ino (chase_fd), ==, expected_ino);
+  g_clear_fd (&chase_fd, NULL);
+
+  /* let's try to test the openat impl */
+  chase_fd = glnx_chaseat (dfd, path,
+                           flags |
+                           GLNX_CHASE_DEBUG_NO_OPENAT2 |
+                           GLNX_CHASE_DEBUG_NO_OPEN_TREE,
+                           &error);
+  g_assert_no_error (error);
+  g_assert_cmpint (chase_fd, >=, 0);
+  g_assert_cmpint (get_ino (chase_fd), ==, expected_ino);
+  g_clear_fd (&chase_fd, NULL);
+}
+
+static void
+check_chase_error (int             dfd,
+                   const char     *path,
+                   GlnxChaseFlags  flags,
+                   GQuark          err_domain,
+                   gint            err_code)
+{
+  g_autoptr(GError) error = NULL;
+  glnx_autofd int chase_fd = -1;
+
+  /* let's try to test the openat2 impl */
+  chase_fd = glnx_chaseat (dfd, path, flags, &error);
+  g_assert_cmpint (chase_fd, <, 0);
+  g_assert_error (error, err_domain, err_code);
+  g_clear_error (&error);
+
+  /* let's try to test the open_tree impl */
+  chase_fd = glnx_chaseat (dfd, path,
+                           flags | GLNX_CHASE_DEBUG_NO_OPENAT2,
+                           &error);
+  g_assert_cmpint (chase_fd, <, 0);
+  g_assert_error (error, err_domain, err_code);
+  g_clear_error (&error);
+
+  /* let's try to test the openat impl */
+  chase_fd = glnx_chaseat (dfd, path,
+                           flags |
+                           GLNX_CHASE_DEBUG_NO_OPENAT2 |
+                           GLNX_CHASE_DEBUG_NO_OPEN_TREE,
+                           &error);
+  g_assert_cmpint (chase_fd, <, 0);
+  g_assert_error (error, err_domain, err_code);
+  g_clear_error (&error);
+}
+
+static void
+test_chase_relative (void)
+{
+  g_autoptr(GError) error = NULL;
+  glnx_autofd int dfd = -1;
+  int expected_ino;
+
+  g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755,
+                                              &dfd,
+                                              NULL, &error));
+  g_assert_no_error (error);
+  g_assert_cmpint (dfd, >=, 0);
+
+  expected_ino = get_ino (dfd);
+
+  for (size_t i = 0; i < G_N_ELEMENTS (test_paths); i++)
+    check_chase (AT_FDCWD, test_paths[i], 0, expected_ino);
+
+  check_chase_error (AT_FDCWD, "nope", 0, G_IO_ERROR, G_IO_ERROR_NOT_FOUND);
+}
+
+static void
+test_chase_relative_fd (void)
+{
+  g_autoptr(GError) error = NULL;
+  glnx_autofd int dfd = -1;
+  int expected_ino;
+  glnx_autofd int cwdfd = -1;
+
+  g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755,
+                                              &dfd,
+                                              NULL, &error));
+  g_assert_no_error (error);
+  g_assert_cmpint (dfd, >=, 0);
+
+  expected_ino = get_ino (dfd);
+
+  cwdfd = openat (AT_FDCWD, ".", O_PATH | O_CLOEXEC | O_NOFOLLOW);
+  g_assert_cmpint (cwdfd, >=, 0);
+
+  for (size_t i = 0; i < G_N_ELEMENTS (test_paths); i++)
+    check_chase (cwdfd, test_paths[i], 0, expected_ino);
+
+  check_chase_error (cwdfd, "nope", 0, G_IO_ERROR, G_IO_ERROR_NOT_FOUND);
+}
+
+static void
+test_chase_absolute (void)
+{
+  g_autoptr(GError) error = NULL;
+  glnx_autofd int dfd = -1;
+  int expected_ino;
+  glnx_autofd int cwdfd = -1;
+  g_autofree char *proc_fd_path = NULL;
+  g_autofree char *cwd_path = NULL;
+
+  g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755,
+                                              &dfd,
+                                              NULL, &error));
+  g_assert_no_error (error);
+  g_assert_cmpint (dfd, >=, 0);
+
+  expected_ino = get_ino (dfd);
+
+  cwdfd = openat (AT_FDCWD, ".", O_PATH | O_CLOEXEC | O_NOFOLLOW);
+  g_assert_cmpint (cwdfd, >=, 0);
+
+  cwd_path = get_abspath (cwdfd, "");
+
+  for (size_t i = 0; i < G_N_ELEMENTS (test_paths); i++)
+    {
+      g_autofree char *abspath = NULL;
+
+      abspath = g_strdup_printf ("%s/%s", cwd_path, test_paths[i]);
+      check_chase (AT_FDCWD, abspath, 0, expected_ino);
+    }
+
+  check_chase_error (AT_FDCWD, "/nope/nope/nope/345298308497623012313243543", 0,
+                     G_IO_ERROR, G_IO_ERROR_NOT_FOUND);
+}
+
+static void
+test_chase_link (void)
+{
+  g_autoptr(GError) error = NULL;
+  glnx_autofd int dfd = -1;
+  int link_ino;
+  int target_ino;
+
+  g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755,
+                                              &dfd,
+                                              NULL, &error));
+  g_assert_no_error (error);
+  g_assert_cmpint (dfd, >=, 0);
+
+  g_assert_cmpint (symlinkat ("file/baz", AT_FDCWD, "link"), ==, 0);
+
+  target_ino = get_ino (dfd);
+  link_ino = path_get_ino ("link");
+
+  check_chase (AT_FDCWD, "link", 0, target_ino);
+  check_chase (AT_FDCWD, "link/", 0, target_ino);
+  check_chase (AT_FDCWD, "link///", 0, target_ino);
+  check_chase (AT_FDCWD, "link/.//.", 0, target_ino);
+  check_chase (AT_FDCWD, "link", 0, target_ino);
+
+  check_chase (AT_FDCWD, "link", GLNX_CHASE_NOFOLLOW, link_ino);
+  check_chase (AT_FDCWD, "./file/../link", GLNX_CHASE_NOFOLLOW, link_ino);
+  check_chase (AT_FDCWD, "link/", GLNX_CHASE_NOFOLLOW, target_ino);
+  check_chase (AT_FDCWD, "././link/.", GLNX_CHASE_NOFOLLOW, target_ino);
+  check_chase (AT_FDCWD, "link/.//", GLNX_CHASE_NOFOLLOW, target_ino);
+
+  check_chase (AT_FDCWD, "link",
+               GLNX_CHASE_NOFOLLOW | GLNX_CHASE_RESOLVE_NO_SYMLINKS,
+               link_ino);
+  check_chase_error (AT_FDCWD, "link",
+                     GLNX_CHASE_RESOLVE_NO_SYMLINKS,
+                     G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS);
+}
+
+static void
+test_chase_resolve (void)
+{
+  g_autoptr(GError) error = NULL;
+  glnx_autofd int foo_dfd = -1;
+  glnx_autofd int bar_dfd = -1;
+  g_autofree char *foo_abspath = NULL;
+  int ino;
+
+  g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "foo", 0755,
+                                              &foo_dfd,
+                                              NULL, &error));
+  g_assert_no_error (error);
+  g_assert_cmpint (foo_dfd, >=, 0);
+
+  g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "foo/bar", 0755,
+                                              &bar_dfd,
+                                              NULL, &error));
+  g_assert_no_error (error);
+  g_assert_cmpint (bar_dfd, >=, 0);
+
+  foo_abspath = get_abspath (foo_dfd, "");
+
+  g_assert_cmpint (symlinkat ("..", foo_dfd, "link1"), ==, 0);
+  g_assert_cmpint (symlinkat ("bar/../..", foo_dfd, "link2"), ==, 0);
+  g_assert_cmpint (symlinkat (foo_abspath, foo_dfd, "link3"), ==, 0);
+  g_assert_cmpint (symlinkat ("/bar", foo_dfd, "link4"), ==, 0);
+  g_assert_cmpint (symlinkat ("link1/foo", foo_dfd, "link5"), ==, 0);
+  g_assert_cmpint (symlinkat ("link7", foo_dfd, "link6"), ==, 0);
+  g_assert_cmpint (symlinkat ("link6", foo_dfd, "link7"), ==, 0);
+
+  ino = get_ino (bar_dfd);
+
+  /* A bunch of different ways to get from CWD and foo to bar */
+  check_chase (foo_dfd, "./bar", 0, ino);
+  check_chase (foo_dfd, "../foo/bar", 0, ino);
+  check_chase (foo_dfd, "link1/foo/bar", 0, ino);
+  check_chase (AT_FDCWD, "foo/link1/foo/bar", 0, ino);
+  check_chase (foo_dfd, "link2/foo/bar", 0, ino);
+  check_chase (AT_FDCWD, ".///foo/./link2/foo/bar", 0, ino);
+  check_chase (foo_dfd, "link3/bar", 0, ino);
+  check_chase (AT_FDCWD, ".///foo/./link3/bar", 0, ino);
+  check_chase (foo_dfd, "link5/bar", 0, ino);
+
+  /* check that NO_SYMLINKS works with a component in the middle */
+  check_chase_error (AT_FDCWD, "foo/link3/bar",
+                     GLNX_CHASE_RESOLVE_NO_SYMLINKS,
+                     G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS);
+
+  /* link6 points to link 7, points to link6, ... This should error out! */
+  check_chase_error (foo_dfd, "link6/bar", 0,
+                     G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS);
+
+  /* Test with links which never go below the dfd */
+  check_chase (AT_FDCWD, "foo/link1/foo/bar",
+               GLNX_CHASE_RESOLVE_BENEATH,
+               ino);
+  check_chase (AT_FDCWD, "foo/link2/foo/bar",
+               GLNX_CHASE_RESOLVE_BENEATH,
+               ino);
+  /* An absolute link is always below the dfd */
+  check_chase_error (AT_FDCWD, "foo/link3/foo/bar",
+                     GLNX_CHASE_RESOLVE_BENEATH,
+                     G_IO_ERROR, G_IO_ERROR_FAILED);
+
+  /* Same, but from foo instead of cwd */
+  check_chase_error (foo_dfd, "link1/foo/bar",
+                     GLNX_CHASE_RESOLVE_BENEATH,
+                     G_IO_ERROR, G_IO_ERROR_FAILED);
+  check_chase_error (foo_dfd, "link2/foo/bar",
+                     GLNX_CHASE_RESOLVE_BENEATH,
+                     G_IO_ERROR, G_IO_ERROR_FAILED);
+  check_chase_error (foo_dfd, "link3/foo/bar",
+                     GLNX_CHASE_RESOLVE_BENEATH,
+                     G_IO_ERROR, G_IO_ERROR_FAILED);
+
+  /* Check that trying to be below the dfd with RESOLVE_IN_ROOT resolves to the
+   * dfd itself */
+  check_chase (foo_dfd, "link1/bar",
+               GLNX_CHASE_RESOLVE_IN_ROOT,
+               ino);
+  check_chase (foo_dfd, "link2/bar",
+               GLNX_CHASE_RESOLVE_IN_ROOT,
+               ino);
+  /* The absolute link is relative to dfd with RESOLVE_IN_ROOT, so this
+   * fails... */
+  check_chase_error (foo_dfd, "link3",
+                     GLNX_CHASE_RESOLVE_IN_ROOT,
+                     G_IO_ERROR, G_IO_ERROR_NOT_FOUND);
+  /* ... but the link /bar resolves correctly from foo as dfd. */
+  check_chase (foo_dfd, "link4",
+               GLNX_CHASE_RESOLVE_IN_ROOT,
+               ino);
+}
+
+static void
+test_chase_resolve_in_root_absolute (void)
+{
+  g_autoptr(GError) error = NULL;
+  glnx_autofd int foo_dfd = -1;
+  glnx_autofd int bar_dfd = -1;
+  glnx_autofd int baz_dfd = -1;
+
+  g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "foo", 0755,
+                                              &foo_dfd,
+                                              NULL, &error));
+  g_assert_no_error (error);
+  g_assert_cmpint (foo_dfd, >=, 0);
+
+  g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "foo/bar", 0755,
+                                              &bar_dfd,
+                                              NULL, &error));
+  g_assert_no_error (error);
+  g_assert_cmpint (bar_dfd, >=, 0);
+
+  g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "foo/bar/baz", 0755,
+                                              &baz_dfd,
+                                              NULL, &error));
+  g_assert_no_error (error);
+  g_assert_cmpint (baz_dfd, >=, 0);
+
+  /* Test the absolute symlink doesn't break tracking of the root level */
+  g_assert_cmpint (symlinkat ("/..", baz_dfd, "link1"), ==, 0);
+
+  /* We should not be able to break out of the root! */
+  check_chase (bar_dfd, "./baz/link1", GLNX_CHASE_RESOLVE_IN_ROOT, get_ino (bar_dfd));
+}
+
+static void
+check_chase_and_statxat (int             dfd,
+                         const char     *path,
+                         GlnxChaseFlags  flags,
+                         ino_t           expected_ino,
+                         mode_t          expected_type)
+{
+  g_autoptr(GError) error = NULL;
+  glnx_autofd int chase_fd = -1;
+  struct glnx_statx stx;
+
+  /* let's try to test the openat2 impl */
+  chase_fd = glnx_chase_and_statxat (dfd, path, flags,
+                                     GLNX_STATX_TYPE | GLNX_STATX_INO,
+                                     &stx, &error);
+  g_assert_cmpint (chase_fd, >=, 0);
+  g_assert_no_error (error);
+  g_assert_cmpint (stx.stx_ino, ==, expected_ino);
+  g_assert_cmpint (stx.stx_mode & S_IFMT, ==, expected_type);
+  g_clear_fd (&chase_fd, NULL);
+
+  /* let's try to test the open_tree impl */
+  chase_fd = glnx_chase_and_statxat (dfd, path,
+                                     flags | GLNX_CHASE_DEBUG_NO_OPENAT2,
+                                     GLNX_STATX_TYPE | GLNX_STATX_INO,
+                                     &stx, &error);
+  g_assert_cmpint (chase_fd, >=, 0);
+  g_assert_no_error (error);
+  g_assert_cmpint (stx.stx_ino, ==, expected_ino);
+  g_assert_cmpint (stx.stx_mode & S_IFMT, ==, expected_type);
+  g_clear_fd (&chase_fd, NULL);
+
+  /* let's try to test the openat impl */
+  chase_fd = glnx_chase_and_statxat (dfd, path,
+                                     flags |
+                                     GLNX_CHASE_DEBUG_NO_OPENAT2 |
+                                     GLNX_CHASE_DEBUG_NO_OPEN_TREE,
+                                     GLNX_STATX_TYPE | GLNX_STATX_INO,
+                                     &stx, &error);
+  g_assert_cmpint (chase_fd, >=, 0);
+  g_assert_no_error (error);
+  g_assert_cmpint (stx.stx_ino, ==, expected_ino);
+  g_assert_cmpint (stx.stx_mode & S_IFMT, ==, expected_type);
+  g_clear_fd (&chase_fd, NULL);
+}
+
+static void
+check_chase_and_statxat_error (int             dfd,
+                               const char     *path,
+                               GlnxChaseFlags  flags,
+                               GQuark          err_domain,
+                               gint            err_code)
+{
+  g_autoptr(GError) error = NULL;
+  glnx_autofd int chase_fd = -1;
+  struct glnx_statx stx;
+
+  /* let's try to test the openat2 impl */
+  chase_fd = glnx_chase_and_statxat (dfd, path, flags,
+                                     GLNX_STATX_TYPE | GLNX_STATX_INO,
+                                     &stx, &error);
+  g_assert_cmpint (chase_fd, <, 0);
+  g_assert_error (error, err_domain, err_code);
+  g_clear_error (&error);
+
+  /* let's try to test the open_tree impl */
+  chase_fd = glnx_chase_and_statxat (dfd, path,
+                                     flags | GLNX_CHASE_DEBUG_NO_OPENAT2,
+                                     GLNX_STATX_TYPE | GLNX_STATX_INO,
+                                     &stx, &error);
+  g_assert_cmpint (chase_fd, <, 0);
+  g_assert_error (error, err_domain, err_code);
+  g_clear_error (&error);
+
+  /* let's try to test the openat impl */
+  chase_fd = glnx_chase_and_statxat (dfd, path,
+                                     flags |
+                                     GLNX_CHASE_DEBUG_NO_OPENAT2 |
+                                     GLNX_CHASE_DEBUG_NO_OPEN_TREE,
+                                     GLNX_STATX_TYPE | GLNX_STATX_INO,
+                                     &stx, &error);
+  g_assert_cmpint (chase_fd, <, 0);
+  g_assert_error (error, err_domain, err_code);
+  g_clear_error (&error);
+}
+
+static void
+test_chase_and_statxat_basic (void)
+{
+  g_autoptr(GError) error = NULL;
+  glnx_autofd int dfd = -1;
+  glnx_autofd int file_fd = -1;
+  ino_t expected_ino;
+
+  g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755,
+                                              &dfd,
+                                              NULL, &error));
+  g_assert_no_error (error);
+  g_assert_cmpint (dfd, >=, 0);
+
+  expected_ino = get_ino (dfd);
+
+  /* Test with various path forms */
+  for (size_t i = 0; i < G_N_ELEMENTS (test_paths); i++)
+    check_chase_and_statxat (AT_FDCWD, test_paths[i], 0, expected_ino, S_IFDIR);
+
+  /* Create a regular file and test it */
+  file_fd = openat (dfd, "testfile", O_WRONLY | O_CREAT | O_CLOEXEC, 0644);
+  g_assert_cmpint (file_fd, >=, 0);
+  g_clear_fd (&file_fd, NULL);
+
+  expected_ino = path_get_ino ("file/baz/testfile");
+  check_chase_and_statxat (AT_FDCWD, "file/baz/testfile", 0, expected_ino, S_IFREG);
+
+  /* Test error cases */
+  check_chase_and_statxat_error (AT_FDCWD, "nope", 0, G_IO_ERROR, G_IO_ERROR_NOT_FOUND);
+}
+
+static void
+test_chase_and_statxat_symlink (void)
+{
+  g_autoptr(GError) error = NULL;
+  glnx_autofd int dfd = -1;
+  glnx_autofd int chase_fd = -1;
+  ino_t link_ino;
+  ino_t target_ino;
+  struct glnx_statx stx;
+
+  g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755,
+                                              &dfd,
+                                              NULL, &error));
+  g_assert_no_error (error);
+  g_assert_cmpint (dfd, >=, 0);
+
+  g_assert_cmpint (symlinkat ("file/baz", AT_FDCWD, "fstatlink"), ==, 0);
+
+  target_ino = get_ino (dfd);
+  link_ino = path_get_ino ("fstatlink");
+
+  /* Following symlinks should give us the directory */
+  check_chase_and_statxat (AT_FDCWD, "fstatlink", 0, target_ino, S_IFDIR);
+  check_chase_and_statxat (AT_FDCWD, "fstatlink/", 0, target_ino, S_IFDIR);
+
+  /* With NOFOLLOW, we should get the symlink itself */
+  check_chase_and_statxat (AT_FDCWD, "fstatlink", GLNX_CHASE_NOFOLLOW, link_ino, S_IFLNK);
+
+  /* Verify we can distinguish between regular files, directories, and symlinks */
+  chase_fd = glnx_chase_and_statxat (AT_FDCWD, "fstatlink", GLNX_CHASE_NOFOLLOW,
+                                     GLNX_STATX_TYPE | GLNX_STATX_INO,
+                                     &stx, &error);
+  g_assert_cmpint (chase_fd, >=, 0);
+  g_assert_no_error (error);
+  g_assert_true (S_ISLNK (stx.stx_mode));
+  g_clear_fd (&chase_fd, NULL);
+
+  chase_fd = glnx_chase_and_statxat (AT_FDCWD, "fstatlink", 0,
+                                     GLNX_STATX_TYPE | GLNX_STATX_INO,
+                                     &stx, &error);
+  g_assert_cmpint (chase_fd, >=, 0);
+  g_assert_no_error (error);
+  g_assert_true (S_ISDIR (stx.stx_mode));
+  g_clear_fd (&chase_fd, NULL);
+
+  /* Test with RESOLVE_NO_SYMLINKS */
+  check_chase_and_statxat_error (AT_FDCWD, "fstatlink",
+                                 GLNX_CHASE_RESOLVE_NO_SYMLINKS,
+                                 G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS);
+}
+
+static void
+test_chase_and_statxat_permissions (void)
+{
+  g_autoptr(GError) error = NULL;
+  glnx_autofd int dfd = -1;
+  glnx_autofd int file_fd = -1;
+  glnx_autofd int chase_fd = -1;
+  struct glnx_statx stx;
+  mode_t expected_mode = 0640;
+
+  g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "permtest", 0755,
+                                              &dfd,
+                                              NULL, &error));
+  g_assert_no_error (error);
+
+  /* Create a file with specific permissions */
+  file_fd = openat (dfd, "testfile", O_WRONLY | O_CREAT | O_CLOEXEC, expected_mode);
+  g_assert_cmpint (file_fd, >=, 0);
+  g_clear_fd (&file_fd, NULL);
+
+  /* Verify that glnx_chase_and_statxat returns the correct permissions */
+  chase_fd = glnx_chase_and_statxat (dfd, "testfile", 0,
+                                     GLNX_STATX_TYPE | GLNX_STATX_MODE,
+                                     &stx, &error);
+  g_assert_cmpint (chase_fd, >=, 0);
+  g_assert_no_error (error);
+  g_assert_cmpint (stx.stx_mode & 0777, ==, expected_mode);
+  g_assert_true (S_ISREG (stx.stx_mode));
+  g_clear_fd (&chase_fd, NULL);
+}
+
+int main (int argc, char **argv)
+{
+  _GLNX_TEST_SCOPED_TEMP_DIR;
+  int ret;
+
+  g_test_init (&argc, &argv, NULL);
+
+  g_test_add_func ("/chase-relative", test_chase_relative);
+  g_test_add_func ("/chase-relative-fd", test_chase_relative_fd);
+  g_test_add_func ("/chase-absolute", test_chase_absolute);
+  g_test_add_func ("/chase-link", test_chase_link);
+  g_test_add_func ("/chase-resolve", test_chase_resolve);
+  g_test_add_func ("/chase-resolve-in-root-absolute", test_chase_resolve_in_root_absolute);
+  g_test_add_func ("/chase-and-statxat-basic", test_chase_and_statxat_basic);
+  g_test_add_func ("/chase-and-statxat-symlink", test_chase_and_statxat_symlink);
+  g_test_add_func ("/chase-and-statxat-permissions", test_chase_and_statxat_permissions);
+
+  ret = g_test_run();
+
+  return ret;
+}
diff -Nru flatpak-1.16.3/subprojects/libglnx/tests/test-libglnx-fdio.c flatpak-1.16.6/subprojects/libglnx/tests/test-libglnx-fdio.c
--- flatpak-1.16.3/subprojects/libglnx/tests/test-libglnx-fdio.c	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/subprojects/libglnx/tests/test-libglnx-fdio.c	2026-04-10 18:28:42.000000000 +0100
@@ -286,6 +286,158 @@
     }
 }
 
+static void
+test_fd_reopen (void)
+{
+  g_autoptr(GError) error = NULL;
+  glnx_autofd int dfd = -1;
+  glnx_autofd int opath_fd = -1;
+  glnx_autofd int regular_fd = -1;
+  glnx_autofd int testfile_fd = -1;
+  glnx_autofd int link_opath_fd = -1;
+  glnx_autofd int reopened_fd = -1;
+  struct stat st1, st2;
+  const char *test_data = "test content";
+  char buf[100];
+  ssize_t n;
+  gboolean ok;
+  int flags;
+
+  /* Create a test directory and file */
+  ok = glnx_shutil_mkdir_p_at_open (AT_FDCWD, "reopen_test", 0755, &dfd, NULL, &error);
+  g_assert_no_error (error);
+  g_assert_true (ok);
+  g_assert_no_errno (dfd);
+
+  glnx_file_replace_contents_at (dfd, "testfile",
+                                 (const void *) test_data, strlen (test_data),
+                                 GLNX_FILE_REPLACE_NODATASYNC, NULL, &error);
+  g_assert_no_error (error);
+
+  /* Test 1: Reopen O_PATH fd as regular fd for reading and writing */
+  opath_fd = openat (dfd, "testfile", O_PATH | O_CLOEXEC);
+  g_assert_no_errno (opath_fd);
+
+  regular_fd = glnx_fd_reopen (opath_fd, O_RDWR, &error);
+  g_assert_no_errno (regular_fd);
+  g_assert_no_error (error);
+
+  flags = fcntl (regular_fd, F_GETFL);
+  g_assert_no_errno (flags);
+  g_assert_cmpint (flags & (O_RDONLY | O_WRONLY | O_RDWR), ==, O_RDWR);
+  g_assert_cmpint (flags & (O_PATH | O_DIRECTORY | O_NOFOLLOW), ==, 0);
+  flags = fcntl (regular_fd, F_GETFD);
+  g_assert_no_errno (flags);
+  g_assert_cmpint (flags & FD_CLOEXEC, ==, FD_CLOEXEC);
+
+  /* Verify we can read from the reopened fd */
+  n = read (regular_fd, buf, sizeof (buf));
+  g_assert_cmpmem (buf, n, test_data, strlen (test_data));
+
+  g_clear_fd (&regular_fd, NULL);
+  g_clear_fd (&opath_fd, NULL);
+
+  /* Test 2: Reopen directory fd with O_DIRECTORY */
+  opath_fd = openat (AT_FDCWD, "reopen_test", O_PATH | O_CLOEXEC);
+  g_assert_no_errno (opath_fd);
+
+  reopened_fd = glnx_fd_reopen (opath_fd, O_RDONLY | O_DIRECTORY, &error);
+  g_assert_no_error (error);
+  g_assert_no_errno (reopened_fd);
+
+  flags = fcntl (reopened_fd, F_GETFL);
+  g_assert_no_errno (flags);
+  g_assert_cmpint (flags & (O_RDONLY | O_WRONLY | O_RDWR), ==, O_RDONLY);
+  g_assert_cmpint (flags & (O_PATH | O_DIRECTORY | O_NOFOLLOW), ==, O_DIRECTORY);
+  flags = fcntl (reopened_fd, F_GETFD);
+  g_assert_no_errno (flags);
+  g_assert_cmpint (flags & FD_CLOEXEC, ==, FD_CLOEXEC);
+
+  /* Verify both fds point to the same inode */
+  g_assert_no_errno (fstat (opath_fd, &st1));
+  g_assert_no_errno (fstat (reopened_fd, &st2));
+  g_assert_cmpint (st1.st_ino, ==, st2.st_ino);
+
+  g_clear_fd (&reopened_fd, NULL);
+  g_clear_fd (&opath_fd, NULL);
+
+  /* Test 3: Reopen AT_FDCWD */
+  reopened_fd = glnx_fd_reopen (AT_FDCWD, O_RDONLY | O_DIRECTORY, &error);
+  g_assert_no_error (error);
+  g_assert_no_errno (reopened_fd);
+
+  g_clear_fd (&reopened_fd, NULL);
+
+  /* Test 4: Test that O_NOFOLLOW is rejected */
+  opath_fd = openat (dfd, "testfile", O_PATH | O_CLOEXEC);
+  g_assert_no_errno (opath_fd);
+
+  regular_fd = glnx_fd_reopen (opath_fd, O_RDONLY | O_NOFOLLOW, &error);
+  g_assert_cmpint (regular_fd, <, 0);
+  g_assert_error (error, G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS);
+  g_clear_error (&error);
+
+  g_clear_fd (&opath_fd, NULL);
+
+  /* Test 5: Reopen O_PATH fd to symlink with O_PATH (should work) */
+  g_assert_no_errno (symlinkat ("testfile", dfd, "testlink"));
+
+  link_opath_fd = openat (dfd, "testlink", O_PATH | O_NOFOLLOW);
+  g_assert_no_errno (link_opath_fd);
+
+  /* Verify it's a symlink */
+  g_assert_no_errno (fstatat (link_opath_fd, "", &st1, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW));
+  g_assert_true (S_ISLNK (st1.st_mode));
+
+  /* Reopen with O_PATH should work */
+  reopened_fd = glnx_fd_reopen (link_opath_fd, O_PATH, &error);
+  g_assert_no_error (error);
+  g_assert_no_errno (reopened_fd);
+
+  flags = fcntl (reopened_fd, F_GETFL);
+  g_assert_no_errno (flags);
+  g_assert_cmpint (flags & (O_RDONLY | O_WRONLY | O_RDWR), ==, O_RDONLY);
+  g_assert_cmpint (flags & (O_PATH | O_DIRECTORY | O_NOFOLLOW), ==, O_PATH);
+  flags = fcntl (reopened_fd, F_GETFD);
+  g_assert_no_errno (flags);
+  g_assert_cmpint (flags & FD_CLOEXEC, ==, FD_CLOEXEC);
+
+  /* Verify both point to the same symlink */
+  g_assert_no_errno (fstatat (reopened_fd, "", &st2, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW));
+  g_assert_cmpint (st1.st_ino, ==, st2.st_ino);
+  g_assert_true (S_ISLNK (st2.st_mode));
+
+  g_clear_fd (&reopened_fd, NULL);
+
+  /* Test 6: Reopening O_PATH fd to symlink without O_PATH should fail with ELOOP */
+  reopened_fd = glnx_fd_reopen (link_opath_fd, O_RDONLY, &error);
+  g_assert_cmpint (reopened_fd, <, 0);
+  g_assert_error (error, G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS);
+  g_clear_error (&error);
+
+  g_clear_fd (&link_opath_fd, NULL);
+
+  /* Test 7: Verify read index is reset */
+  testfile_fd = openat (dfd, "testfile", O_RDONLY | O_CLOEXEC);
+  g_assert_no_errno (testfile_fd);
+
+  /* Read some data to advance the read index */
+  n = read (testfile_fd, buf, 4);
+  g_assert_cmpint (n, ==, 4);
+
+  /* Reopen should reset the read index */
+  reopened_fd = glnx_fd_reopen (testfile_fd, O_RDONLY, &error);
+  g_assert_no_error (error);
+  g_assert_no_errno (reopened_fd);
+
+  /* Should read from the beginning again */
+  n = read (reopened_fd, buf, sizeof (buf));
+  g_assert_cmpmem (buf, n, test_data, strlen (test_data));
+
+  g_clear_fd (&reopened_fd, NULL);
+  g_clear_fd (&testfile_fd, NULL);
+}
+
 int main (int argc, char **argv)
 {
   _GLNX_TEST_SCOPED_TEMP_DIR;
@@ -300,6 +452,7 @@
   g_test_add_func ("/renameat2-noreplace", test_renameat2_noreplace);
   g_test_add_func ("/renameat2-exchange", test_renameat2_exchange);
   g_test_add_func ("/fstat", test_fstatat);
+  g_test_add_func ("/fd-reopen", test_fd_reopen);
 
   ret = g_test_run();
 
diff -Nru flatpak-1.16.3/system-helper/flatpak-system-helper.c flatpak-1.16.6/system-helper/flatpak-system-helper.c
--- flatpak-1.16.3/system-helper/flatpak-system-helper.c	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/system-helper/flatpak-system-helper.c	2026-04-10 18:28:42.000000000 +0100
@@ -358,23 +358,31 @@
 }
 
 static OngoingPull *
-take_ongoing_pull_by_dir (const gchar *src_dir)
+take_ongoing_pull_by_dir (const char *src_dir,
+                          uid_t       uid)
 {
   OngoingPull *pull = NULL;
-  gpointer key, value;
+  char *cache_dir_name = NULL;
 
   G_LOCK (cache_dirs_in_use);
-  /* Keep src_dir key inside hashtable but remove its OngoingPull
-   * value and set it to NULL. This way src_dir is still marked
-   * as in-use (as Deploy or CancelPull might be executing on it,
-   * whereas OngoingPull ownership is transferred to respective
-   * callers. */
-  if (g_hash_table_steal_extended (cache_dirs_in_use, src_dir, &key, &value))
-    {
-      if (value)
+  if (g_hash_table_steal_extended (cache_dirs_in_use, src_dir,
+                                   (gpointer) &cache_dir_name,
+                                   (gpointer) &pull))
+    {
+      if (pull && pull->uid == uid)
+        {
+          /* Keep src_dir key inside hashtable but remove its OngoingPull
+           * value and set it to NULL. This way src_dir is still marked
+           * as in-use (as Deploy or CancelPull might be executing on it,
+           * whereas OngoingPull ownership is transferred to respective
+           * callers. */
+          g_hash_table_insert (cache_dirs_in_use, cache_dir_name, NULL);
+        }
+      else
         {
-          g_hash_table_insert (cache_dirs_in_use, key, NULL);
-          pull = value;
+          /* Otherwise, re-insert what is currently there and return NULL */
+          g_hash_table_insert (cache_dirs_in_use, cache_dir_name, pull);
+          pull = NULL;
         }
     }
   G_UNLOCK (cache_dirs_in_use);
@@ -426,6 +434,9 @@
 
   if (strlen (arg_repo_path) > 0)
     {
+      g_autoptr(GError) local_error = NULL;
+      uid_t uid;
+
       if (!g_file_query_exists (repo_file, NULL))
         {
           g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
@@ -433,30 +444,17 @@
           return G_DBUS_METHOD_INVOCATION_HANDLED;
         }
 
+      /* Ensure that pull's uid is same as the caller's uid */
+      if (!get_connection_uid (invocation, &uid, &local_error))
+        {
+          g_dbus_method_invocation_return_gerror (invocation, local_error);
+          return G_DBUS_METHOD_INVOCATION_HANDLED;
+        }
+
       src_dir = g_path_get_dirname (arg_repo_path);
-      ongoing_pull = take_ongoing_pull_by_dir (src_dir);
+      ongoing_pull = take_ongoing_pull_by_dir (src_dir, uid);
       if (ongoing_pull != NULL)
         {
-          g_autoptr(GError) local_error = NULL;
-          uid_t uid;
-
-          /* Ensure that pull's uid is same as the caller's uid */
-          if (!get_connection_uid (invocation, &uid, &local_error))
-            {
-              g_dbus_method_invocation_return_gerror (invocation, local_error);
-              return G_DBUS_METHOD_INVOCATION_HANDLED;
-            }
-          else
-            {
-              if (ongoing_pull->uid != uid)
-                {
-                  g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
-                                                         "Ongoing pull's uid(%d) does not match with peer uid(%d)",
-                                                         ongoing_pull->uid, uid);
-                  return G_DBUS_METHOD_INVOCATION_HANDLED;
-                }
-            }
-
           terminate_revokefs_backend (ongoing_pull);
 
           if (!flatpak_canonicalize_permissions (AT_FDCWD,
@@ -738,31 +736,20 @@
       return G_DBUS_METHOD_INVOCATION_HANDLED;
     }
 
-  ongoing_pull = take_ongoing_pull_by_dir (arg_src_dir);
-  if (ongoing_pull == NULL)
+  if (!get_connection_uid (invocation, &uid, &error))
     {
-      g_set_error (&error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
-                   "Cannot find ongoing pull to cancel at %s", arg_src_dir);
       g_dbus_method_invocation_return_gerror (invocation, error);
       return G_DBUS_METHOD_INVOCATION_HANDLED;
     }
 
-  /* Ensure that pull's uid is same as the caller's uid */
-  if (!get_connection_uid (invocation, &uid, &error))
+  ongoing_pull = take_ongoing_pull_by_dir (arg_src_dir, uid);
+  if (ongoing_pull == NULL)
     {
+      g_set_error (&error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+                   "Cannot find ongoing pull to cancel at %s", arg_src_dir);
       g_dbus_method_invocation_return_gerror (invocation, error);
       return G_DBUS_METHOD_INVOCATION_HANDLED;
     }
-  else
-    {
-      if (ongoing_pull->uid != uid)
-        {
-          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
-                                                 "Ongoing pull's uid(%d) does not match with peer uid(%d)",
-                                                 ongoing_pull->uid, uid);
-          return G_DBUS_METHOD_INVOCATION_HANDLED;
-        }
-    }
 
   ongoing_pull->preserve_pull = (arg_flags & FLATPAK_HELPER_CANCEL_PULL_FLAGS_PRESERVE_PULL) != 0;
   ongoing_pull_free (ongoing_pull);
diff -Nru flatpak-1.16.3/tests/apply-extra-static.c flatpak-1.16.6/tests/apply-extra-static.c
--- flatpak-1.16.3/tests/apply-extra-static.c	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/tests/apply-extra-static.c	2026-04-10 18:28:42.000000000 +0100
@@ -0,0 +1,15 @@
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+static const char msg[] = "noruntime-extra-data\n";
+
+int main(void) {
+    mkdir("/app/extra", 0755);
+    int fd = open("/app/extra/ran", O_WRONLY | O_CREAT | O_TRUNC, 0644);
+    if (fd < 0)
+        return 1;
+    write(fd, msg, sizeof(msg) - 1);
+    close(fd);
+    return 0;
+}
diff -Nru flatpak-1.16.3/tests/libtest.sh flatpak-1.16.6/tests/libtest.sh
--- flatpak-1.16.3/tests/libtest.sh	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/tests/libtest.sh	2026-04-10 18:28:42.000000000 +0100
@@ -312,7 +312,7 @@
         RUNTIME_REPO=${TEST_DATA_DIR}/runtime-repo
         (
             flock -s 200
-            if [ ! -d ${RUNTIME_REPO} ]; then
+            if [ ! -f "${RUNTIME_REPO}/refs/heads/${RUNTIME_REF}" ]; then
                 $(dirname $0)/make-test-runtime.sh ${RUNTIME_REPO} org.test.Platform ${BRANCH} "" "" > /dev/null
             fi
         ) 200>${TEST_DATA_DIR}/runtime-repo-lock
diff -Nru flatpak-1.16.3/tests/meson.build flatpak-1.16.6/tests/meson.build
--- flatpak-1.16.3/tests/meson.build	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/tests/meson.build	2026-04-10 18:28:42.000000000 +0100
@@ -291,6 +291,21 @@
   install_dir : installed_testdir,
 )
 
+if cc.links('#include <stdio.h>\nint main(void) { printf("test"); return 0; }',
+            args: ['-static'],
+            name: 'static libc')
+  executable(
+    'apply-extra-static',
+    'apply-extra-static.c',
+    install : get_option('installed_tests'),
+    install_dir : installed_testdir,
+    override_options : ['b_sanitize=none'],
+    link_args : ['-static'],
+  )
+else
+  warning('static libc not available, apply-extra-static will be skipped')
+endif
+
 subdir('share/xdg-desktop-portal/portals')
 subdir('services')
 subdir('test-keyring')
@@ -362,6 +377,7 @@
     'test-summaries.sh' : 60,
     'test-unused.sh' : 90,
     'test-update-portal.sh' : 90,
+    'test-extra-data.sh' : 90,
   }.get(script, 30)
 
   is_parallel = {
diff -Nru flatpak-1.16.3/tests/test-extra-data.sh flatpak-1.16.6/tests/test-extra-data.sh
--- flatpak-1.16.3/tests/test-extra-data.sh	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/tests/test-extra-data.sh	2026-04-10 18:28:42.000000000 +0100
@@ -0,0 +1,104 @@
+#!/bin/bash
+#
+# Copyright (C) 2025 Red Hat, Inc
+#
+# 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 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, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+set -euo pipefail
+
+. "$(dirname $0)/libtest.sh"
+
+skip_without_bwrap
+
+REPONAME="test"
+BRANCH="master"
+COLLECTION_ID="org.test.Collection.${REPONAME}"
+
+setup_repo ${REPONAME} ${COLLECTION_ID}
+
+# create the extra data
+EXTRA_DATA_FILE="extra-data-test"
+EXTRA_DATA_DIR="${TEST_DATA_DIR}/extra-data-server/"
+mkdir -p "${EXTRA_DATA_DIR}"
+echo "extra-data-test-content" > "${EXTRA_DATA_DIR}/${EXTRA_DATA_FILE}"
+
+# serve the extra data
+httpd web-server.py "${EXTRA_DATA_DIR}"
+EXTRA_DATA_URL="http://127.0.0.1:$(cat httpd-port)/${EXTRA_DATA_FILE}"
+
+# download to get the size and sha256 sum
+DOWNLOADED_EXTRA_DATA="${TEST_DATA_DIR}/downloaded-extra-data"
+curl "${EXTRA_DATA_URL}" -o "${DOWNLOADED_EXTRA_DATA}"
+EXTRA_DATA_SIZE=$(stat --printf="%s" "${DOWNLOADED_EXTRA_DATA}")
+EXTRA_DATA_SHA256=$(sha256sum "${DOWNLOADED_EXTRA_DATA}" | cut -f1 -d' ')
+
+echo "1..2"
+
+# build the app with the extra data
+EXTRA_DATA="--extra-data=test:${EXTRA_DATA_SHA256}:${EXTRA_DATA_SIZE}:${EXTRA_DATA_SIZE}:${EXTRA_DATA_URL}"
+BUILD_FINISH_ARGS=${EXTRA_DATA} make_updated_app ${REPONAME} ${COLLECTION_ID} ${BRANCH} UPDATE1
+
+# ensure it installs correctly
+install_repo ${REPONAME} ${BRANCH}
+
+# ensure the right extra-data got downloaded
+${FLATPAK} run --command=sh org.test.Hello -c "cat /app/extra/test" > out
+assert_file_has_content out "extra-data-test-content"
+
+${FLATPAK} ${U} uninstall -y org.test.Hello >&2
+
+ok "install extra data app with ostree"
+
+build_extra_data_noruntime_app() {
+    local repo="$1"
+    local branch="$2"
+
+    DIR="$(mktemp -d)"
+    ARCH="$(flatpak --default-arch)"
+
+    mkdir -p "${DIR}/files/bin"
+
+    cp "${G_TEST_BUILDDIR}/apply-extra-static" "${DIR}/files/bin/apply_extra"
+    chmod +x "${DIR}/files/bin/apply_extra"
+
+    cat > "${DIR}/metadata" <<EOF
+[Application]
+name=org.test.ExtraDataNoRuntime
+runtime=org.test.Platform/${ARCH}/${branch}
+
+[Extra Data]
+NoRuntime=true
+uri=${EXTRA_DATA_URL}
+size=${EXTRA_DATA_SIZE}
+checksum=${EXTRA_DATA_SHA256}
+EOF
+
+    flatpak build-finish "${DIR}" >&2
+    flatpak build-export ${FL_GPGARGS} "${repo}" "${DIR}" "${branch}" >&2
+    rm -rf "${DIR}"
+}
+
+if test -f "${G_TEST_BUILDDIR}/apply-extra-static"; then
+    build_extra_data_noruntime_app repos/test ${BRANCH}
+    update_repo ${REPONAME} ${COLLECTION_ID}
+    ${FLATPAK} ${U} install --or-update -y ${REPONAME}-repo org.test.ExtraDataNoRuntime ${BRANCH} >&2
+    DEPLOY_DIR="$(${FLATPAK} ${U} info --show-location org.test.ExtraDataNoRuntime)"
+    assert_file_has_content "${DEPLOY_DIR}/files/extra/ran" "^noruntime-extra-data$"
+    ${FLATPAK} ${U} uninstall -y org.test.ExtraDataNoRuntime >&2
+    ok "install+run extra data app with NoRuntime"
+else
+    ok "# SKIP install+run extra data app with NoRuntime apply-extra-static not found"
+fi
diff -Nru flatpak-1.16.3/tests/test-matrix/meson.build flatpak-1.16.6/tests/test-matrix/meson.build
--- flatpak-1.16.3/tests/test-matrix/meson.build	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/tests/test-matrix/meson.build	2026-04-10 18:28:42.000000000 +0100
@@ -29,6 +29,8 @@
 wrapped_tests += {'name' : 'test-summaries@system.wrap', 'script' : 'test-summaries.sh'}
 wrapped_tests += {'name' : 'test-subset@user.wrap', 'script' : 'test-subset.sh'}
 wrapped_tests += {'name' : 'test-subset@system.wrap', 'script' : 'test-subset.sh'}
+wrapped_tests += {'name' : 'test-extra-data@user.wrap', 'script' : 'test-extra-data.sh'}
+wrapped_tests += {'name' : 'test-extra-data@system.wrap', 'script' : 'test-extra-data.sh'}
 wrapped_tests += {'name' : 'test-basic.sh', 'script' : 'test-basic.sh'}
 wrapped_tests += {'name' : 'test-completion.sh', 'script' : 'test-completion.sh'}
 wrapped_tests += {'name' : 'test-config.sh', 'script' : 'test-config.sh'}
@@ -45,3 +47,5 @@
 wrapped_tests += {'name' : 'test-prune.sh', 'script' : 'test-prune.sh'}
 wrapped_tests += {'name' : 'test-seccomp.sh', 'script' : 'test-seccomp.sh'}
 wrapped_tests += {'name' : 'test-repair.sh', 'script' : 'test-repair.sh'}
+wrapped_tests += {'name' : 'test-run-custom@user.wrap', 'script' : 'test-run-custom.sh'}
+wrapped_tests += {'name' : 'test-run-custom@system.wrap', 'script' : 'test-run-custom.sh'}
\ No newline at end of file
diff -Nru flatpak-1.16.3/tests/test-run-custom.sh flatpak-1.16.6/tests/test-run-custom.sh
--- flatpak-1.16.3/tests/test-run-custom.sh	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/tests/test-run-custom.sh	2026-04-10 18:28:42.000000000 +0100
@@ -0,0 +1,200 @@
+#!/bin/bash
+#
+# Copyright (C) 2026 Red Hat, Inc.
+#
+# 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 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, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+set -euo pipefail
+
+. "$(dirname "$0")/libtest.sh"
+
+skip_without_bwrap
+skip_revokefs_without_fuse
+
+echo "1..12"
+
+# Use stable rather than master as the branch so we can test that the run
+# command automatically finds the branch correctly
+setup_repo "" "" stable
+install_repo "" stable
+
+setup_repo_no_add custom org.test.Collection.Custom master
+make_updated_runtime custom org.test.Collection.Custom master CUSTOM
+make_updated_app custom org.test.Collection.Custom master CUSTOM
+
+ostree checkout -U --repo=repos/custom runtime/org.test.Platform/${ARCH}/master custom-runtime >&2
+ostree checkout -U --repo=repos/custom app/org.test.Hello/$ARCH/master custom-app >&2
+
+cat custom-runtime/files/bin/runtime_hello.sh > runtime_hello
+assert_file_has_content runtime_hello "runtimeCUSTOM"
+cat custom-app/files/bin/hello.sh > app_hello
+assert_file_has_content app_hello "sandboxCUSTOM"
+
+run org.test.Hello > hello_out
+assert_file_has_content hello_out '^Hello world, from a sandbox$'
+
+run --command=/usr/bin/runtime_hello.sh org.test.Hello > hello_out
+assert_file_has_content hello_out '^Hello world, from a runtime$'
+
+ok "setup"
+
+! run --app-path="" --command=/app/bin/hello.sh org.test.Hello > /dev/null
+
+run --app-path="" --command=/usr/bin/runtime_hello.sh org.test.Hello > hello_out
+assert_file_has_content hello_out '^Hello world, from a runtime$'
+
+run --app-path="" --command=/run/parent/app/bin/hello.sh org.test.Hello > hello_out
+assert_file_has_content hello_out '^Hello world, from a sandbox$'
+
+! run --app-path="" --command=/run/parent/usr/bin/runtime_hello.sh org.test.Hello > /dev/null
+
+ok "empty app path"
+
+run --app-path=custom-app/files --command=/app/bin/hello.sh org.test.Hello > hello_out
+assert_file_has_content hello_out '^Hello world, from a sandboxCUSTOM$'
+
+run --app-path=custom-app/files --command=/usr/bin/runtime_hello.sh org.test.Hello > hello_out
+assert_file_has_content hello_out '^Hello world, from a runtime$'
+
+run --app-path=custom-app/files --command=/run/parent/app/bin/hello.sh org.test.Hello > hello_out
+assert_file_has_content hello_out '^Hello world, from a sandbox$'
+
+! run --app-path=custom-app/files --command=/run/parent/usr/bin/runtime_hello.sh org.test.Hello > /dev/null
+
+ok "custom app path"
+
+! run --app-path=path-which-does-not-exist org.test.Hello > /dev/null
+
+ok "bad custom app path"
+
+exec 3< custom-app/files
+run --app-fd=3 --command=/app/bin/hello.sh org.test.Hello > hello_out
+assert_file_has_content hello_out '^Hello world, from a sandboxCUSTOM$'
+exec 3>&-
+
+! run --app-fd=3 --command=/app/bin/hello.sh org.test.Hello > /dev/null
+
+ok "custom app fd"
+
+run --usr-path=custom-runtime/files --command=/app/bin/hello.sh org.test.Hello > hello_out
+assert_file_has_content hello_out '^Hello world, from a sandbox$'
+
+run --usr-path=custom-runtime/files --command=/usr/bin/runtime_hello.sh org.test.Hello > hello_out
+assert_file_has_content hello_out '^Hello world, from a runtimeCUSTOM$'
+
+! run --usr-path=custom-runtime/files --command=/run/parent/app/bin/hello.sh org.test.Hello > /dev/null
+
+run --usr-path=custom-runtime/files --command=/run/parent/usr/bin/runtime_hello.sh org.test.Hello > hello_out
+assert_file_has_content hello_out '^Hello world, from a runtime$'
+
+ok "custom usr path"
+
+! run --usr-path=path-which-does-not-exist org.test.Hello > /dev/null
+
+ok "bad custom usr path"
+
+exec 3< custom-runtime/files
+run --usr-fd=3 --command=/app/bin/hello.sh org.test.Hello > hello_out
+assert_file_has_content hello_out '^Hello world, from a sandbox$'
+exec 3>&-
+
+exec 3< custom-runtime/files
+run --usr-fd=3 --command=/usr/bin/runtime_hello.sh org.test.Hello > hello_out
+assert_file_has_content hello_out '^Hello world, from a runtimeCUSTOM$'
+exec 3>&-
+
+! run --usr-fd=3 --command=/app/bin/hello.sh org.test.Hello > /dev/null
+
+ok "custom usr fd"
+
+run --usr-path=custom-runtime/files --app-path=custom-app/files \
+    --command=/app/bin/hello.sh org.test.Hello > hello_out
+assert_file_has_content hello_out '^Hello world, from a sandboxCUSTOM$'
+
+run --usr-path=custom-runtime/files --app-path=custom-app/files \
+    --command=/usr/bin/runtime_hello.sh org.test.Hello > hello_out
+assert_file_has_content hello_out '^Hello world, from a runtimeCUSTOM$'
+
+run --usr-path=custom-runtime/files --app-path=custom-app/files \
+    --command=/run/parent/app/bin/hello.sh org.test.Hello > hello_out
+assert_file_has_content hello_out '^Hello world, from a sandbox$'
+
+run --usr-path=custom-runtime/files --app-path=custom-app/files \
+    --command=/run/parent/usr/bin/runtime_hello.sh org.test.Hello > hello_out
+assert_file_has_content hello_out '^Hello world, from a runtime$'
+
+ok "custom usr and app path"
+
+! run --usr-path=custom-runtime/files --app-path="" \
+    --command=/app/bin/hello.sh org.test.Hello > /dev/null
+
+run --usr-path=custom-runtime/files --app-path="" \
+    --command=/usr/bin/runtime_hello.sh org.test.Hello > hello_out
+assert_file_has_content hello_out '^Hello world, from a runtimeCUSTOM$'
+
+run --usr-path=custom-runtime/files --app-path="" \
+    --command=/run/parent/app/bin/hello.sh org.test.Hello > hello_out
+assert_file_has_content hello_out '^Hello world, from a sandbox$'
+
+run --usr-path=custom-runtime/files --app-path="" \
+    --command=/run/parent/usr/bin/runtime_hello.sh org.test.Hello > hello_out
+assert_file_has_content hello_out '^Hello world, from a runtime$'
+
+ok "custom usr and empty app path"
+
+path="$(readlink -f .)/foo"
+echo "bar" > "${path}"
+
+exec 3< "${path}"
+run --bind-fd=3 --command=cat org.test.Hello "${path}" > hello_out
+assert_file_has_content hello_out '^bar$'
+
+exec 3< "${path}"
+run --bind-fd=3 --command=bash org.test.Hello -c "echo baz > ${path}" > /dev/null
+assert_file_has_content "${path}" '^baz$'
+exec 3>&-
+
+exec 3< "${path}"
+run --ro-bind-fd=3 --command=cat org.test.Hello "${path}" > hello_out
+assert_file_has_content hello_out '^baz$'
+exec 3>&-
+
+exec 3< "${path}"
+! run --ro-bind-fd=3 --command=bash org.test.Hello -c "echo baz > ${path}" > /dev/null
+exec 3>&-
+
+ok "bind-fd and ro-bind-fd"
+
+exec 3< custom-app/files
+exec 4< custom-runtime/files
+exec 5< "${path}"
+exec 6< "${path}"
+run --app-fd=3 --usr-fd=4 --bind-fd=5 --ro-bind-fd=6 \
+    --command=sh org.test.Hello \
+    -c 'for fd in $(ls /proc/self/fd); do readlink -f /proc/self/fd/$fd; done' > hello_out
+exec 6>&-
+exec 5>&-
+exec 4>&-
+exec 3>&-
+
+wd="$(readlink -f .)"
+while read fdpath; do
+  if [[ "$fdpath" == "$wd"* && "$fdpath" != "$wd/hello_out" ]]; then
+    assert_not_reached "A fd for '$fdpath' unexpectedly made it to the app"
+  fi
+done < hello_out
+
+ok "check no fd leak"
\ No newline at end of file
diff -Nru flatpak-1.16.3/tests/update-test-matrix flatpak-1.16.6/tests/update-test-matrix
--- flatpak-1.16.3/tests/update-test-matrix	2026-01-21 11:19:58.000000000 +0000
+++ flatpak-1.16.6/tests/update-test-matrix	2026-04-10 18:28:42.000000000 +0100
@@ -34,6 +34,7 @@
 	'tests/test-prune.sh' \
 	'tests/test-seccomp.sh' \
 	'tests/test-repair.sh' \
+	'tests/test-extra-data.sh{user+system}' \
 )
 
 "${tests_srcdir}/expand-test-matrix.sh" --meson "${TEST_MATRIX_SOURCE[*]}" > "${tests_srcdir}/test-matrix/meson.build"
