debdiff *.dsc | filterdiff -p1 -x'debian/patches/**.patch' >| flatpak_1.14.10-1~deb12u2.filtered.diff

diff -Nru flatpak-1.14.10/app/flatpak-builtins-build.c flatpak-1.14.10/app/flatpak-builtins-build.c
--- flatpak-1.14.10/app/flatpak-builtins-build.c	2024-04-17 18:19:04.000000000 +0100
+++ flatpak-1.14.10/app/flatpak-builtins-build.c	2026-04-15 21:38:15.000000000 +0100
@@ -457,7 +457,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.14.10/app/flatpak-builtins-run.c flatpak-1.14.10/app/flatpak-builtins-run.c
--- flatpak-1.14.10/app/flatpak-builtins-run.c	2024-04-17 18:19:04.000000000 +0100
+++ flatpak-1.14.10/app/flatpak-builtins-run.c	2026-04-15 21:38:15.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 }
 };
 
@@ -110,6 +205,11 @@
   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;
+
+  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);
@@ -306,14 +406,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,
@@ -322,6 +475,8 @@
                         rest_argc - 1,
                         opt_instance_id_fd,
                         NULL,
+                        opt_bind_fds,
+                        opt_ro_bind_fds,
                         cancellable,
                         error))
     return FALSE;
diff -Nru flatpak-1.14.10/common/flatpak-bwrap.c flatpak-1.14.10/common/flatpak-bwrap.c
--- flatpak-1.14.10/common/flatpak-bwrap.c	2024-04-17 18:19:04.000000000 +0100
+++ flatpak-1.14.10/common/flatpak-bwrap.c	2026-04-15 21:38:15.000000000 +0100
@@ -41,15 +41,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 *
@@ -59,9 +50,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);
@@ -141,6 +132,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, ...)
 {
@@ -511,7 +522,8 @@
          us use the same fd_array multiple times */
       if (lseek (fd, 0, SEEK_SET) < 0)
         {
-          /* Ignore the error, this happens on e.g. pipe fds */
+          /* Ignore the error, not all fds are seekable
+           * (for example pipes and O_PATH fds are not) */
         }
 
       fcntl (fd, F_SETFD, 0);
diff -Nru flatpak-1.14.10/common/flatpak-bwrap-private.h flatpak-1.14.10/common/flatpak-bwrap-private.h
--- flatpak-1.14.10/common/flatpak-bwrap-private.h	2024-04-17 18:19:04.000000000 +0100
+++ flatpak-1.14.10/common/flatpak-bwrap-private.h	2026-04-15 21:38:15.000000000 +0100
@@ -62,6 +62,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.14.10/common/flatpak-context.c flatpak-1.14.10/common/flatpak-context.c
--- flatpak-1.14.10/common/flatpak-context.c	2024-08-14 13:45:33.000000000 +0100
+++ flatpak-1.14.10/common/flatpak-context.c	2026-04-15 21:38:15.000000000 +0100
@@ -1304,21 +1304,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.14.10/common/flatpak-dir.c flatpak-1.14.10/common/flatpak-dir.c
--- flatpak-1.14.10/common/flatpak-dir.c	2024-08-12 19:40:18.000000000 +0100
+++ flatpak-1.14.10/common/flatpak-dir.c	2026-04-15 21:38:15.000000000 +0100
@@ -8183,7 +8183,10 @@
       /* We also seek all fds to the start, because this lets
          us use the same fd_array multiple times */
       if (lseek (fd, 0, SEEK_SET) < 0)
-        g_printerr ("lseek error in child setup");
+        {
+          /* Ignore the error, not all fds are seekable
+           * (for example pipes and O_PATH fds are not) */
+        }
 
       fcntl (fd, F_SETFD, 0);
     }
@@ -8216,6 +8219,7 @@
   int exit_status;
   const char *group = FLATPAK_METADATA_GROUP_APPLICATION;
   g_autoptr(GError) local_error = NULL;
+  FlatpakRunFlags run_flags;
 
   apply_extra_file = g_file_resolve_relative_path (checkoutdir, "files/bin/apply_extra");
   if (!g_file_query_exists (apply_extra_file, cancellable))
@@ -8292,11 +8296,29 @@
                           "--cap-drop", "ALL",
                           NULL);
 
-  if (!flatpak_run_setup_base_argv (bwrap, runtime_files, NULL, runtime_arch,
-                                    /* Might need multiarch in apply_extra (see e.g. #3742). Should be pretty safe in this limited context */
-                                    FLATPAK_RUN_FLAG_MULTIARCH |
-                                    FLATPAK_RUN_FLAG_NO_SESSION_HELPER | FLATPAK_RUN_FLAG_NO_PROC,
-                                    error))
+  run_flags = FLATPAK_RUN_FLAG_NO_SESSION_HELPER;
+
+  /* Might need multiarch in apply_extra (see e.g. #3742).
+   * Should be pretty safe in this limited context. */
+  run_flags |= FLATPAK_RUN_FLAG_MULTIARCH;
+
+  /* This sandbox is run as root and /proc/self/exe can sometimes be used to
+   * access outside files (see cd21428).
+   * Disable /proc entirely in this context. */
+  run_flags |= FLATPAK_RUN_FLAG_NO_PROC;
+
+  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;
 
   app_context = flatpak_context_new ();
diff -Nru flatpak-1.14.10/common/flatpak-installation.c flatpak-1.14.10/common/flatpak-installation.c
--- flatpak-1.14.10/common/flatpak-installation.c	2024-04-17 18:19:04.000000000 +0100
+++ flatpak-1.14.10/common/flatpak-installation.c	2026-04-15 21:38:15.000000000 +0100
@@ -700,15 +700,17 @@
 
   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,
                         NULL,
                         NULL, 0, -1,
                         &instance_dir,
+                        NULL, NULL,
                         cancellable, error))
     return FALSE;
 
diff -Nru flatpak-1.14.10/common/flatpak-oci-registry.c flatpak-1.14.10/common/flatpak-oci-registry.c
--- flatpak-1.14.10/common/flatpak-oci-registry.c	2024-04-17 18:19:04.000000000 +0100
+++ flatpak-1.14.10/common/flatpak-oci-registry.c	2026-04-15 21:38:15.000000000 +0100
@@ -265,6 +265,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,
@@ -276,7 +279,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.14.10/common/flatpak-run.c flatpak-1.14.10/common/flatpak-run.c
--- flatpak-1.14.10/common/flatpak-run.c	2024-08-12 19:40:18.000000000 +0100
+++ flatpak-1.14.10/common/flatpak-run.c	2026-04-15 21:38:15.000000000 +0100
@@ -2676,13 +2676,17 @@
                                gboolean            build,
                                gboolean            devel,
                                char              **app_info_path_out,
-                               int                 instance_id_fd,
+                               int                 instance_id_fd_arg,
                                char              **instance_id_host_dir_out,
                                GError             **error)
 {
   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;
@@ -2828,8 +2832,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),
@@ -2840,7 +2844,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));
@@ -2849,9 +2852,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,
@@ -2868,8 +2871,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));
@@ -2892,10 +2893,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;
@@ -2905,13 +2902,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);
@@ -2919,40 +2917,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 = "/usr/share/zoneinfo";
+
+  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)
     {
-      /* Check for host /usr/share/zoneinfo */
-      if (g_file_test ("/usr/share/zoneinfo", G_FILE_TEST_IS_DIR))
+      g_autofree char *runtime_zoneinfo = NULL;
+      glnx_autofd int runtime_zoneinfo_fd = -1;
+
+      runtime_zoneinfo = g_strconcat ("share/zoneinfo/", raw_timezone, NULL);
+
+      /* 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", "/usr/share/zoneinfo", "/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",
@@ -3432,26 +3455,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;
@@ -3465,17 +3509,16 @@
                                   "--symlink", link, create,
                                   NULL);
         }
-      else
-        {
-          g_debug ("%s does not exist",
-                   flatpak_file_get_path_cached (runtime_subdir));
-        }
     }
 }
 
+/*
+ * @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,
@@ -3488,7 +3531,8 @@
   struct group *g;
   gulong pers;
   gid_t gid = getgid ();
-  g_autoptr(GFile) etc = NULL;
+
+  g_return_val_if_fail (runtime_fd >= -1, FALSE);
 
   run_dir = g_strdup_printf ("/run/user/%d", getuid ());
 
@@ -3563,22 +3607,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;
@@ -3595,9 +3643,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;
 
@@ -3608,9 +3666,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);
             }
         }
     }
@@ -3631,9 +3692,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;
 
@@ -3860,7 +3921,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)
@@ -3900,7 +3961,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)
     {
@@ -4117,12 +4178,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,
@@ -4131,17 +4192,14 @@
                  int                n_args,
                  int                instance_id_fd,
                  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;
@@ -4173,18 +4231,39 @@
   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_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.
    */
@@ -4303,38 +4382,53 @@
     flatpak_context_merge (app_context, extra_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)
+    {
+      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)
     {
-      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";
+      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);
@@ -4421,19 +4515,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");
+
+      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
+    {
+      g_assert_not_reached ();
+    }
 
-      /* Mount the original app below here */
-      app_target_path = "/run/parent/app";
+  /* 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 if (original_app_files != NULL)
+  else
     {
-      app_files = g_object_ref (original_app_files);
+      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);
@@ -4446,75 +4582,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;
 
-      g_clear_object (&runtime_child);
-      runtime_child = g_file_get_child (original_runtime_files, "etc");
+      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);
+        }
 
-      if (g_file_query_exists (runtime_child, NULL))
-        flatpak_bwrap_add_args (bwrap,
-                                "--symlink", "usr/etc", "/run/parent/etc",
-                                NULL);
+      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);
+        }
+
+      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
     {
@@ -4523,7 +4670,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,
@@ -4536,26 +4683,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) */
@@ -4567,7 +4725,7 @@
                                       bwrap->fds,
                                       app_id_dir,
                                       checksum,
-                                      runtime_files,
+                                      runtime_fd,
                                       generate_ld_so_conf,
                                       cancellable, error);
       if (ld_so_fd == -1)
@@ -4577,7 +4735,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)
@@ -4600,7 +4758,9 @@
                                       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, &instance_id_host_dir,
+                                      &app_info_path,
+                                      g_steal_fd (&instance_id_fd),
+                                      &instance_id_host_dir,
                                       error))
     return FALSE;
 
@@ -4658,6 +4818,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.14.10/common/flatpak-run-private.h flatpak-1.14.10/common/flatpak-run-private.h
--- flatpak-1.14.10/common/flatpak-run-private.h	2024-04-17 18:19:04.000000000 +0100
+++ flatpak-1.14.10/common/flatpak-run-private.h	2026-04-15 21:38:15.000000000 +0100
@@ -28,6 +28,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,
                                         GError    **error);
 
@@ -151,7 +156,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,
@@ -181,12 +186,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,
@@ -195,6 +200,8 @@
                           int                 n_args,
                           int                 instance_id_fd,
                           char              **instance_dir_out,
+                          GArray             *bind_fds,
+                          GArray             *ro_bind_fds,
                           GCancellable       *cancellable,
                           GError            **error);
 
diff -Nru flatpak-1.14.10/common/flatpak-utils.c flatpak-1.14.10/common/flatpak-utils.c
--- flatpak-1.14.10/common/flatpak-utils.c	2024-04-17 18:19:04.000000000 +0100
+++ flatpak-1.14.10/common/flatpak-utils.c	2026-04-15 21:38:15.000000000 +0100
@@ -1407,6 +1407,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
@@ -1416,6 +1432,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,
@@ -1459,10 +1478,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)
@@ -9419,3 +9449,139 @@
 }
 
 #endif /* GLIB_CHECK_VERSION (2, 68, 0) */
+
+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.14.10/common/flatpak-utils-private.h flatpak-1.14.10/common/flatpak-utils-private.h
--- flatpak-1.14.10/common/flatpak-utils-private.h	2024-04-17 18:19:04.000000000 +0100
+++ flatpak-1.14.10/common/flatpak-utils-private.h	2026-04-15 21:38:15.000000000 +0100
@@ -944,6 +944,18 @@
 
 gboolean running_under_sudo (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.14.10/common/Makefile.am.inc flatpak-1.14.10/common/Makefile.am.inc
--- flatpak-1.14.10/common/Makefile.am.inc	2024-04-17 18:19:04.000000000 +0100
+++ flatpak-1.14.10/common/Makefile.am.inc	2026-04-15 21:38:15.000000000 +0100
@@ -222,6 +222,7 @@
 	$(SYSTEMD_LIBS) \
 	$(XAUTH_LIBS) \
 	$(XML_LIBS) \
+	libglnx.la \
 	$(NULL)
 
 
@@ -253,7 +254,6 @@
 	$(AM_LIBADD) \
 	libflatpak-common.la \
 	libflatpak-common-base.la \
-	libglnx.la \
 	$(BASE_LIBS)	\
 	$(OSTREE_LIBS)	\
 	$(CURL_LIBS)	\
diff -Nru flatpak-1.14.10/debian/changelog flatpak-1.14.10/debian/changelog
--- flatpak-1.14.10/debian/changelog	2024-08-14 15:49:20.000000000 +0100
+++ flatpak-1.14.10/debian/changelog	2026-04-15 20:27:40.000000000 +0100
@@ -1,3 +1,49 @@
+flatpak (1.14.10-1~deb12u2) bookworm-security; urgency=high
+
+  * Security update
+  * d/p/CVE-2026-34078-prep/*.patch:
+    Backport libglnx changes required to address CVE-2026-34078
+  * d/p/CVE-2026-34078/*.patch:
+    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)
+  * d/p/CVE-2026-34079/*.patch:
+    Prevent arbitrary file deletion outside the sandbox by a malicious or
+    compromised Flatpak app
+    (CVE-2026-34079, GHSA-p29x-r292-46pp) (Closes: #1132944)
+  * d/p/GHSA-2fxp-43j9-pwvc/*.patch:
+    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)
+  * d/p/GHSA-89xm-3m96-w3jg/*.patch:
+    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)
+  * d/p/portal-Use-G_LOCK_DEFINE_STATIC.patch,
+    d/p/portal-Don-t-run-method-invocations-in-a-thread.patch:
+    Add patches from upstream flatpak-1.14.x branch (which never got into a
+    release before the branch was discontinued), originally from 1.16.1,
+    fixing a thread-safety issue in flatpak-portal
+  * d/p/1.16.5/*.patch:
+    Add regression fixes taken from the upstream 1.16.5 release,
+    fixing various regressions introduced by fixing CVE-2026-34078
+    and improving test coverage
+    (Closes: #1132960)
+  * d/p/1.16.6/*.patch:
+    Add regression fixes taken from the upstream 1.16.6 release,
+    fixing additional regressions introduced by fixing CVE-2026-34078
+    and improving test coverage
+    (Closes: #1132968)
+    - d/control: Add curl(1) to Build-Depends and flatpak-tests Depends
+  * d/p/1.16.7/bwrap-Clarify-a-comment.patch,
+    d/p/dir-Silence-a-spurious-warning-when-installing-extra-data.patch:
+    Silence a spurious warning seen while testing 1.16.6
+
+ -- Simon McVittie <smcv@debian.org>  Wed, 15 Apr 2026 20:27:40 +0100
+
 flatpak (1.14.10-1~deb12u1) bookworm-security; urgency=high
 
   * Backport upstream stable release into Debian 12 (CVE-2024-42472)
diff -Nru flatpak-1.14.10/debian/control flatpak-1.14.10/debian/control
--- flatpak-1.14.10/debian/control	2024-08-14 15:49:20.000000000 +0100
+++ flatpak-1.14.10/debian/control	2026-04-15 20:27:40.000000000 +0100
@@ -12,6 +12,7 @@
  bubblewrap (>= 0.8.0-2+deb12u1~),
  bubblewrap (<< 0.8.1~) | bubblewrap (>= 0.10.0~),
  ca-certificates <!nocheck>,
+ curl <!nocheck>,
  dbus-daemon,
  debhelper-compat (= 13),
  desktop-file-utils <!nocheck>,
@@ -121,6 +122,7 @@
 Depends:
  attr,
  ca-certificates,
+ curl,
  dbus-daemon,
  desktop-file-utils,
  flatpak (= ${binary:Version}),
diff -Nru flatpak-1.14.10/debian/patches/series flatpak-1.14.10/debian/patches/series
--- flatpak-1.14.10/debian/patches/series	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.14.10/debian/patches/series	2026-04-15 20:27:40.000000000 +0100
@@ -0,0 +1,53 @@
+portal-Use-G_LOCK_DEFINE_STATIC.patch
+portal-Don-t-run-method-invocations-in-a-thread.patch
+CVE-2026-34078-prep/backports-Add-g_clear_fd.patch
+CVE-2026-34078-prep/glnx-errors.h-add-glnx_fd_throw-_-variants.patch
+CVE-2026-34078-prep/fdio-Add-glnx_fd_reopen.patch
+CVE-2026-34078-prep/missing-Add-syscall-and-structs-for-statx.patch
+CVE-2026-34078-prep/fdio-Add-glnx_statx.patch
+CVE-2026-34078-prep/missing-Add-syscall-number-for-openat2-and-open_tree.patch
+CVE-2026-34078-prep/chase-Add-glnx_chaseat-which-functions-similar-to-openat2.patch
+CVE-2026-34078-prep/chase-Add-glnx_chase_and_statxat.patch
+CVE-2026-34078-prep/chase-Don-t-left-shift-signed-integer-1-by-31-places.patch
+CVE-2026-34078-prep/chase-Don-t-leak-struct-glnx_statx-when-we-go-up-a-level.patch
+CVE-2026-34078-prep/chase-Factor-out-a-function-to-append-to-the-queue.patch
+CVE-2026-34078-prep/build-Add-glnx-chase.-ch-to-subprojects.patch
+CVE-2026-34078-prep/build-Link-libglnx-into-libflatpak-common-not-just-into-l.patch
+CVE-2026-34078/flatpak-bwrap-Add-dup-ing-variant-flatpak_bwrap_add_args_.patch
+CVE-2026-34078/utils-Add-flatpak_parse_fd.patch
+CVE-2026-34078/flatpak-bwrap-Use-glnx_close_fd-as-clear-func.patch
+CVE-2026-34078/run-Use-O_PATH-fds-for-the-runtime-and-app-deploy-directo.patch
+CVE-2026-34078/run-Add-usr-fd-and-app-fd-options.patch
+CVE-2026-34078/run-Add-ro-bind-fds-to-flatpak_run_app.patch
+CVE-2026-34078/run-Add-ro-bind-fd-options.patch
+CVE-2026-34078/portal-Use-bind-fd-app-fd-and-usr-fd-options-to-avoid-rac.patch
+CVE-2026-34079/utils-Only-remove-cached-files-in-the-cache-directory.patch
+GHSA-2fxp-43j9-pwvc/utils-Do-not-follow-symlinks-in-local_open_file.patch
+GHSA-89xm-3m96-w3jg/system-helper-Only-remove-an-ongoing-pull-if-users-match.patch
+1.16.5/run-Fix-checking-wrong-variable-in-runtime-fd-selection.patch
+1.16.5/run-Mount-original-app-on-run-parent-app-when-using-app-p.patch
+1.16.5/portal-update-max_fd-after-creating-the-instance-ID-pipe.patch
+1.16.5/run-Fix-fd-tracking-in-flatpak_run_add_app_info_args.patch
+1.16.5/utils-Improve-error-message-when-passing-an-FD-numer-whic.patch
+1.16.5/run-Do-not-close-bind-ro-bind.patch
+1.16.5/run-Use-the-same-FD-validation-for-all-FD-options.patch
+1.16.5/run-Add-bind-fd-and-ro-bind-fd-binds-after-all-other-bind.patch
+1.16.5/portal-use-g_array_index-to-read-from-expose_fds-expose_f.patch
+1.16.5/run-Fix-backport-mistake.patch
+1.16.5/tests-test-run-custom-Test-usr-path-usr-fd-app-path-app-f.patch
+1.16.5/tests-test-run-custom-Test-bind-fd-and-ro-bind-fd.patch
+1.16.6/run-Cope-with-an-empty-runtime.patch
+1.16.6/dir-In-apply_extra_data-don-t-assume-there-is-always-a-ru.patch
+1.16.6/tests-Add-test-extra-data.sh-to-test-extra-data-installat.patch
+1.16.6/utils-Add-flatpak_set_cloexec.patch
+1.16.6/run-context-Mark-fd-arguments-as-close-on-exec.patch
+1.16.6/utils-Move-flatpak_get_path_for_fd-to-here.patch
+1.16.6/portal-Avoid-crash-if-sandbox-expose-ro-fd-is-out-of-rang.patch
+1.16.6/portal-Log-and-ignore-unusable-sandbox-expose-fds-instead.patch
+1.16.6/portal-Reinstate-flatpak_get_path_for_fd-checks.patch
+1.16.6/libtest-Allow-adding-a-new-ref-to-an-existing-temporary-o.patch
+1.16.6/tests-Check-that-flatpak-run-fd-arguments-do-not-leak-to-.patch
+1.16.6/app-context-Never-close-fds-0-1-or-2.patch
+1.16.6/app-context-Factor-out-flatpak_accept_fd_argument.patch
+1.16.7/bwrap-Clarify-a-comment.patch
+dir-Silence-a-spurious-warning-when-installing-extra-data.patch
diff -Nru flatpak-1.14.10/portal/flatpak-portal.c flatpak-1.14.10/portal/flatpak-portal.c
--- flatpak-1.14.10/portal/flatpak-portal.c	2024-04-17 18:19:04.000000000 +0100
+++ flatpak-1.14.10/portal/flatpak-portal.c	2026-04-15 21:38:15.000000000 +0100
@@ -76,7 +76,7 @@
 static gboolean opt_poll_when_metered;
 static FlatpakSpawnSupportFlags supports = 0;
 
-G_LOCK_DEFINE (update_monitors); /* This protects the three variables below */
+G_LOCK_DEFINE_STATIC (update_monitors); /* This protects the three variables below */
 static GHashTable *update_monitors;
 static guint update_monitors_timeout = 0;
 static gboolean update_monitors_timeout_running_thread = FALSE;
@@ -551,195 +551,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;
+
+  if (needs_writable)
+    access_mode |= W_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");
+  /* 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");
 
-  if (access (proc_path, W_OK) == 0)
-    writable = TRUE;
+  return TRUE;
+}
 
-  if (writable_out != NULL)
-    *writable_out = writable;
+static int
+fd_map_remap_fd (GArray *fd_map,
+                 int    *max_fd_in_out,
+                 int     fd)
+{
+  FdMapEntry fd_map_entry;
+
+  /* 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
@@ -794,9 +679,12 @@
   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_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;
@@ -919,29 +807,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_debug ("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_debug ("exposing %s", expose);
-      if (!is_valid_expose (expose, &error))
-        {
-          g_dbus_method_invocation_return_gerror (invocation, error);
-          return G_DBUS_METHOD_INVOCATION_HANDLED;
-        }
-    }
-
   g_debug ("Running spawn command %s", arg_argv[0]);
 
   n_fds = 0;
@@ -1115,10 +980,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,
@@ -1129,16 +998,12 @@
         }
 
       env_fd = glnx_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++)
@@ -1233,6 +1098,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)
@@ -1246,111 +1112,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)
+  if (instance_path != NULL)
     {
-      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));
+      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;
+        }
+    }
+
+  for (i = 0; sandbox_expose != NULL && sandbox_expose[i] != NULL; i++)
+    {
+      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_debug ("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_debug ("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_debug ("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)
         {
@@ -1362,18 +1295,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_debug ("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)
     {
@@ -1382,8 +1308,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)
         {
@@ -1395,18 +1321,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_debug ("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]));
@@ -2905,9 +2824,6 @@
 
   g_object_set_data_full (G_OBJECT (portal), "track-alive", GINT_TO_POINTER (42), skeleton_died_cb);
 
-  g_dbus_interface_skeleton_set_flags (G_DBUS_INTERFACE_SKELETON (portal),
-                                       G_DBUS_INTERFACE_SKELETON_FLAGS_HANDLE_METHOD_INVOCATIONS_IN_THREAD);
-
   portal_flatpak_set_version (PORTAL_FLATPAK (portal), 6);
   portal_flatpak_set_supports (PORTAL_FLATPAK (portal), supports);
 
diff -Nru flatpak-1.14.10/subprojects/libglnx/glnx-backports.c flatpak-1.14.10/subprojects/libglnx/glnx-backports.c
--- flatpak-1.14.10/subprojects/libglnx/glnx-backports.c	2024-08-12 19:40:53.000000000 +0100
+++ flatpak-1.14.10/subprojects/libglnx/glnx-backports.c	2026-04-15 21:38:15.000000000 +0100
@@ -60,3 +60,75 @@
   return TRUE;
 }
 #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
diff -Nru flatpak-1.14.10/subprojects/libglnx/glnx-backports.h flatpak-1.14.10/subprojects/libglnx/glnx-backports.h
--- flatpak-1.14.10/subprojects/libglnx/glnx-backports.h	2024-08-12 19:40:53.000000000 +0100
+++ flatpak-1.14.10/subprojects/libglnx/glnx-backports.h	2026-04-15 21:38:15.000000000 +0100
@@ -24,6 +24,7 @@
 
 #pragma once
 
+#include <glib/gstdio.h>
 #include <gio/gio.h>
 
 G_BEGIN_DECLS
@@ -48,6 +49,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, 44, 0)
 
 #define g_strv_contains glnx_strv_contains
diff -Nru flatpak-1.14.10/subprojects/libglnx/glnx-chase.c flatpak-1.14.10/subprojects/libglnx/glnx-chase.c
--- flatpak-1.14.10/subprojects/libglnx/glnx-chase.c	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.14.10/subprojects/libglnx/glnx-chase.c	2026-04-15 21:38:15.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.14.10/subprojects/libglnx/glnx-chase.h flatpak-1.14.10/subprojects/libglnx/glnx-chase.h
--- flatpak-1.14.10/subprojects/libglnx/glnx-chase.h	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.14.10/subprojects/libglnx/glnx-chase.h	2026-04-15 21:38:15.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.14.10/subprojects/libglnx/glnx-errors.h flatpak-1.14.10/subprojects/libglnx/glnx-errors.h
--- flatpak-1.14.10/subprojects/libglnx/glnx-errors.h	2024-08-12 19:40:53.000000000 +0100
+++ flatpak-1.14.10/subprojects/libglnx/glnx-errors.h	2026-04-15 21:38:15.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,
@@ -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.14.10/subprojects/libglnx/glnx-fdio.c flatpak-1.14.10/subprojects/libglnx/glnx-fdio.c
--- flatpak-1.14.10/subprojects/libglnx/glnx-fdio.c	2024-08-12 19:40:53.000000000 +0100
+++ flatpak-1.14.10/subprojects/libglnx/glnx-fdio.c	2026-04-15 21:38:15.000000000 +0100
@@ -1194,3 +1194,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.14.10/subprojects/libglnx/glnx-fdio.h flatpak-1.14.10/subprojects/libglnx/glnx-fdio.h
--- flatpak-1.14.10/subprojects/libglnx/glnx-fdio.h	2024-08-12 19:40:53.000000000 +0100
+++ flatpak-1.14.10/subprojects/libglnx/glnx-fdio.h	2026-04-15 21:38:15.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.14.10/subprojects/libglnx/glnx-missing-syscall.h flatpak-1.14.10/subprojects/libglnx/glnx-missing-syscall.h
--- flatpak-1.14.10/subprojects/libglnx/glnx-missing-syscall.h	2024-08-12 19:40:53.000000000 +0100
+++ flatpak-1.14.10/subprojects/libglnx/glnx-missing-syscall.h	2026-04-15 21:38:15.000000000 +0100
@@ -32,6 +32,8 @@
 */
 
 #include "libglnx-config.h"
+#include <glib.h>
+#include <stdint.h>
 
 #if !HAVE_DECL_RENAMEAT2
 #  ifndef __NR_renameat2
@@ -155,3 +157,355 @@
 
 #  define copy_file_range missing_copy_file_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.14.10/subprojects/libglnx/libglnx.h flatpak-1.14.10/subprojects/libglnx/libglnx.h
--- flatpak-1.14.10/subprojects/libglnx/libglnx.h	2024-08-12 19:40:53.000000000 +0100
+++ flatpak-1.14.10/subprojects/libglnx/libglnx.h	2026-04-15 21:38:15.000000000 +0100
@@ -30,6 +30,7 @@
 #include <glnx-local-alloc.h>
 #include <glnx-backport-autocleanups.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.14.10/subprojects/Makefile-libglnx.am.inc flatpak-1.14.10/subprojects/Makefile-libglnx.am.inc
--- flatpak-1.14.10/subprojects/Makefile-libglnx.am.inc	2024-08-14 14:40:45.000000000 +0100
+++ flatpak-1.14.10/subprojects/Makefile-libglnx.am.inc	2026-04-15 21:38:15.000000000 +0100
@@ -35,6 +35,8 @@
 	subprojects/libglnx/glnx-backport-autoptr.h \
 	subprojects/libglnx/glnx-backports.h \
 	subprojects/libglnx/glnx-backports.c \
+	subprojects/libglnx/glnx-chase.h \
+	subprojects/libglnx/glnx-chase.c \
 	subprojects/libglnx/glnx-local-alloc.h \
 	subprojects/libglnx/glnx-local-alloc.c \
 	subprojects/libglnx/glnx-errors.h \
diff -Nru flatpak-1.14.10/system-helper/flatpak-system-helper.c flatpak-1.14.10/system-helper/flatpak-system-helper.c
--- flatpak-1.14.10/system-helper/flatpak-system-helper.c	2024-04-17 18:19:04.000000000 +0100
+++ flatpak-1.14.10/system-helper/flatpak-system-helper.c	2026-04-15 21:38:15.000000000 +0100
@@ -355,23 +355,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);
@@ -423,6 +431,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,
@@ -430,30 +441,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,
@@ -735,31 +733,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.14.10/tests/libtest.sh flatpak-1.14.10/tests/libtest.sh
--- flatpak-1.14.10/tests/libtest.sh	2024-04-17 18:19:04.000000000 +0100
+++ flatpak-1.14.10/tests/libtest.sh	2026-04-15 21:38:15.000000000 +0100
@@ -308,7 +308,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.14.10/tests/Makefile-test-matrix.am.inc flatpak-1.14.10/tests/Makefile-test-matrix.am.inc
--- flatpak-1.14.10/tests/Makefile-test-matrix.am.inc	2024-04-17 18:19:04.000000000 +0100
+++ flatpak-1.14.10/tests/Makefile-test-matrix.am.inc	2026-04-15 21:38:15.000000000 +0100
@@ -6,6 +6,8 @@
 	tests/test-run@system,deltas.wrap \
 	tests/test-run@system-norevokefs,nodeltas.wrap \
 	tests/test-run@system-norevokefs,deltas.wrap \
+	tests/test-run-custom@user.wrap \
+	tests/test-run-custom@system.wrap \
 	tests/test-info@user.wrap \
 	tests/test-info@system.wrap \
 	tests/test-repo@user.wrap \
@@ -28,6 +30,8 @@
 	tests/test-summaries@system.wrap \
 	tests/test-subset@user.wrap \
 	tests/test-subset@system.wrap \
+	tests/test-extra-data@user.wrap \
+	tests/test-extra-data@system.wrap \
 	$(NULL)
 TEST_MATRIX_DIST= \
 	tests/test-basic.sh \
@@ -49,6 +53,7 @@
 	$(NULL)
 TEST_MATRIX_EXTRA_DIST= \
 	tests/test-run.sh \
+	tests/test-run-custom.sh \
 	tests/test-info.sh \
 	tests/test-repo.sh \
 	tests/test-sideload.sh \
@@ -58,4 +63,5 @@
 	tests/test-update-portal.sh \
 	tests/test-summaries.sh \
 	tests/test-subset.sh \
+	tests/test-extra-data.sh \
 	$(NULL)
diff -Nru flatpak-1.14.10/tests/test-extra-data.sh flatpak-1.14.10/tests/test-extra-data.sh
--- flatpak-1.14.10/tests/test-extra-data.sh	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.14.10/tests/test-extra-data.sh	2026-04-15 21:38:15.000000000 +0100
@@ -0,0 +1,63 @@
+#!/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..1"
+
+# 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"
diff -Nru flatpak-1.14.10/tests/test-run-custom.sh flatpak-1.14.10/tests/test-run-custom.sh
--- flatpak-1.14.10/tests/test-run-custom.sh	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.14.10/tests/test-run-custom.sh	2026-04-15 21:38:15.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.14.10/tests/update-test-matrix flatpak-1.14.10/tests/update-test-matrix
--- flatpak-1.14.10/tests/update-test-matrix	2024-04-17 18:19:04.000000000 +0100
+++ flatpak-1.14.10/tests/update-test-matrix	2026-04-15 21:38:15.000000000 +0100
@@ -14,6 +14,7 @@
 	'tests/test-build-update-repo.sh' \
 	'tests/test-http-utils.sh' \
 	'tests/test-run.sh{{user+system+system-norevokefs},{nodeltas+deltas}}' \
+	'tests/test-run-custom.sh{user+system}' \
 	'tests/test-info.sh{user+system}' \
 	'tests/test-repo.sh{{user+system+system-norevokefs}+{{user+system},oldsummary}}' \
 	'tests/test-history.sh' \
@@ -34,6 +35,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" --automake "${TEST_MATRIX_SOURCE[*]}" > "${tests_srcdir}/Makefile-test-matrix.am.inc"
