diffstat for flatpak-1.16.6 flatpak-1.16.6

 app/flatpak-builtins-build-init.c                                                                                      |  220 +++--
 common/flatpak-bwrap.c                                                                                                 |    3 
 common/flatpak-dir-private.h                                                                                           |    5 
 common/flatpak-dir-utils.c                                                                                             |   51 +
 common/flatpak-dir.c                                                                                                   |  376 ++++++---
 common/flatpak-installation.c                                                                                          |    8 
 common/flatpak-oci-registry.c                                                                                          |   45 -
 common/flatpak-ref-utils-private.h                                                                                     |    3 
 common/flatpak-ref-utils.c                                                                                             |   43 -
 common/flatpak-run-dbus.c                                                                                              |    4 
 common/flatpak-run.c                                                                                                   |  220 ++++-
 common/flatpak-utils-private.h                                                                                         |   15 
 common/flatpak-utils.c                                                                                                 |  243 ++----
 debian/changelog                                                                                                       |   68 +
 debian/patches/GHSA-8688-9x26-hhxj/run-Harden-ensure_data_dir-and-var-setup-against-symlink-.patch                     |  132 +++
 debian/patches/GHSA-8688-9x26-hhxj/test-run-Check-that-the-way-com.valvesoftware.Steam-behav.patch                     |  107 ++
 debian/patches/GHSA-8688-9x26-hhxj/tests-Add-a-regression-test-for-GHSA-8688-9x26-hhxj.patch                           |   78 +
 debian/patches/GHSA-99wv-m8rp-g58x/common-Fix-return-value-and-typos-in-flatpak_switch_symli.patch                     |   50 +
 debian/patches/GHSA-99wv-m8rp-g58x/common-Use-fd-based-operations-in-flatpak_switch_symlink_.patch                     |  193 ++++
 debian/patches/GHSA-99wv-m8rp-g58x/run-Harden-regenerate_ld_cache-against-symlink-attacks.patch                        |  173 ++++
 debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/build-init-Use-fd-relative-operations-to-prevent-path-tra.patch |  341 ++++++++
 debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/dir-Use-chaseat-in-apply_extra_data-to-prevent-path-trave.patch |  255 ++++++
 debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/dir-Use-chaseat-in-extract_extra_data-to-prevent-path-tra.patch |  159 ++++
 debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/dir-Validate-extra-data-name.patch                              |   42 +
 debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/utils-Add-flatpak_cp_a_at-a-fd-based-flatpak_cp_a-variant.patch |  127 +++
 debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/utils-Make-flatpak_cp_a-a-wrapper-around-flatpak_cp_a_at.patch  |  196 ++++
 debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/utils-Remove-unused-GFile-based-flatpak_cp_a.patch              |   67 +
 debian/patches/GHSA-jr92-2v97-wgvc/oci-Avoid-overflow-in-delta_read_data.patch                                         |   36 
 debian/patches/GHSA-jr92-2v97-wgvc/oci-Limit-delta-path-length-to-PATH_MAX.patch                                       |   66 +
 debian/patches/GHSA-jr92-2v97-wgvc/oci-Use-gsize-for-OCI-delta-sizes.patch                                             |   76 +
 debian/patches/GHSA-q4gr-vc25-57m5/dir-Fall-back-to-the-deployed-ref-commit-when-up-downgrad.patch                     |   36 
 debian/patches/GHSA-q4gr-vc25-57m5/system-dir-Prevent-removing-of-deployed-refs-via-the-syst.patch                     |  193 ++++
 debian/patches/GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-in-chmod.patch                                |  114 ++
 debian/patches/GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-in-link-rename.patch                          |  111 ++
 debian/patches/GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-in-symlink.patch                              |   87 ++
 debian/patches/GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-out-of-basefd-in-si.patch                     |  219 +++++
 debian/patches/GHSA-r7hp-698j-2h6c/run-dbus-Correct-broadcast-rules-for-the-AT-SPI-bus.patch                           |   34 
 debian/patches/GHSA-v2gw-v9h5-9q4x/ref-utils-Make-remote-name-validation-public-and-take-an-.patch                     |   96 ++
 debian/patches/GHSA-v2gw-v9h5-9q4x/system-helper-Validate-each-remote-name-and-architecture-.patch                     |  139 +++
 debian/patches/GHSA-v2gw-v9h5-9q4x/tests-Add-some-unit-tests-for-ref-utils-name-component-va.patch                     |  394 ++++++++++
 debian/patches/GHSA-w69g-9x8j-7p8f/dir-utils-Reject-path-traversal-in-extension-metadata.patch                         |   92 ++
 debian/patches/GHSA-w69g-9x8j-7p8f/run-Use-RESOLVE_BENEATH-for-host-side-extension-file-acce.patch                     |  106 ++
 debian/patches/bwrap-Clarify-a-comment.patch                                                                           |   29 
 debian/patches/dir-Use-flatpak_bwrap_child_setup_inherit_fds_cb-to-apply.patch                                         |   64 +
 debian/patches/hardening/dir-Harden-confining-subpath-deploy-to-the-deploy-base.patch                                  |   62 +
 debian/patches/hardening/dir-Use-fd-based-traversal-for-marker-files-in-deploy-dir.patch                               |  100 ++
 debian/patches/libglnx/chase-Add-glnx_chase_and_mkdirat.patch                                                          |  232 +++++
 debian/patches/libglnx/chase-Add-internal-glnx_chaseat_full-for-a-strategic-call.patch                                 |  201 +++++
 debian/patches/portal-Actually-use-the-AppInfo-hash-table.patch                                                        |   41 +
 debian/patches/series                                                                                                  |   38 
 debian/patches/subprojects-Ignore-.wraplock-file-generated-by-recent-Mes.patch                                         |   19 
 debian/patches/subprojects-Update-dbus-proxy.wrap-to-v0.1.7.patch                                                      |   25 
 debian/patches/tests/tests-Fix-checks-where-we-expect-a-command-to-fail.patch                                          |  133 +++
 portal/flatpak-portal-app-info.c                                                                                       |    5 
 revokefs/writer.c                                                                                                      |  214 +++--
 subprojects/.gitignore                                                                                                 |    1 
 subprojects/dbus-proxy.wrap                                                                                            |    4 
 subprojects/libglnx/glnx-chase.c                                                                                       |  197 ++++-
 subprojects/libglnx/glnx-chase.h                                                                                       |    6 
 subprojects/libglnx/tests/test-libglnx-chase.c                                                                         |   69 +
 system-helper/flatpak-system-helper.c                                                                                  |   61 +
 tests/libtest.sh                                                                                                       |    9 
 tests/meson.build                                                                                                      |    1 
 tests/test-ref-utils.c                                                                                                 |  360 +++++++++
 tests/test-run-custom.sh                                                                                               |   22 
 tests/test-run.sh                                                                                                      |  111 ++
 66 files changed, 6409 insertions(+), 618 deletions(-)

diff -Nru flatpak-1.16.6/app/flatpak-builtins-build-init.c flatpak-1.16.6/app/flatpak-builtins-build-init.c
--- flatpak-1.16.6/app/flatpak-builtins-build-init.c	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/app/flatpak-builtins-build-init.c	2026-08-11 14:32:06.000000000 +0100
@@ -72,7 +72,7 @@
 
 static gboolean
 ensure_extensions (FlatpakDeploy *src_deploy, const char *default_arch, const char *default_branch,
-                   char *src_extensions[], GFile *top_dir, GCancellable *cancellable, GError **error)
+                   char *src_extensions[], int top_dfd, GCancellable *cancellable, GError **error)
 {
   g_autoptr(GKeyFile) metakey = flatpak_deploy_get_metadata (src_deploy);
   GList *extensions = NULL, *l;
@@ -118,22 +118,47 @@
                     return flatpak_fail (error, _("Requested extension %s/%s/%s is only partially installed"), ext->installed_id, default_arch, default_branch);
                 }
 
-              if (top_dir)
+              if (top_dfd >= 0)
                 {
-                  g_autoptr(GFile) target = g_file_resolve_relative_path (top_dir, ext->directory);
-                  g_autoptr(GFile) target_parent = g_file_get_parent (target);
-                  g_autoptr(GFile) ext_deploy_files = g_file_new_for_path (ext->files_path);
+                  glnx_autofd int ext_src_dfd = -1;
+                  glnx_autofd int parent_dfd = -1;
+                  g_autofree char *dir_parent = g_path_get_dirname (ext->directory);
+                  const char *dir_basename = glnx_basename (ext->directory);
+
+                  if (strcmp (dir_parent, ".") != 0)
+                    {
+                      glnx_autofd int parent_opath_fd = -1;
+
+                      parent_opath_fd = glnx_chase_and_mkdirat (top_dfd, dir_parent,
+                                                                GLNX_CHASE_RESOLVE_BENEATH |
+                                                                GLNX_CHASE_RESOLVE_NO_SYMLINKS,
+                                                                0755, error);
+                      if (parent_opath_fd < 0)
+                        return FALSE;
+
+                      parent_dfd = glnx_fd_reopen (parent_opath_fd,
+                                                   O_RDONLY | O_DIRECTORY | O_CLOEXEC,
+                                                   error);
+                      if (parent_dfd < 0)
+                        return FALSE;
+                    }
+                  else
+                    {
+                      parent_dfd = dup (top_dfd);
+                      if (parent_dfd < 0)
+                        return glnx_throw_errno_prefix (error, "dup");
+                    }
 
-                  if (!flatpak_mkdir_p (target_parent, cancellable, error))
+                  /* An extension overrides whatever is there before, so we clean up first */
+                  if (!glnx_shutil_rm_rf_at (parent_dfd, dir_basename, cancellable, error))
                     return FALSE;
 
-                  /* An extension overrides whatever is there before, so we clean up first */
-                  if (!flatpak_rm_rf (target, cancellable, error))
+                  if (!glnx_opendirat (AT_FDCWD, ext->files_path, FALSE, &ext_src_dfd, error))
                     return FALSE;
 
-                  if (!flatpak_cp_a (ext_deploy_files, target,
-                                     FLATPAK_CP_FLAGS_NO_CHOWN,
-                                     cancellable, error))
+                  if (!flatpak_cp_a_at (ext_src_dfd, parent_dfd, dir_basename,
+                                        FLATPAK_CP_FLAGS_NO_CHOWN,
+                                        cancellable, error))
                     return FALSE;
                 }
 
@@ -170,15 +195,8 @@
   g_autoptr(GOptionContext) context = NULL;
   g_autoptr(GFile) var_deploy_files = NULL;
   g_autoptr(GFile) base = NULL;
-  g_autoptr(GFile) files_dir = NULL;
-  g_autoptr(GFile) usr_dir = NULL;
-  g_autoptr(GFile) var_dir = NULL;
-  g_autoptr(GFile) var_tmp_dir = NULL;
-  g_autoptr(GFile) var_run_dir = NULL;
-  g_autoptr(GFile) metadata_file = NULL;
-  g_autoptr(GFile) gitignore_file = NULL;
+  glnx_autofd int base_dfd = -1;
   g_autoptr(GString) metadata_contents = NULL;
-  g_autoptr(GError) my_error = NULL;
   g_autoptr(FlatpakDeploy) runtime_deploy = NULL;
   g_autoptr(FlatpakDeploy) sdk_deploy = NULL;
   const char *app_id;
@@ -237,8 +255,12 @@
   else
     is_app = TRUE;
 
-  if (!flatpak_is_valid_name (app_id, -1, &my_error))
-    return flatpak_fail (error, _("'%s' is not a valid application name: %s"), app_id, my_error->message);
+  {
+    g_autoptr(GError) my_error = NULL;
+
+    if (!flatpak_is_valid_name (app_id, -1, &my_error))
+      return flatpak_fail (error, _("'%s' is not a valid application name: %s"), app_id, my_error->message);
+  }
 
 
   kinds = FLATPAK_KINDS_RUNTIME;
@@ -283,16 +305,18 @@
   if (!flatpak_mkdir_p (base, cancellable, error))
     return FALSE;
 
-  files_dir = g_file_get_child (base, "files");
-  var_dir = g_file_get_child (base, "var");
-  var_tmp_dir = g_file_get_child (var_dir, "tmp");
-  var_run_dir = g_file_get_child (var_dir, "run");
-  metadata_file = g_file_get_child (base, "metadata");
-  gitignore_file = g_file_get_child (base, ".gitignore");
-
-  if (!opt_update &&
-      g_file_query_exists (files_dir, cancellable))
-    return flatpak_fail (error, _("Build directory %s already initialized"), directory);
+  if (!glnx_opendirat (AT_FDCWD, flatpak_file_get_path_cached (base), FALSE, &base_dfd, error))
+    return FALSE;
+
+  {
+    struct stat stbuf;
+
+    if (!glnx_fstatat_allow_noent (base_dfd, "files", &stbuf, AT_SYMLINK_NOFOLLOW, error))
+      return FALSE;
+
+    if (!opt_update && errno == 0)
+      return flatpak_fail (error, _("Build directory %s already initialized"), directory);
+  }
 
   sdk_deploy = flatpak_dir_load_deployed (sdk_dir, sdk_ref, NULL, cancellable, error);
   if (sdk_deploy == NULL)
@@ -300,36 +324,41 @@
 
   if (opt_writable_sdk || is_runtime)
     {
+      const char *usr_dir_name = opt_sdk_dir ? opt_sdk_dir : "usr";
       g_autoptr(GFile) sdk_deploy_files = NULL;
+      glnx_autofd int sdk_dfd = -1;
 
-      if (opt_sdk_dir)
-        usr_dir = g_file_get_child (base, opt_sdk_dir);
-      else
-        usr_dir = g_file_get_child (base, "usr");
-
-      if (!flatpak_rm_rf (usr_dir, NULL, &my_error))
-        {
-          if (!g_error_matches (my_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
-            {
-              g_propagate_error (error, g_steal_pointer (&my_error));
-              return FALSE;
-            }
-
-          g_clear_error (&my_error);
-        }
+      if (!glnx_shutil_rm_rf_at (base_dfd, usr_dir_name, cancellable, error))
+        return FALSE;
 
       sdk_deploy_files = flatpak_deploy_get_files (sdk_deploy);
-      if (!flatpak_cp_a (sdk_deploy_files, usr_dir, FLATPAK_CP_FLAGS_NO_CHOWN, cancellable, error))
+      if (!glnx_opendirat (AT_FDCWD, flatpak_file_get_path_cached (sdk_deploy_files),
+                           FALSE, &sdk_dfd, error))
+        return FALSE;
+      if (!flatpak_cp_a_at (sdk_dfd, base_dfd, usr_dir_name,
+                            FLATPAK_CP_FLAGS_NO_CHOWN, cancellable, error))
         return FALSE;
     }
 
   sdk_branch = flatpak_decomposed_dup_branch (sdk_ref);
   sdk_arch = flatpak_decomposed_dup_arch (sdk_ref);
 
-  if (opt_sdk_extensions &&
-      !ensure_extensions (sdk_deploy, sdk_arch, sdk_branch,
-                          opt_sdk_extensions, usr_dir, cancellable, error))
-    return FALSE;
+  if (opt_sdk_extensions)
+    {
+      glnx_autofd int usr_dfd = -1;
+
+      if (opt_writable_sdk || is_runtime)
+        {
+          const char *usr_dir_name = opt_sdk_dir ? opt_sdk_dir : "usr";
+
+          if (!glnx_opendirat (base_dfd, usr_dir_name, FALSE, &usr_dfd, error))
+            return FALSE;
+        }
+
+      if (!ensure_extensions (sdk_deploy, sdk_arch, sdk_branch,
+                              opt_sdk_extensions, usr_dfd, cancellable, error))
+        return FALSE;
+    }
 
   if (opt_var)
     {
@@ -345,14 +374,15 @@
   if (opt_update)
     return TRUE;
 
-  if (!g_file_make_directory (files_dir, cancellable, error))
-    return FALSE;
+  if (TEMP_FAILURE_RETRY (mkdirat (base_dfd, "files", 0777)) != 0)
+    return glnx_throw_errno_prefix (error, "mkdirat(files)");
 
   if (opt_base)
     {
       const char *base_branch;
       g_autoptr(GFile) base_deploy_files = NULL;
       g_autoptr(FlatpakDeploy) base_deploy = NULL;
+      glnx_autofd int base_src_dfd = -1;
 
       base_branch = opt_base_version ? opt_base_version : "master";
       base_ref = flatpak_build_app_ref (opt_base, base_branch, opt_arch);
@@ -361,35 +391,55 @@
         return FALSE;
 
       base_deploy_files = flatpak_deploy_get_files (base_deploy);
-      if (!flatpak_cp_a (base_deploy_files, files_dir,
-                         FLATPAK_CP_FLAGS_MERGE | FLATPAK_CP_FLAGS_NO_CHOWN,
-                         cancellable, error))
+      if (!glnx_opendirat (AT_FDCWD, flatpak_file_get_path_cached (base_deploy_files),
+                           FALSE, &base_src_dfd, error))
         return FALSE;
-
-
-      if (opt_base_extensions &&
-          !ensure_extensions (base_deploy, opt_arch, base_branch,
-                              opt_base_extensions, files_dir, cancellable, error))
+      if (!flatpak_cp_a_at (base_src_dfd, base_dfd, "files",
+                            FLATPAK_CP_FLAGS_MERGE | FLATPAK_CP_FLAGS_NO_CHOWN,
+                            cancellable, error))
         return FALSE;
-    }
 
-  if (var_deploy_files)
-    {
-      if (!flatpak_cp_a (var_deploy_files, var_dir, FLATPAK_CP_FLAGS_NONE, cancellable, error))
-        return FALSE;
-    }
-  else
-    {
-      if (!g_file_make_directory (var_dir, cancellable, error))
-        return FALSE;
+      if (opt_base_extensions)
+        {
+          glnx_autofd int files_dfd = -1;
+          if (!glnx_opendirat (base_dfd, "files", FALSE, &files_dfd, error))
+            return FALSE;
+          if (!ensure_extensions (base_deploy, opt_arch, base_branch,
+                                  opt_base_extensions, files_dfd, cancellable, error))
+            return FALSE;
+        }
     }
 
-  if (!flatpak_mkdir_p (var_tmp_dir, cancellable, error))
-    return FALSE;
+  {
+    glnx_autofd int var_dfd = -1;
 
-  if (!g_file_query_exists (var_run_dir, cancellable) &&
-      !g_file_make_symbolic_link (var_run_dir, "/run", cancellable, error))
-    return FALSE;
+    if (var_deploy_files)
+      {
+        glnx_autofd int var_src_dfd = -1;
+
+        if (!glnx_opendirat (AT_FDCWD, flatpak_file_get_path_cached (var_deploy_files),
+                             FALSE, &var_src_dfd, error))
+          return FALSE;
+
+        if (!flatpak_cp_a_at (var_src_dfd, base_dfd, "var",
+                              FLATPAK_CP_FLAGS_NONE, cancellable, error))
+          return FALSE;
+      }
+    else
+      {
+        if (TEMP_FAILURE_RETRY (mkdirat (base_dfd, "var", 0777)) != 0)
+          return glnx_throw_errno_prefix (error, "mkdirat(var)");
+      }
+
+    if (!glnx_opendirat (base_dfd, "var", FALSE, &var_dfd, error))
+      return FALSE;
+
+    if (!glnx_shutil_mkdir_p_at (var_dfd, "tmp", 0777, cancellable, error))
+      return FALSE;
+
+    if (TEMP_FAILURE_RETRY (symlinkat ("/run", var_dfd, "run")) != 0 && errno != EEXIST)
+      return glnx_throw_errno_prefix (error, "symlinkat(var/run)");
+  }
 
 
   metadata_contents = g_string_new ("");
@@ -468,17 +518,17 @@
 
   keyfile_data = g_key_file_to_data (keyfile, &keyfile_data_len, NULL);
 
-  if (!g_file_replace_contents (metadata_file,
-                                keyfile_data, keyfile_data_len, NULL, FALSE,
-                                G_FILE_CREATE_REPLACE_DESTINATION,
-                                NULL, cancellable, error))
+  if (!glnx_file_replace_contents_at (base_dfd, "metadata",
+                                      (const guint8 *) keyfile_data, keyfile_data_len,
+                                      GLNX_FILE_REPLACE_NODATASYNC,
+                                      cancellable, error))
     return FALSE;
 
-  if (!g_file_replace_contents (gitignore_file,
-                                GIT_IGNORE_FILE, sizeof (GIT_IGNORE_FILE),
-                                NULL, FALSE,
-                                G_FILE_CREATE_REPLACE_DESTINATION,
-                                NULL, cancellable, error))
+  if (!glnx_file_replace_contents_at (base_dfd, ".gitignore",
+                                      (const guint8 *) GIT_IGNORE_FILE,
+                                      sizeof (GIT_IGNORE_FILE),
+                                      GLNX_FILE_REPLACE_NODATASYNC,
+                                      cancellable, error))
     return FALSE;
 
   return TRUE;
diff -Nru flatpak-1.16.6/common/flatpak-bwrap.c flatpak-1.16.6/common/flatpak-bwrap.c
--- flatpak-1.16.6/common/flatpak-bwrap.c	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/common/flatpak-bwrap.c	2026-08-11 14:32:06.000000000 +0100
@@ -533,7 +533,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.16.6/common/flatpak-dir.c flatpak-1.16.6/common/flatpak-dir.c
--- flatpak-1.16.6/common/flatpak-dir.c	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/common/flatpak-dir.c	2026-08-11 14:32:06.000000000 +0100
@@ -5810,8 +5810,14 @@
 
       extra_data_sha256 = ostree_checksum_from_bytes (sha256_bytes);
 
-      if (*extra_data_name == 0)
-        return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Empty name for extra data uri %s"), extra_data_uri);
+      if (extra_data_name == NULL || *extra_data_name == '\0' ||
+          strcmp (extra_data_name, ".") == 0 ||
+          strcmp (extra_data_name, "..") == 0 ||
+          strchr (extra_data_name, '/') != NULL)
+        {
+          return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA,
+                                     _("Invalid extra data name '%s'"), extra_data_name);
+        }
 
       /* Don't allow file uris here as that could read local files based on remote data */
       if (!g_str_has_prefix (extra_data_uri, "http:") &&
@@ -6384,6 +6390,18 @@
                                  &current_checksum, NULL, error))
     return FALSE;
 
+  /* The remote tracking ref may be absent (e.g. the remote was removed and
+   * re-added, the app was installed from a bundle, or the ref was deleted by
+   * ostree-prune/flatpak-repair).  Fall back to the deploy/ ref that flatpak
+   * always writes on every successful deploy so that our downgrade check can
+   * work. */
+  if (current_checksum == NULL)
+    {
+      g_autofree char *deploy_ref = g_strconcat ("deploy/", ref, NULL);
+      flatpak_repo_resolve_rev (self->repo, NULL, NULL, deploy_ref, TRUE,
+                                &current_checksum, NULL, NULL);
+    }
+
   if (current_checksum != NULL &&
       !ostree_repo_load_commit (self->repo, current_checksum, &old_commit, NULL, error))
     return FALSE;
@@ -8190,7 +8208,7 @@
 static gboolean
 extract_extra_data (FlatpakDir   *self,
                     const char   *checksum,
-                    GFile        *extradir,
+                    int           app_files_dfd,
                     gboolean     *created_extra_data,
                     GCancellable *cancellable,
                     GError      **error)
@@ -8199,6 +8217,7 @@
   g_autoptr(GVariant) extra_data = NULL;
   g_autoptr(GVariant) extra_data_sources = NULL;
   g_autoptr(GError) local_error = NULL;
+  glnx_autofd int extra_dfd = -1;
   gsize i, n_extra_data = 0;
   gsize n_extra_data_sources;
 
@@ -8221,7 +8240,7 @@
   if (n_extra_data_sources == 0)
     return TRUE;
 
-  g_info ("extracting extra data to %s", flatpak_file_get_path_cached (extradir));
+  g_info ("extracting extra data");
 
   if (!ostree_repo_read_commit_detached_metadata (self->repo, checksum, &detached_metadata,
                                                   cancellable, error))
@@ -8242,7 +8261,10 @@
   if (n_extra_data < n_extra_data_sources)
     return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Extra data missing in detached metadata"));
 
-  if (!flatpak_mkdir_p (extradir, cancellable, error))
+  extra_dfd = glnx_chase_and_mkdirat (app_files_dfd, "extra",
+                                      GLNX_CHASE_RESOLVE_BENEATH,
+                                      0777, error);
+  if (extra_dfd < 0)
     {
       g_prefix_error (error, _("While creating extradir: "));
       return FALSE;
@@ -8277,7 +8299,6 @@
       for (j = 0; j < n_extra_data; j++)
         {
           g_autoptr(GVariant) content = NULL;
-          g_autoptr(GFile) dest = NULL;
           g_autofree char *sha256 = NULL;
           const char *extra_data_name = NULL;
           const guchar *data;
@@ -8290,6 +8311,13 @@
           if (strcmp (extra_data_source_name, extra_data_name) != 0)
             continue;
 
+          if (extra_data_name[0] == '\0' ||
+              strcmp (extra_data_name, ".") == 0 ||
+              strcmp (extra_data_name, "..") == 0 ||
+              strchr (extra_data_name, '/') != NULL)
+            return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA,
+                                       _("Invalid extra data filename '%s'"), extra_data_name);
+
           data = g_variant_get_data (content);
           len = g_variant_get_size (content);
 
@@ -8300,12 +8328,10 @@
           if (strcmp (sha256, extra_data_sha256) != 0)
             return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Invalid checksum for extra data"));
 
-          dest = g_file_get_child (extradir, extra_data_name);
-          if (!g_file_replace_contents (dest,
-                                        g_variant_get_data (content),
-                                        g_variant_get_size (content),
-                                        NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION,
-                                        NULL, cancellable, error))
+          if (!glnx_file_replace_contents_at (extra_dfd, extra_data_name,
+                                              data, len,
+                                              GLNX_FILE_REPLACE_NODATASYNC,
+                                              cancellable, error))
             {
               g_prefix_error (error, _("While writing extra data file '%s': "), extra_data_name);
               return FALSE;
@@ -8324,37 +8350,12 @@
   return TRUE;
 }
 
-static void
-child_setup (gpointer user_data)
-{
-  GArray *fd_array = user_data;
-  int i;
-
-  /* If no fd_array was specified, don't care. */
-  if (fd_array == NULL)
-    return;
-
-  /* Otherwise, mark not - close-on-exec all the fds in the array */
-  for (i = 0; i < fd_array->len; i++)
-    {
-      int fd = g_array_index (fd_array, int, i);
-
-      /* 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");
-
-      fcntl (fd, F_SETFD, 0);
-    }
-}
-
 static gboolean
 apply_extra_data (FlatpakDir   *self,
-                  GFile        *checkoutdir,
+                  int           checkoutdir_dfd,
                   GCancellable *cancellable,
                   GError      **error)
 {
-  g_autoptr(GFile) metadata = NULL;
   g_autofree char *metadata_contents = NULL;
   gsize metadata_size;
   g_autoptr(GKeyFile) metakey = NULL;
@@ -8363,27 +8364,60 @@
   g_autoptr(FlatpakDecomposed) runtime_ref = NULL;
   g_autoptr(FlatpakDeploy) runtime_deploy = NULL;
   g_autoptr(FlatpakBwrap) bwrap = NULL;
-  g_autoptr(GFile) app_files = NULL;
-  g_autoptr(GFile) apply_extra_file = NULL;
-  g_autoptr(GFile) app_export_file = NULL;
-  g_autoptr(GFile) extra_export_file = NULL;
-  g_autoptr(GFile) extra_files = NULL;
   g_autoptr(GFile) runtime_files = NULL;
   g_autoptr(FlatpakContext) app_context = NULL;
   g_auto(GStrv) minimal_envp = NULL;
   g_autofree char *runtime_arch = NULL;
+  glnx_autofd int app_files_dfd = -1;
+  glnx_autofd int metadata_path_fd = -1;
+  glnx_autofd int metadata_read_fd = -1;
+  glnx_autofd int extra_dfd = -1;
+  glnx_autofd int usr_fd = -1;
   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))
-    return TRUE;
+  app_files_dfd = glnx_chaseat (checkoutdir_dfd, "files",
+                                GLNX_CHASE_RESOLVE_NO_SYMLINKS |
+                                GLNX_CHASE_MUST_BE_DIRECTORY,
+                                error);
+  if (app_files_dfd < 0)
+    return FALSE;
 
-  metadata = g_file_get_child (checkoutdir, "metadata");
+  {
+    glnx_autofd int apply_extra_fd = -1;
 
-  if (!g_file_load_contents (metadata, cancellable, &metadata_contents, &metadata_size, NULL, error))
+    apply_extra_fd = glnx_chaseat (app_files_dfd, "bin/apply_extra",
+                                   GLNX_CHASE_RESOLVE_BENEATH |
+                                   GLNX_CHASE_MUST_BE_REGULAR,
+                                   &local_error);
+    if (apply_extra_fd < 0)
+      {
+        if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+          {
+            g_clear_error (&local_error);
+            return TRUE;
+          }
+        g_propagate_error (error, g_steal_pointer (&local_error));
+        return FALSE;
+      }
+  }
+
+  metadata_path_fd = glnx_chaseat (checkoutdir_dfd, "metadata",
+                                   GLNX_CHASE_RESOLVE_NO_SYMLINKS |
+                                   GLNX_CHASE_MUST_BE_REGULAR,
+                                   error);
+  if (metadata_path_fd < 0)
+    return FALSE;
+
+  metadata_read_fd = glnx_fd_reopen (metadata_path_fd, O_RDONLY, error);
+  if (metadata_read_fd < 0)
+    return FALSE;
+
+  metadata_contents = glnx_fd_readall_utf8 (metadata_read_fd, &metadata_size,
+                                            cancellable, error);
+  if (metadata_contents == NULL)
     return FALSE;
 
   metakey = g_key_file_new ();
@@ -8436,24 +8470,36 @@
       runtime_files = flatpak_deploy_get_files (runtime_deploy);
     }
 
-  app_files = g_file_get_child (checkoutdir, "files");
-  app_export_file = g_file_get_child (checkoutdir, "export");
-  extra_files = g_file_get_child (app_files, "extra");
-  extra_export_file = g_file_get_child (extra_files, "export");
+  extra_dfd = glnx_chaseat (app_files_dfd, "extra",
+                            GLNX_CHASE_RESOLVE_BENEATH |
+                            GLNX_CHASE_MUST_BE_DIRECTORY,
+                            error);
+  if (extra_dfd < 0)
+    return FALSE;
 
   minimal_envp = flatpak_run_get_minimal_env (FALSE, FALSE);
   bwrap = flatpak_bwrap_new (minimal_envp);
   flatpak_bwrap_add_args (bwrap, flatpak_get_bwrap (), NULL);
 
   if (runtime_files)
-    flatpak_bwrap_add_args (bwrap,
-                            "--ro-bind", flatpak_file_get_path_cached (runtime_files), "/usr",
-                            "--lock-file", "/usr/.ref",
-                            NULL);
+    {
+      usr_fd = glnx_chaseat (AT_FDCWD, flatpak_file_get_path_cached (runtime_files),
+                             GLNX_CHASE_MUST_BE_DIRECTORY,
+                             error);
+      if (usr_fd < 0)
+        return FALSE;
+
+      if (!flatpak_bwrap_add_args_data_fd_dup (bwrap, "--ro-bind-fd", usr_fd, "/usr", error))
+        return FALSE;
+      flatpak_bwrap_add_args (bwrap, "--lock-file", "/usr/.ref", NULL);
+    }
+
+  if (!flatpak_bwrap_add_args_data_fd_dup (bwrap, "--ro-bind-fd", app_files_dfd, "/app", error))
+    return FALSE;
+  if (!flatpak_bwrap_add_args_data_fd_dup (bwrap, "--bind-fd", extra_dfd, "/app/extra", error))
+    return FALSE;
 
   flatpak_bwrap_add_args (bwrap,
-                          "--ro-bind", flatpak_file_get_path_cached (app_files), "/app",
-                          "--bind", flatpak_file_get_path_cached (extra_files), "/app/extra",
                           "--chdir", "/app/extra",
                           /* We run as root in the system-helper case, so drop all caps */
                           "--cap-drop", "ALL",
@@ -8475,16 +8521,6 @@
    * 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;
@@ -8511,23 +8547,31 @@
    * this runs as root in the system helper case. We canonicalize the permissions at the
    * end, but to avoid non-canonical permissions leaking out before then we make the
    * toplevel dir only accessible to the user */
-  if (chmod (flatpak_file_get_path_cached (extra_files), 0700) != 0)
-    {
-      glnx_set_error_from_errno (error);
+  {
+    glnx_autofd int extra_real_dfd =
+      glnx_fd_reopen (extra_dfd, O_RDONLY | O_DIRECTORY, error);
+
+    if (extra_real_dfd < 0)
       return FALSE;
-    }
+
+    if (fchmod (extra_real_dfd, 0700) != 0)
+      {
+        glnx_set_error_from_errno (error);
+        return FALSE;
+      }
+  }
 
   if (!g_spawn_sync (NULL,
                      (char **) bwrap->argv->pdata,
                      bwrap->envp,
                      G_SPAWN_SEARCH_PATH,
-                     child_setup, bwrap->fds,
+                     flatpak_bwrap_child_setup_inherit_fds_cb, bwrap->fds,
                      NULL, NULL,
                      &exit_status,
                      error))
     return FALSE;
 
-  if (!flatpak_canonicalize_permissions (AT_FDCWD, flatpak_file_get_path_cached (extra_files),
+  if (!flatpak_canonicalize_permissions (app_files_dfd, "extra",
                                          getuid () == 0 ? 0 : -1,
                                          getuid () == 0 ? 0 : -1,
                                          error))
@@ -8540,16 +8584,20 @@
       return FALSE;
     }
 
-  if (g_file_query_exists (extra_export_file, cancellable))
-    {
-      if (!flatpak_mkdir_p (app_export_file, cancellable, error))
-        return FALSE;
-      if (!flatpak_cp_a (extra_export_file,
-                         app_export_file,
-                         FLATPAK_CP_FLAGS_MERGE,
-                         cancellable, error))
-        return FALSE;
-    }
+  {
+    glnx_autofd int extra_export_dfd = -1;
+
+    extra_export_dfd = glnx_chaseat (extra_dfd, "export",
+                                     GLNX_CHASE_RESOLVE_BENEATH |
+                                     GLNX_CHASE_MUST_BE_DIRECTORY,
+                                     NULL);
+    if (extra_export_dfd >= 0)
+      {
+        if (!flatpak_cp_a_at (extra_export_dfd, checkoutdir_dfd, "export",
+                              FLATPAK_CP_FLAGS_MERGE, cancellable, error))
+          return FALSE;
+      }
+  }
 
   return TRUE;
 }
@@ -8750,7 +8798,6 @@
   g_autofree char *checkoutdirpath = NULL;
   const char *checkoutdir_basename;
   g_autoptr(GFile) real_checkoutdir = NULL;
-  g_autoptr(GFile) dotref = NULL;
   g_autoptr(GFile) files_etc = NULL;
   g_autoptr(GFile) deploy_data_file = NULL;
   g_autoptr(GVariant) commit_data = NULL;
@@ -8762,6 +8809,7 @@
   OstreeRepoCheckoutAtOptions options = { 0, };
   const char *checksum;
   glnx_autofd int checkoutdir_dfd = -1;
+  glnx_autofd int app_files_dfd = -1;
   const char *xa_ref = NULL;
   g_autofree char *checkout_basename = NULL;
   gboolean created_extra_data = FALSE;
@@ -8888,28 +8936,27 @@
       for (i = 0; subpaths[i] != NULL; i++)
         {
           g_autofree char *subpath = g_build_filename ("files", subpaths[i], NULL);
-          g_autofree char *dstpath = g_build_filename (checkoutdirpath, "/files", subpaths[i], NULL);
-          g_autofree char *dstpath_parent = g_path_get_dirname (dstpath);
-          g_autofree char *dstpath_relative_to_deploy_base = g_build_filename (checkoutdir_basename, "/files", subpaths[i], NULL);
+          g_autofree char *dstpath = g_build_filename (checkoutdir_basename, subpath, NULL);
+          g_autofree char *dstdir = g_path_get_dirname (dstpath);
+          g_autofree char *dstbase = g_path_get_basename (dstpath);
+          glnx_autofd int parent_dfd = -1;
           g_autoptr(GFile) child = NULL;
 
           child = g_file_resolve_relative_path (root, subpath);
-
           if (!g_file_query_exists (child, cancellable))
             {
               g_info ("subpath %s not in tree", subpaths[i]);
               continue;
             }
 
-          if (g_mkdir_with_parents (dstpath_parent, 0755))
-            {
-              glnx_set_error_from_errno (error);
-              return FALSE;
-            }
+          parent_dfd = glnx_chase_and_mkdirat (deploy_base_dfd, dstdir,
+                                               GLNX_CHASE_RESOLVE_BENEATH, 0755, error);
+          if (parent_dfd < 0)
+            return FALSE;
 
           options.subpath = subpath;
           if (!ostree_repo_checkout_at (self->repo, &options,
-                                        deploy_base_dfd, dstpath_relative_to_deploy_base,
+                                        parent_dfd, dstbase,
                                         checksum,
                                         cancellable, error))
             {
@@ -8919,20 +8966,29 @@
         }
     }
 
+  if (!glnx_opendirat (deploy_base_dfd, checkoutdir_basename, FALSE, &checkoutdir_dfd, error))
+    return FALSE;
+
   /* Extract any extra data */
-  extradir = g_file_resolve_relative_path (checkoutdir, "files/extra");
-  if (!flatpak_rm_rf (extradir, cancellable, error))
+  app_files_dfd = glnx_chaseat (checkoutdir_dfd, "files",
+                                GLNX_CHASE_RESOLVE_NO_SYMLINKS |
+                                GLNX_CHASE_MUST_BE_DIRECTORY,
+                                error);
+  if (app_files_dfd < 0)
+    return FALSE;
+
+  if (!glnx_shutil_rm_rf_at (app_files_dfd, "extra", cancellable, error))
     {
       g_prefix_error (error, _("While trying to remove existing extra dir: "));
       return FALSE;
     }
 
-  if (!extract_extra_data (self, checksum, extradir, &created_extra_data, cancellable, error))
+  if (!extract_extra_data (self, checksum, app_files_dfd, &created_extra_data, cancellable, error))
     return FALSE;
 
   if (created_extra_data)
     {
-      if (!apply_extra_data (self, checkoutdir, cancellable, error))
+      if (!apply_extra_data (self, checkoutdir_dfd, cancellable, error))
         {
           g_prefix_error (error, _("While trying to apply extra data: "));
           return FALSE;
@@ -9010,10 +9066,19 @@
                                  metadata_contents, metadata_size, error))
     return FALSE;
 
-  dotref = g_file_resolve_relative_path (checkoutdir, "files/.ref");
-  if (!g_file_replace_contents (dotref, "", 0, NULL, FALSE,
-                                G_FILE_CREATE_REPLACE_DESTINATION, NULL, cancellable, error))
-    return FALSE;
+  {
+    g_autofree char *files_path = g_build_filename (checkoutdir_basename, "files", NULL);
+    glnx_autofd int files_dfd = -1;
+
+    if (!glnx_opendirat (deploy_base_dfd, files_path, FALSE, &files_dfd, error))
+      return FALSE;
+
+    if (!glnx_file_replace_contents_at (files_dfd, ".ref",
+                                        (const guint8 *) "", 0,
+                                        GLNX_FILE_REPLACE_NODATASYNC,
+                                        cancellable, error))
+      return FALSE;
+  }
 
   export = g_file_get_child (checkoutdir, "export");
 
@@ -9127,9 +9192,6 @@
   if (!flatpak_bytes_save (deploy_data_file, deploy_data, cancellable, error))
     return FALSE;
 
-  if (!glnx_opendirat (deploy_base_dfd, checkoutdir_basename, TRUE, &checkoutdir_dfd, error))
-    return FALSE;
-
   if (syncfs (checkoutdir_dfd) != 0)
     {
       glnx_set_error_from_errno (error);
@@ -11235,7 +11297,6 @@
   g_autofree char *id = NULL;
   g_autofree char *dirname = NULL;
   g_autofree char *current_active = NULL;
-  g_autoptr(GFile) change_file = NULL;
   g_autoptr(GError) child_error = NULL;
   int i, retry;
 
@@ -11327,18 +11388,23 @@
     }
 
 
-  if (is_update)
-    change_file = g_file_resolve_relative_path (removed_subdir, "files/.updated");
-  else
-    change_file = g_file_resolve_relative_path (removed_subdir, "files/.removed");
-
-  if (!g_file_replace_contents (change_file, "", 0, NULL, FALSE,
-                                G_FILE_CREATE_REPLACE_DESTINATION, NULL, NULL, &child_error))
-    {
-      g_autofree gchar *path = g_file_get_path (change_file);
-      g_warning ("Unable to clear %s: %s", path, child_error->message);
-      g_clear_error (&child_error);
-    }
+  {
+    glnx_autofd int files_dfd = -1;
+    const char *marker = is_update ? ".updated" : ".removed";
+    g_autofree char *files_path = g_build_filename (flatpak_file_get_path_cached (removed_subdir), "files", NULL);
+
+    if (!glnx_opendirat (AT_FDCWD, files_path,
+                         FALSE, &files_dfd, &child_error) ||
+        !glnx_file_replace_contents_at (files_dfd, marker,
+                                        (const guint8 *) "", 0,
+                                        GLNX_FILE_REPLACE_NODATASYNC,
+                                        NULL, &child_error))
+      {
+        g_warning ("Unable to write %s marker: %s", marker,
+                   child_error ? child_error->message : g_strerror (errno));
+        g_clear_error (&child_error);
+      }
+  }
 
   if (force_remove || !dir_is_locked (removed_subdir))
     {
@@ -11421,25 +11487,61 @@
  * flatpak_dir_remove_ref:
  * @self: a #FlatpakDir
  * @remote_name: the name of the remote
- * @ref: the flatpak ref to remove
+ * @ref: the ref to remove
  * @cancellable: (nullable) (optional): a #GCancellable
  * @error: a #GError
  *
- * Remove the flatpak ref given by @remote_name:@ref from the underlying
- * OSTree repo. Attempting to remove a ref that is currently deployed
- * is an error, you need to uninstall the flatpak first. Note that this does
- * not remove the objects bound to @ref from the disk, you will need to
- * call flatpak_dir_prune() to do that.
+ * Remove the ref given by @remote_name:@ref from the underlying
+ * OSTree repo. Note that this does not remove the objects bound to
+ * @ref from the disk, you will need to call flatpak_dir_prune()
+ * to do that.
  *
  * Returns: %TRUE if removing the ref succeeded, %FALSE otherwise.
  */
 gboolean
 flatpak_dir_remove_ref (FlatpakDir        *self,
                         const char        *remote_name,
-                        const char        *ref, /* NOTE: Not necessarily a app/runtime ref */
+                        const char        *ref,
                         GCancellable      *cancellable,
                         GError           **error)
 {
+  if (!ostree_repo_set_ref_immediate (self->repo,
+                                      remote_name,
+                                      ref,
+                                      NULL,
+                                      cancellable,
+                                      error))
+    return FALSE;
+
+  return TRUE;
+}
+
+/**
+ * flatpak_dir_remove_undeployed_ref:
+ * @self: a #FlatpakDir
+ * @remote_name: the name of the remote
+ * @ref: the flatpak ref to remove
+ * @cancellable: (nullable) (optional): a #GCancellable
+ * @error: a #GError
+ *
+ * Remove the flatpak ref given by @remote_name:@ref from the underlying
+ * OSTree repo. The ref must be a valid app or runtime ref, and it must
+ * not be currently deployed. Note that this does not remove the objects
+ * bound to @ref from the disk, you will need to call
+ * flatpak_dir_prune() to do that.
+ *
+ * Returns: %TRUE if removing the ref succeeded, %FALSE otherwise.
+ */
+gboolean
+flatpak_dir_remove_undeployed_ref (FlatpakDir    *self,
+                                   const char    *remote_name,
+                                   const char    *ref,
+                                   GCancellable  *cancellable,
+                                   GError       **error)
+{
+  g_autoptr(FlatpakDecomposed) decomposed = NULL;
+  g_autoptr(GBytes) deploy_data = NULL;
+
   if (flatpak_dir_use_system_helper (self, NULL))
     {
       const char *installation = flatpak_dir_get_id (self);
@@ -11456,15 +11558,21 @@
       return TRUE;
     }
 
-  if (!ostree_repo_set_ref_immediate (self->repo,
-                                      remote_name,
-                                      ref,
-                                      NULL,
-                                      cancellable,
-                                      error))
+  decomposed = flatpak_decomposed_new_from_ref (ref, error);
+  if (decomposed == NULL)
     return FALSE;
 
-  return TRUE;
+  deploy_data = flatpak_dir_get_deploy_data (self, decomposed,
+                                             FLATPAK_DEPLOY_VERSION_ANY,
+                                             NULL, NULL);
+  if (deploy_data != NULL)
+    {
+      g_set_error (error, FLATPAK_ERROR, FLATPAK_ERROR_ALREADY_INSTALLED,
+                   _("%s is currently deployed, you need to uninstall it first"), ref);
+      return FALSE;
+    }
+
+  return flatpak_dir_remove_ref (self, remote_name, ref, cancellable, error);
 }
 
 gboolean
@@ -13942,7 +14050,7 @@
       FlatpakDecomposed *ref = g_ptr_array_index (undeployed_refs, i);
       g_autofree gchar *remote = flatpak_decomposed_dup_remote (ref);
 
-      if (!flatpak_dir_remove_ref (self, remote, flatpak_decomposed_get_ref (ref), cancellable, error))
+      if (!flatpak_dir_remove_undeployed_ref (self, remote, flatpak_decomposed_get_ref (ref), cancellable, error))
         return FALSE;
     }
 
diff -Nru flatpak-1.16.6/common/flatpak-dir-private.h flatpak-1.16.6/common/flatpak-dir-private.h
--- flatpak-1.16.6/common/flatpak-dir-private.h	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/common/flatpak-dir-private.h	2026-08-11 14:32:06.000000000 +0100
@@ -772,6 +772,11 @@
                                                                              const char                    *ref,
                                                                              GCancellable                  *cancellable,
                                                                              GError                       **error);
+gboolean              flatpak_dir_remove_undeployed_ref                     (FlatpakDir                    *self,
+                                                                             const char                    *remote_name,
+                                                                             const char                    *ref,
+                                                                             GCancellable                  *cancellable,
+                                                                             GError                       **error);
 gboolean              flatpak_dir_update_exports                            (FlatpakDir                    *self,
                                                                              const char                    *app,
                                                                              GCancellable                  *cancellable,
diff -Nru flatpak-1.16.6/common/flatpak-dir-utils.c flatpak-1.16.6/common/flatpak-dir-utils.c
--- flatpak-1.16.6/common/flatpak-dir-utils.c	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/common/flatpak-dir-utils.c	2026-08-11 14:32:06.000000000 +0100
@@ -24,6 +24,7 @@
 #include "flatpak-dir-private.h"
 #include "flatpak-metadata-private.h"
 #include "flatpak-utils-private.h"
+#include "flatpak-utils-base-private.h"
 
 char **
 flatpak_list_deployed_refs (const char   *type,
@@ -393,6 +394,21 @@
   return ext;
 }
 
+static gboolean
+relative_path_has_no_traversal (const char *path)
+{
+  if (path == NULL || *path == '\0' || *path == '/')
+    return FALSE;
+
+  if (strcmp (path, "..") == 0 ||
+      g_str_has_prefix (path, "../") ||
+      strstr (path, "/../") != NULL ||
+      g_str_has_suffix (path, "/.."))
+    return FALSE;
+
+  return TRUE;
+}
+
 static GList *
 add_extension (GKeyFile   *metakey,
                const char *group,
@@ -423,8 +439,39 @@
   g_autoptr(GFile) deploy_dir = NULL;
   g_autoptr(FlatpakDir) dir = NULL;
 
-  if (directory == NULL)
-    return res;
+  if (directory == NULL || !relative_path_has_no_traversal (directory))
+    {
+      g_warning ("Extension %s key %s is missing or has directory traversal, ignoring",
+                 extension, FLATPAK_METADATA_KEY_DIRECTORY);
+      return res;
+    }
+
+  if (subdir_suffix != NULL && !relative_path_has_no_traversal (subdir_suffix))
+    {
+      g_warning ("Extension %s key %s is missing or has directory traversal, ignoring",
+                 extension, FLATPAK_METADATA_KEY_SUBDIRECTORY_SUFFIX);
+      return res;
+    }
+
+  if (add_ld_path != NULL && !relative_path_has_no_traversal (add_ld_path))
+    {
+      g_warning ("Extension %s key %s is missing or has directory traversal, ignoring",
+                 extension, FLATPAK_METADATA_KEY_ADD_LD_PATH);
+      return res;
+    }
+
+  if (merge_dirs != NULL)
+    {
+      for (size_t i = 0; merge_dirs[i] != NULL; i++)
+        {
+          if (!relative_path_has_no_traversal (merge_dirs[i]))
+            {
+              g_warning ("Extension %s key %s is missing or has directory traversal, ignoring",
+                         extension, FLATPAK_METADATA_KEY_MERGE_DIRS);
+              return res;
+            }
+        }
+    }
 
   ref = flatpak_decomposed_new_from_parts (FLATPAK_KINDS_RUNTIME, extension, arch, branch, NULL);
   if (ref == NULL)
diff -Nru flatpak-1.16.6/common/flatpak-installation.c flatpak-1.16.6/common/flatpak-installation.c
--- flatpak-1.16.6/common/flatpak-installation.c	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/common/flatpak-installation.c	2026-08-11 14:32:06.000000000 +0100
@@ -2911,8 +2911,10 @@
  * @error: return location for a #GError
  *
  * Remove the OSTree ref given by @remote_name:@ref from the local flatpak
- * repository. The next time the underlying OSTree repo is pruned, objects
- * which were attached to that ref will be removed. This is useful if you
+ * repository. The ref must not be currently deployed; attempting to remove
+ * a deployed ref will return an error. Use flatpak_installation_uninstall_full()
+ * to remove deployed refs. The next time the underlying OSTree repo is pruned,
+ * objects which were attached to that ref will be removed. This is useful if you
  * pulled a flatpak ref using flatpak_installation_install_full() and
  * specified %FLATPAK_INSTALL_FLAGS_NO_DEPLOY but then decided not to
  * deploy the ref later on and want to remove the local ref to prevent it
@@ -2936,7 +2938,7 @@
   if (dir == NULL)
     return FALSE;
 
-  return flatpak_dir_remove_ref (dir, remote_name, ref, cancellable, error);
+  return flatpak_dir_remove_undeployed_ref (dir, remote_name, ref, cancellable, error);
 }
 
 /**
diff -Nru flatpak-1.16.6/common/flatpak-oci-registry.c flatpak-1.16.6/common/flatpak-oci-registry.c
--- flatpak-1.16.6/common/flatpak-oci-registry.c	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/common/flatpak-oci-registry.c	2026-08-11 14:32:06.000000000 +0100
@@ -1731,7 +1731,7 @@
 
 static gboolean
 delta_read_varuint (GInputStream   *in,
-                    guint64        *out,
+                    gsize          *out,
                     GCancellable   *cancellable,
                     GError        **error)
 {
@@ -1755,6 +1755,9 @@
     }
   while (more_data);
 
+  if (res > G_MAXSIZE)
+    return flatpak_fail (error, _("Invalid delta file format"));
+
   *out = res;
   return TRUE;
 }
@@ -1762,7 +1765,7 @@
 static gboolean
 delta_copy_data (GInputStream   *in,
                  GOutputStream  *out,
-                 guint64         size,
+                 gsize           size,
                  guchar         *buffer,
                  GCancellable   *cancellable,
                  GError        **error)
@@ -1790,7 +1793,7 @@
 delta_add_data (GInputStream   *in1,
                 GInputStream   *in2,
                 GOutputStream  *out,
-                guint64         size,
+                gsize           size,
                 guchar         *buffer1,
                 guchar         *buffer2,
                 GCancellable   *cancellable,
@@ -1823,11 +1826,16 @@
 
 static guchar *
 delta_read_data (GInputStream   *in,
-                 guint64         size,
+                 gsize           size,
                  GCancellable   *cancellable,
                  GError        **error)
 {
-  g_autofree guchar *buf = g_malloc (size+1);
+  g_autofree guchar *buf = NULL;
+
+  if (size > SIZE_MAX - 1)
+    return glnx_null_throw (error, _("Invalid delta file format"));
+
+  buf = g_malloc (size + 1);
 
   if (!g_input_stream_read_all (in, buf, size, NULL, cancellable, error))
     return NULL;
@@ -1924,9 +1932,7 @@
   while (TRUE)
     {
       guint8 op;
-      guint64 size;
-      g_autofree char *path = NULL;
-      g_autofree char *clean_path = NULL;
+      gsize size;
       g_autoptr(GError) local_error = NULL;
       gboolean eof;
 
@@ -1949,22 +1955,29 @@
           break;
 
         case DELTA_OP_OPEN:
-          path = (char *)delta_read_data (in, size, cancellable, error);
-          if (path == NULL)
-            return FALSE;
-          clean_path = delta_clean_path (path);
-
-          g_clear_object (&content_file);
-
           {
-            g_autoptr(GFile) child = g_file_resolve_relative_path (content_dir, clean_path);
+            g_autofree char *path = NULL;
+            g_autofree char *clean_path = NULL;
+            g_autoptr(GFile) child = NULL;
             g_autoptr(GFileInputStream) child_in = NULL;
 
+            if (size > PATH_MAX)
+              return flatpak_fail (error, _("Invalid delta file format"));
+
+            path = (char *) delta_read_data (in, size, cancellable, error);
+            if (path == NULL)
+              return FALSE;
+
+            clean_path = delta_clean_path (path);
+
+            child = g_file_resolve_relative_path (content_dir, clean_path);
+
             child_in = g_file_read (child, cancellable, error);
             if (child_in == NULL)
               return FALSE;
 
             /* We can't seek in the ostree repo file, so copy it to temp file */
+            g_clear_object (&content_file);
             content_file = copy_stream_to_file (self, G_INPUT_STREAM (child_in), cancellable, error);
             if (content_file == NULL)
               return FALSE;
diff -Nru flatpak-1.16.6/common/flatpak-ref-utils.c flatpak-1.16.6/common/flatpak-ref-utils.c
--- flatpak-1.16.6/common/flatpak-ref-utils.c	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/common/flatpak-ref-utils.c	2026-08-11 14:32:06.000000000 +0100
@@ -567,25 +567,42 @@
     c == '.';
 }
 
-static gboolean
-is_valid_remote_name (const char *remote,
-                      gsize len)
+gboolean
+flatpak_is_valid_remote_name (const char *string,
+                              gssize      len,
+                              GError    **error)
 {
   const char *end;
 
-  if (len == 0)
-    return FALSE;
+  if (len < 0)
+    len = strlen (string);
 
-  end = remote + len;
+  if (G_UNLIKELY (len == 0))
+    {
+      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME,
+                          _("Remote name can't be empty"));
+      return FALSE;
+    }
+
+  end = string + len;
 
-  if (!is_valid_initial_remote_name_character (*remote++))
-    return FALSE;
+  if (G_UNLIKELY (!is_valid_initial_remote_name_character (*string)))
+    {
+      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME,
+                          _("Remote name can't start with %c"), *string);
+      return FALSE;
+    }
+  string++;
 
-  while (remote < end)
+  while (string < end)
     {
-      char c = *remote++;
-      if (!is_valid_remote_name_character (c))
-        return FALSE;
+      if (G_UNLIKELY (!is_valid_remote_name_character (*string)))
+        {
+          flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME,
+                              _("Remote name can't contain %c"), *string);
+          return FALSE;
+        }
+      string++;
     }
 
   return TRUE;
@@ -622,7 +639,7 @@
       const char *colon = strchr (p, ':');
       if (colon != NULL)
         {
-          if (!is_valid_remote_name (ref, colon - ref))
+          if (!flatpak_is_valid_remote_name (ref, colon - ref, NULL))
             {
               flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Invalid remote name"));
               return NULL;
diff -Nru flatpak-1.16.6/common/flatpak-ref-utils-private.h flatpak-1.16.6/common/flatpak-ref-utils-private.h
--- flatpak-1.16.6/common/flatpak-ref-utils-private.h	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/common/flatpak-ref-utils-private.h	2026-08-11 14:32:06.000000000 +0100
@@ -33,6 +33,9 @@
 gboolean flatpak_is_valid_arch (const char *string,
                                 gssize      len,
                                 GError    **error);
+gboolean flatpak_is_valid_remote_name (const char *string,
+                                       gssize      len,
+                                       GError    **error);
 gboolean flatpak_has_name_prefix (const char *string,
                                   const char *name);
 gboolean flatpak_name_matches_one_wildcard_prefix (const char         *string,
diff -Nru flatpak-1.16.6/common/flatpak-run.c flatpak-1.16.6/common/flatpak-run.c
--- flatpak-1.16.6/common/flatpak-run.c	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/common/flatpak-run.c	2026-08-11 14:32:06.000000000 +0100
@@ -149,7 +149,8 @@
       g_autofree char *directory = g_build_filename (target_path, ext->directory, NULL);
       g_autofree char *full_directory = g_build_filename (directory, ext->subdir_suffix, NULL);
       g_autofree char *ref_file = g_build_filename (full_directory, ".ref", NULL);
-      g_autofree char *real_ref = g_build_filename (ext->files_path, ext->directory, ".ref", NULL);
+      g_autofree char *ref_subpath = g_build_filename (ext->directory, ".ref", NULL);
+      glnx_autofd int ext_files_dfd = -1;
 
       if (ext->needs_tmpfs)
         {
@@ -168,10 +169,16 @@
                               "--ro-bind", ext->files_path, full_directory,
                               NULL);
 
-      if (g_file_test (real_ref, G_FILE_TEST_EXISTS))
-        flatpak_bwrap_add_args (bwrap,
-                                "--lock-file", ref_file,
-                                NULL);
+      if (glnx_opendirat (AT_FDCWD, ext->files_path, FALSE, &ext_files_dfd, NULL))
+        {
+          glnx_autofd int ref_fd = glnx_chaseat (ext_files_dfd, ref_subpath,
+                                                 GLNX_CHASE_RESOLVE_BENEATH,
+                                                 NULL);
+          if (ref_fd >= 0)
+            flatpak_bwrap_add_args (bwrap,
+                                    "--lock-file", ref_file,
+                                    NULL);
+        }
     }
 
   g_list_free (path_sorted_extensions);
@@ -183,8 +190,16 @@
       FlatpakExtension *ext = l->data;
       g_autofree char *directory = g_build_filename (target_path, ext->directory, NULL);
       g_autofree char *full_directory = g_build_filename (directory, ext->subdir_suffix, NULL);
+      glnx_autofd int files_dfd = -1;
       int i;
 
+      if (!glnx_opendirat (AT_FDCWD, ext->files_path, FALSE, &files_dfd, NULL))
+        {
+          return flatpak_fail_error (error, FLATPAK_ERROR,
+                                     "Failed to open extension %s files",
+                                     ext->installed_id);
+        }
+
       if (used_extensions->len > 0)
         g_string_append (used_extensions, ";");
       g_string_append (used_extensions, ext->installed_id);
@@ -221,11 +236,30 @@
         {
           g_autofree char *parent = g_path_get_dirname (directory);
           g_autofree char *merge_dir = g_build_filename (parent, ext->merge_dirs[i], NULL);
-          g_autofree char *source_dir = g_build_filename (ext->files_path, ext->merge_dirs[i], NULL);
+          glnx_autofd int source_dfd = -1;
+          glnx_autofd int source_read_dfd = -1;
           g_auto(GLnxDirFdIterator) source_iter = { 0 };
           struct dirent *dent;
 
-          if (glnx_dirfd_iterator_init_at (AT_FDCWD, source_dir, TRUE, &source_iter, NULL))
+          source_dfd = glnx_chaseat (files_dfd, ext->merge_dirs[i],
+                                     GLNX_CHASE_RESOLVE_BENEATH |
+                                     GLNX_CHASE_MUST_BE_DIRECTORY,
+                                     NULL);
+          if (source_dfd < 0)
+            {
+              if (errno == ENOENT || errno == ENOTDIR)
+                continue;
+
+              return flatpak_fail_error (error, FLATPAK_ERROR,
+                                         "Extension %s has invalid merge-dirs",
+                                         ext->installed_id);
+            }
+
+          source_read_dfd = glnx_fd_reopen (source_dfd, O_RDONLY, error);
+          if (source_read_dfd < 0)
+            return FALSE;
+
+          if (glnx_dirfd_iterator_init_take_fd (&source_read_dfd, &source_iter, NULL))
             {
               while (glnx_dirfd_iterator_next_dent (&source_iter, &dent, NULL, NULL) && dent != NULL)
                 {
@@ -761,30 +795,35 @@
                          GCancellable *cancellable,
                          GError      **error)
 {
-  g_autoptr(GFile) data_dir = g_file_get_child (app_id_dir, "data");
-  g_autoptr(GFile) cache_dir = g_file_get_child (app_id_dir, "cache");
-  g_autoptr(GFile) fontconfig_cache_dir = g_file_get_child (cache_dir, "fontconfig");
-  g_autoptr(GFile) tmp_dir = g_file_get_child (cache_dir, "tmp");
-  g_autoptr(GFile) config_dir = g_file_get_child (app_id_dir, "config");
-  g_autoptr(GFile) state_dir = g_file_get_child (app_id_dir, ".local/state");
-
-  if (!flatpak_mkdir_p (data_dir, cancellable, error))
-    return FALSE;
-
-  if (!flatpak_mkdir_p (cache_dir, cancellable, error))
-    return FALSE;
-
-  if (!flatpak_mkdir_p (fontconfig_cache_dir, cancellable, error))
-    return FALSE;
+  glnx_autofd int app_id_dir_fd = -1;
+  static const char *const subdirs[] = {
+    "data",
+    "cache",
+    "cache/fontconfig",
+    "cache/tmp",
+    "config",
+    ".local/state",
+  };
 
-  if (!flatpak_mkdir_p (tmp_dir, cancellable, error))
+  app_id_dir_fd = glnx_chase_and_mkdirat (AT_FDCWD,
+                                          flatpak_file_get_path_cached (app_id_dir),
+                                          GLNX_CHASE_DEFAULT,
+                                          0755,
+                                          error);
+  if (app_id_dir_fd < 0)
     return FALSE;
 
-  if (!flatpak_mkdir_p (config_dir, cancellable, error))
-    return FALSE;
+  for (size_t i = 0; i < G_N_ELEMENTS (subdirs); i++)
+    {
+      glnx_autofd int fd = -1;
 
-  if (!flatpak_mkdir_p (state_dir, cancellable, error))
-    return FALSE;
+      fd = glnx_chase_and_mkdirat (app_id_dir_fd, subdirs[i],
+                                   GLNX_CHASE_RESOLVE_BENEATH,
+                                   0755,
+                                   error);
+      if (fd < 0)
+        return FALSE;
+    }
 
   return TRUE;
 }
@@ -2441,18 +2480,39 @@
 
   if (app_id_dir != NULL)
     {
-      g_autoptr(GFile) app_cache_dir = g_file_get_child (app_id_dir, "cache");
-      g_autoptr(GFile) app_tmp_dir = g_file_get_child (app_cache_dir, "tmp");
-      g_autoptr(GFile) app_data_dir = g_file_get_child (app_id_dir, "data");
-      g_autoptr(GFile) app_config_dir = g_file_get_child (app_id_dir, "config");
+      glnx_autofd int app_id_dir_fd = -1;
+      static const struct
+        {
+          const char *src;
+          const char *dst;
+        }
+      mounts[] = {
+        { "cache", "/var/cache" },
+        { "data", "/var/data" },
+        { "config", "/var/config" },
+        { "cache/tmp", "/var/tmp" },
+      };
+
+      app_id_dir_fd = glnx_chaseat (AT_FDCWD,
+                                    flatpak_file_get_path_cached (app_id_dir),
+                                    GLNX_CHASE_MUST_BE_DIRECTORY,
+                                    error);
+      if (app_id_dir_fd < 0)
+        return FALSE;
 
-      flatpak_bwrap_add_args (bwrap,
-                              /* These are nice to have as a fixed path */
-                              "--bind", flatpak_file_get_path_cached (app_cache_dir), "/var/cache",
-                              "--bind", flatpak_file_get_path_cached (app_data_dir), "/var/data",
-                              "--bind", flatpak_file_get_path_cached (app_config_dir), "/var/config",
-                              "--bind", flatpak_file_get_path_cached (app_tmp_dir), "/var/tmp",
-                              NULL);
+      for (i = 0; i < G_N_ELEMENTS (mounts); i++)
+        {
+          glnx_autofd int fd = -1;
+
+          fd = glnx_chase_and_mkdirat (app_id_dir_fd, mounts[i].src,
+                                       GLNX_CHASE_RESOLVE_BENEATH,
+                                       0755, error);
+          if (fd < 0)
+            return FALSE;
+
+          flatpak_bwrap_add_args_data_fd (bwrap, "--bind-fd",
+                                          g_steal_fd (&fd), mounts[i].dst);
+        }
     }
 
   flatpak_run_setup_usr_links (bwrap, runtime_fd, NULL);
@@ -2691,33 +2751,72 @@
 {
   g_autoptr(FlatpakBwrap) bwrap = NULL;
   g_autoptr(GArray) combined_fd_array = NULL;
-  g_autoptr(GFile) ld_so_cache = NULL;
-  g_autoptr(GFile) ld_so_cache_tmp = NULL;
   g_autofree char *sandbox_cache_path = NULL;
   g_autofree char *tmp_basename = NULL;
   g_auto(GStrv) minimal_envp = NULL;
   g_autofree char *commandline = NULL;
   int exit_status;
   glnx_autofd int ld_so_fd = -1;
-  g_autoptr(GFile) ld_so_dir = NULL;
+  glnx_autofd int ld_so_dir_fd = -1;
+  g_autofree char *ld_so_dir_path = NULL;
 
   if (app_id_dir)
-    ld_so_dir = g_file_get_child (app_id_dir, ".ld.so");
+    {
+      glnx_autofd int app_id_dir_fd = -1;
+
+      app_id_dir_fd = glnx_chase_and_mkdirat (AT_FDCWD,
+                                              flatpak_file_get_path_cached (app_id_dir),
+                                              GLNX_CHASE_DEFAULT,
+                                              0700, error);
+      if (app_id_dir_fd < 0)
+        {
+          g_prefix_error (error, "cannot open %s: ",
+                          flatpak_file_get_path_cached (app_id_dir));
+          return -1;
+        }
+
+      ld_so_dir_fd = glnx_chase_and_mkdirat (app_id_dir_fd, ".ld.so",
+                                             GLNX_CHASE_RESOLVE_NO_SYMLINKS,
+                                             0755, error);
+
+      ld_so_dir_path = g_build_filename (flatpak_file_get_path_cached (app_id_dir),
+                                         ".ld.so",
+                                         NULL);
+    }
   else
     {
-      g_autoptr(GFile) base_dir = g_file_new_for_path (g_get_user_cache_dir ());
-      ld_so_dir = g_file_resolve_relative_path (base_dir, "flatpak/ld.so");
+      glnx_autofd int cache_dir_fd = -1;
+
+      cache_dir_fd = glnx_chase_and_mkdirat (AT_FDCWD,
+                                             g_get_user_cache_dir (),
+                                             GLNX_CHASE_DEFAULT,
+                                             0700, error);
+      if (cache_dir_fd < 0)
+        {
+          g_prefix_error (error, "cannot open %s: ",
+                          g_get_user_cache_dir ());
+          return -1;
+        }
+
+      ld_so_dir_fd = glnx_chase_and_mkdirat (cache_dir_fd, "flatpak/ld.so",
+                                             GLNX_CHASE_RESOLVE_NO_SYMLINKS,
+                                             0755, error);
+
+      ld_so_dir_path = g_build_filename (g_get_user_cache_dir (),
+                                         "flatpak/ld.so",
+                                         NULL);
     }
 
-  ld_so_cache = g_file_get_child (ld_so_dir, checksum);
-  ld_so_fd = open (flatpak_file_get_path_cached (ld_so_cache), O_RDONLY);
-  if (ld_so_fd >= 0)
-    return g_steal_fd (&ld_so_fd);
+  if (ld_so_dir_fd < 0)
+    {
+      g_prefix_error (error, "cannot open %s: ", ld_so_dir_path);
+      return -1;
+    }
 
-  g_info ("Regenerating ld.so.cache %s", flatpak_file_get_path_cached (ld_so_cache));
+  if (glnx_openat_rdonly (ld_so_dir_fd, checksum, FALSE, &ld_so_fd, NULL))
+    return g_steal_fd (&ld_so_fd);
 
-  if (!flatpak_mkdir_p (ld_so_dir, cancellable, error))
-    return FALSE;
+  g_info ("Regenerating ld.so.cache %s/%s", ld_so_dir_path, checksum);
 
   minimal_envp = flatpak_run_get_minimal_env (FALSE, FALSE);
   bwrap = flatpak_bwrap_new (minimal_envp);
@@ -2740,7 +2839,6 @@
   glnx_gen_temp_name (tmp_basename);
 
   sandbox_cache_path = g_build_filename ("/run/ld-so-cache-dir", tmp_basename, NULL);
-  ld_so_cache_tmp = g_file_get_child (ld_so_dir, tmp_basename);
 
   flatpak_bwrap_add_args (bwrap,
                           "--unshare-pid",
@@ -2748,8 +2846,13 @@
                           "--unshare-net",
                           "--proc", "/proc",
                           "--dev", "/dev",
-                          "--bind", flatpak_file_get_path_cached (ld_so_dir), "/run/ld-so-cache-dir",
                           NULL);
+
+  if (!flatpak_bwrap_add_args_data_fd_dup (bwrap, "--bind-fd",
+                                           ld_so_dir_fd, "/run/ld-so-cache-dir",
+                                           error))
+    return -1;
+
   flatpak_bwrap_sort_envp (bwrap);
   flatpak_bwrap_envp_to_args (bwrap);
 
@@ -2787,8 +2890,7 @@
       return -1;
     }
 
-  ld_so_fd = open (flatpak_file_get_path_cached (ld_so_cache_tmp), O_RDONLY);
-  if (ld_so_fd < 0)
+  if (!glnx_openat_rdonly (ld_so_dir_fd, tmp_basename, FALSE, &ld_so_fd, NULL))
     {
       flatpak_fail_error (error, FLATPAK_ERROR_SETUP_FAILED, _("Can't open generated ld.so.cache"));
       return -1;
@@ -2797,22 +2899,20 @@
   if (app_id_dir == NULL)
     {
       /* For runs without an app id dir we always regenerate the ld.so.cache */
-      unlink (flatpak_file_get_path_cached (ld_so_cache_tmp));
+      unlinkat (ld_so_dir_fd, tmp_basename, 0);
     }
   else
     {
-      g_autoptr(GFile) active = g_file_get_child (ld_so_dir, "active");
-
       /* For app-dirs we keep one checksum alive, by pointing the active symlink to it */
 
       /* Rename to known name, possibly overwriting existing ref if race */
-      if (rename (flatpak_file_get_path_cached (ld_so_cache_tmp), flatpak_file_get_path_cached (ld_so_cache)) == -1)
+      if (renameat (ld_so_dir_fd, tmp_basename, ld_so_dir_fd, checksum) == -1)
         {
           glnx_set_error_from_errno (error);
           return -1;
         }
 
-      if (!flatpak_switch_symlink_and_remove (flatpak_file_get_path_cached (active),
+      if (!flatpak_switch_symlink_and_remove (ld_so_dir_fd, "active",
                                               checksum, error))
         return -1;
     }
diff -Nru flatpak-1.16.6/common/flatpak-run-dbus.c flatpak-1.16.6/common/flatpak-run-dbus.c
--- flatpak-1.16.6/common/flatpak-run-dbus.c	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/common/flatpak-run-dbus.c	2026-08-11 14:32:06.000000000 +0100
@@ -451,8 +451,8 @@
   flatpak_bwrap_add_args (proxy_arg_bwrap,
                           a11y_address,
                           proxy_socket, "--filter", "--sloppy-names",
-                          "--broadcast=org.a11y.atspi.Registry.EventListenerRegistered=@/org/a11y/atspi/registry",
-                          "--broadcast=org.a11y.atspi.Registry.EventListenerDeregistered=@/org/a11y/atspi/registry",
+                          "--broadcast=org.a11y.atspi.Registry=org.a11y.atspi.Registry.EventListenerRegistered@/org/a11y/atspi/registry",
+                          "--broadcast=org.a11y.atspi.Registry=org.a11y.atspi.Registry.EventListenerDeregistered@/org/a11y/atspi/registry",
                           "--call=org.a11y.atspi.Registry=org.a11y.atspi.Socket.Embed@/org/a11y/atspi/accessible/root",
                           "--call=org.a11y.atspi.Registry=org.a11y.atspi.Socket.Unembed@/org/a11y/atspi/accessible/root",
                           "--call=org.a11y.atspi.Registry=org.a11y.atspi.Registry.GetRegisteredEvents@/org/a11y/atspi/registry",
diff -Nru flatpak-1.16.6/common/flatpak-utils.c flatpak-1.16.6/common/flatpak-utils.c
--- flatpak-1.16.6/common/flatpak-utils.c	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/common/flatpak-utils.c	2026-08-11 14:32:06.000000000 +0100
@@ -938,34 +938,38 @@
   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
- * remove any existing symlink target (once), independent of how many
- * processes do the same operation in parallele. However, it is still
- * possible that we remove the old and then fail to create the new
- * 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.
+/* Atomically replace the @symlink_name symlink in @dir_fd with one
+ * pointing to @target, removing the previous target file (if it
+ * exists and differs from @target).
+ *
+ * This is atomic in the sense that we're guaranteed to remove any
+ * existing symlink target (once), independent of how many processes
+ * do the same operation in parallel. However, it is still possible
+ * that we remove the old and then fail to create the new 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.
+ *
+ * All filesystem operations go through @dir_fd so the caller can
+ * guarantee the directory identity even if the path to it is
+ * attacker-controlled.
+ *
+ * @target shall only contain the characters a-zA-Z0-9 so that the
+ * old target file to be removed is always in the same directory.
  */
 gboolean
-flatpak_switch_symlink_and_remove (const char *symlink_path,
+flatpak_switch_symlink_and_remove (int         dir_fd,
+                                   const char *symlink_name,
                                    const char *target,
                                    GError    **error)
 {
-  g_autofree char *symlink_dir = g_path_get_dirname (symlink_path);
-  int try;
-
-  for (try = 0; try < 100; try++)
+  for (size_t try = 0; try < 100; try++)
     {
-      g_autofree char *tmp_path = NULL;
-      int fd;
+      g_autofree char *tmp_name = NULL;
+      glnx_autofd int fd = -1;
 
       /* Try to atomically create the symlink */
-      if (TEMP_FAILURE_RETRY (symlink (target, symlink_path)) == 0)
+      if (TEMP_FAILURE_RETRY (symlinkat (target, dir_fd, symlink_name)) == 0)
         return TRUE;
 
       if (errno != EEXIST)
@@ -975,48 +979,52 @@
           return FALSE;
         }
 
-      /* The symlink existed, move it to a temporary name atomically, and remove target
-         if that succeeded. */
-      tmp_path = g_build_filename (symlink_dir, ".switched-symlink-XXXXXX", NULL);
+      /* The symlink existed, move it to a temporary name atomically,
+       * and remove its target if that succeeded.
+       * This generated filename doesn't need to be securely unique:
+       * we're only protecting against accidental collisions if two
+       * processes try to regenerate ld.so.cache at the same time. */
+      tmp_name = g_strdup_printf (".switched-symlink-%08X", g_random_int ());
 
-      fd = g_mkstemp_full (tmp_path, O_RDWR, 0644);
+      fd = openat (dir_fd, tmp_name,
+                   O_CREAT | O_WRONLY | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0644);
       if (fd == -1)
         {
+          if (errno == EEXIST)
+            continue;
+
           glnx_set_error_from_errno (error);
           return FALSE;
         }
-      close (fd);
+      g_clear_fd (&fd, NULL);
 
-      if (TEMP_FAILURE_RETRY (rename (symlink_path, tmp_path)) == 0)
+      if (TEMP_FAILURE_RETRY (renameat (dir_fd, symlink_name,
+                                        dir_fd, tmp_name)) == 0)
         {
           /* The move succeeded, now we can remove the old target */
-          g_autofree char *old_target = flatpak_readlink (tmp_path, error);
+          g_autofree char *old_target = NULL;
+
+          old_target = glnx_readlinkat_malloc (dir_fd, tmp_name, NULL, error);
           if (old_target == NULL)
             return FALSE;
 
-          /* Don't remove old file if its the same as the new one */
+          /* Don't remove old file if it's the same as the new one */
           if (strcmp (old_target, target) != 0)
             {
               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);
-                }
+                unlinkat (dir_fd, old_target, 0);
               else
-                {
-                  g_warning ("Refusing to delete old link target %s", old_target);
-                }
+                g_warning ("Refusing to delete old link target %s", old_target);
             }
         }
       else if (errno != ENOENT)
         {
           glnx_set_error_from_errno (error);
-          unlink (tmp_path);
-          return -1;
+          unlinkat (dir_fd, tmp_name, 0);
+          return FALSE;
         }
-      unlink (tmp_path);
+
+      unlinkat (dir_fd, tmp_name, 0);
 
       /* An old target was removed, try again */
     }
@@ -1174,131 +1182,82 @@
 }
 
 gboolean
-flatpak_cp_a (GFile         *src,
-              GFile         *dest,
-              FlatpakCpFlags flags,
-              GCancellable  *cancellable,
-              GError       **error)
+flatpak_cp_a_at (int            src_dfd,
+                 int            dest_parent_dfd,
+                 const char    *dest_name,
+                 FlatpakCpFlags flags,
+                 GCancellable  *cancellable,
+                 GError       **error)
 {
-  gboolean ret = FALSE;
-  GFileEnumerator *enumerator = NULL;
-  GFileInfo *src_info = NULL;
-  GFile *dest_child = NULL;
-  int dest_dfd = -1;
   gboolean merge = (flags & FLATPAK_CP_FLAGS_MERGE) != 0;
   gboolean no_chown = (flags & FLATPAK_CP_FLAGS_NO_CHOWN) != 0;
-  gboolean move = (flags & FLATPAK_CP_FLAGS_MOVE) != 0;
-  g_autoptr(GFileInfo) child_info = NULL;
-  GError *temp_error = NULL;
-  int r;
-
-  enumerator = g_file_enumerate_children (src, "standard::type,standard::name,unix::uid,unix::gid,unix::mode",
-                                          G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
-                                          cancellable, error);
-  if (!enumerator)
-    goto out;
-
-  src_info = g_file_query_info (src, "standard::name,unix::mode,unix::uid,unix::gid," \
-                                     "time::modified,time::modified-usec,time::access,time::access-usec",
-                                G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
-                                cancellable, error);
-  if (!src_info)
-    goto out;
+  struct stat src_stbuf;
+  glnx_autofd int dest_dfd = -1;
+  g_auto(GLnxDirFdIterator) src_iter = { 0, };
 
-  do
-    r = mkdir (flatpak_file_get_path_cached (dest), 0755);
-  while (G_UNLIKELY (r == -1 && errno == EINTR));
-  if (r == -1 &&
+  if (fstat (src_dfd, &src_stbuf) != 0)
+    return glnx_throw_errno_prefix (error, "fstat");
+
+  if (TEMP_FAILURE_RETRY (mkdirat (dest_parent_dfd, dest_name, 0755)) != 0 &&
       (!merge || errno != EEXIST))
-    {
-      glnx_set_error_from_errno (error);
-      goto out;
-    }
+    return glnx_throw_errno_prefix (error, "mkdirat(%s)", dest_name);
 
-  if (!glnx_opendirat (AT_FDCWD, flatpak_file_get_path_cached (dest), TRUE,
-                       &dest_dfd, error))
-    goto out;
+  if (!glnx_opendirat (dest_parent_dfd, dest_name, FALSE, &dest_dfd, error))
+    return FALSE;
 
   if (!no_chown)
     {
-      do
-        r = fchown (dest_dfd,
-                    g_file_info_get_attribute_uint32 (src_info, "unix::uid"),
-                    g_file_info_get_attribute_uint32 (src_info, "unix::gid"));
-      while (G_UNLIKELY (r == -1 && errno == EINTR));
-      if (r == -1)
-        {
-          glnx_set_error_from_errno (error);
-          goto out;
-        }
+      if (TEMP_FAILURE_RETRY (fchown (dest_dfd, src_stbuf.st_uid, src_stbuf.st_gid)) != 0)
+        return glnx_throw_errno_prefix (error, "fchown");
     }
 
-  do
-    r = fchmod (dest_dfd, g_file_info_get_attribute_uint32 (src_info, "unix::mode"));
-  while (G_UNLIKELY (r == -1 && errno == EINTR));
+  if (TEMP_FAILURE_RETRY (fchmod (dest_dfd, src_stbuf.st_mode & 07777)) != 0)
+    return glnx_throw_errno_prefix (error, "fchmod");
 
-  if (dest_dfd != -1)
-    {
-      (void) close (dest_dfd);
-      dest_dfd = -1;
-    }
+  if (!glnx_dirfd_iterator_init_at (src_dfd, ".", FALSE, &src_iter, error))
+    return FALSE;
 
-  while ((child_info = g_file_enumerator_next_file (enumerator, cancellable, &temp_error)))
+  while (TRUE)
     {
-      const char *name = g_file_info_get_name (child_info);
-      g_autoptr(GFile) src_child = g_file_get_child (src, name);
+      struct dirent *dent;
+      const char *name;
 
-      if (dest_child)
-        g_object_unref (dest_child);
-      dest_child = g_file_get_child (dest, name);
+      if (!glnx_dirfd_iterator_next_dent_ensure_dtype (&src_iter, &dent,
+                                                       cancellable, error))
+        return FALSE;
 
-      if (g_file_info_get_file_type (child_info) == G_FILE_TYPE_DIRECTORY)
+      if (dent == NULL)
+        break;
+
+      name = dent->d_name;
+
+      if (dent->d_type == DT_DIR)
         {
-          if (!flatpak_cp_a (src_child, dest_child, flags,
-                             cancellable, error))
-            goto out;
+          glnx_autofd int child_src_dfd = -1;
+
+          if (!glnx_opendirat (src_iter.fd, name, FALSE, &child_src_dfd, error))
+            return FALSE;
+
+          if (!flatpak_cp_a_at (child_src_dfd, dest_dfd,
+                                name, flags, cancellable, error))
+            return FALSE;
         }
       else
         {
-          (void) unlink (flatpak_file_get_path_cached (dest_child));
-          GFileCopyFlags copyflags = G_FILE_COPY_OVERWRITE | G_FILE_COPY_NOFOLLOW_SYMLINKS;
-          if (!no_chown)
-            copyflags |= G_FILE_COPY_ALL_METADATA;
-          if (move)
-            {
-              if (!g_file_move (src_child, dest_child, copyflags,
-                                cancellable, NULL, NULL, error))
-                goto out;
-            }
-          else
-            {
-              if (!g_file_copy (src_child, dest_child, copyflags,
-                                cancellable, NULL, NULL, error))
-                goto out;
-            }
+          GLnxFileCopyFlags copyflags = GLNX_FILE_COPY_OVERWRITE;
+          if (no_chown)
+            copyflags |= GLNX_FILE_COPY_NOCHOWN;
+
+          (void) unlinkat (dest_dfd, name, 0);
+
+          if (!glnx_file_copy_at (src_iter.fd, name, NULL,
+                                  dest_dfd, name, copyflags,
+                                  cancellable, error))
+            return FALSE;
         }
-
-      g_clear_object (&child_info);
     }
 
-  if (temp_error != NULL)
-    {
-      g_propagate_error (error, temp_error);
-      goto out;
-    }
-
-  if (move &&
-      !g_file_delete (src, NULL, error))
-    goto out;
-
-  ret = TRUE;
-out:
-  if (dest_dfd != -1)
-    (void) close (dest_dfd);
-  g_clear_object (&src_info);
-  g_clear_object (&enumerator);
-  g_clear_object (&dest_child);
-  return ret;
+  return TRUE;
 }
 
 static gboolean
diff -Nru flatpak-1.16.6/common/flatpak-utils-private.h flatpak-1.16.6/common/flatpak-utils-private.h
--- flatpak-1.16.6/common/flatpak-utils-private.h	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/common/flatpak-utils-private.h	2026-08-11 14:32:06.000000000 +0100
@@ -154,7 +154,8 @@
   return mutex;
 }
 
-gboolean flatpak_switch_symlink_and_remove (const char *symlink_path,
+gboolean flatpak_switch_symlink_and_remove (int         dir_fd,
+                                            const char *symlink_name,
                                             const char *target,
                                             GError    **error);
 
@@ -195,14 +196,14 @@
   FLATPAK_CP_FLAGS_NONE = 0,
   FLATPAK_CP_FLAGS_MERGE = 1 << 0,
   FLATPAK_CP_FLAGS_NO_CHOWN = 1 << 1,
-  FLATPAK_CP_FLAGS_MOVE = 1 << 2,
 } FlatpakCpFlags;
 
-gboolean   flatpak_cp_a (GFile         *src,
-                         GFile         *dest,
-                         FlatpakCpFlags flags,
-                         GCancellable  *cancellable,
-                         GError       **error);
+gboolean   flatpak_cp_a_at (int            src_dfd,
+                            int            dest_parent_dfd,
+                            const char    *dest_name,
+                            FlatpakCpFlags flags,
+                            GCancellable  *cancellable,
+                            GError       **error);
 
 gboolean flatpak_mkdir_p (GFile        *dir,
                           GCancellable *cancellable,
diff -Nru flatpak-1.16.6/debian/changelog flatpak-1.16.6/debian/changelog
--- flatpak-1.16.6/debian/changelog	2026-04-10 20:03:53.000000000 +0100
+++ flatpak-1.16.6/debian/changelog	2026-08-11 14:03:38.000000000 +0100
@@ -1,3 +1,71 @@
+flatpak (1.16.6-1~deb13u2) trixie-security; urgency=high
+
+  * d/patches: Backport security fixes from 1.18.1 (Closes: #1144130)
+    - d/p/libglnx/*.patch:
+      Backport glnx_chase_and_mkdirat() utility function, required by some
+      of the security fixes below
+    - d/p/tests/*.patch:
+      Backport unit tests fixes which are required by the tests for some
+      of the security fixes below
+    - d/p/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/*.patch:
+      + GHSA-fqx6-vh4p-42cg:
+        Fix writing outside installation directory via crafted commit metadata.
+        A malicious or compromised Flatpak repository could write
+        attacker-controlled files outside /var/lib/flatpak as root.
+      + GHSA-8qxj-x646-phcm:
+        Fix writing outside working directory in `flatpak build-init`.
+        A malicious or compromised SDK could write outside the intended
+        working directory when a developer starts using it for a build.
+    - d/p/GHSA-qrwq-7qwx-q9rp/*.patch:
+      Fix local privilege escalation involving revokefs.
+      A malicious local user could write files outside /var/lib/flatpak
+      as root by tampering with OSTree objects after signature verification.
+    - d/p/GHSA-8688-9x26-hhxj/*.patch:
+      Fix a sandbox escape involving directories inside ~/.var/app/APP_ID.
+      A malicious or compromised Flatpak app could write to arbitrary files
+      outside its sandbox.
+    - d/p/GHSA-99wv-m8rp-g58x/*.patch:
+      Fix a sandbox escape involving the ld.so cache.
+      A malicious or compromised Flatpak app could write files with a fixed
+      name and limited control over content outside the sandbox.
+    - d/p/GHSA-v2gw-v9h5-9q4x/*.patch:
+      Fix local privilege escalation involving crafted OCI architecture names.
+      A malicious local user on a system with an OCI remote configured
+      (unusual on non-Fedora systems) could trick the flatpak-system-helper
+      process into writing outside /var/lib/flatpak.
+    - d/p/GHSA-w69g-9x8j-7p8f/*.patch:
+      Fix reading outside sandbox involving crafted extension metadata.
+      A malicious or compromised Flatpak app could find out whether specific
+      files exist outside the sandbox.
+    - d/p/GHSA-q4gr-vc25-57m5/*.patch:
+      Fix anti-downgrade checks for components installed system-wide.
+      A malicious local user with an active local login session could
+      downgrade an app, runtime or extension to an older, known-vulnerable
+      version and use this to attack other local users.
+    - d/p/GHSA-jr92-2v97-wgvc/*.patch:
+      Fix a buffer overflow when installing or updating from a malicious OCI
+      registry, not believed to be practically exploitable on 64-bit systems.
+    - d/p/hardening/*.patch:
+      Harden file accesses against path traversal, fixing issues that
+      were initially thought to be security vulnerabilities similar to
+      those above, but on further analysis do not seem to be exploitable.
+    - d/p/GHSA-r7hp-698j-2h6c/*.patch:
+      Correct xdg-dbus-proxy rules for receiving selected AT-SPI broadcasts
+      so that GTK accessibility features work as intended.
+      Previously, these accessibility features only worked accidentally as a
+      result of an xdg-dbus-proxy security issue, fixed in 0.1.8.
+  * d/patches: Add additional bug fixes from upstream 1.16.x branch
+    - d/p/subprojects-Ignore-.wraplock-file-generated-by-recent-Mes.patch,
+      d/p/bwrap-Clarify-a-comment.patch,
+      d/p/subprojects-Update-dbus-proxy.wrap-to-v0.1.7.patch:
+      Resync with upstream source, no functional changes
+    - d/p/dir-Use-flatpak_bwrap_child_setup_inherit_fds_cb-to-apply.patch:
+      Silence a spurious warning when apps use the extra_data mechanism
+    - d/p/portal-Actually-use-the-AppInfo-hash-table.patch:
+      Fix a memory leak and potential rare crashes in flatpak-portal
+
+ -- Simon McVittie <smcv@debian.org>  Tue, 11 Aug 2026 14:03:38 +0100
+
 flatpak (1.16.6-1~deb13u1) trixie-security; urgency=high
 
   * Backport new upstream stable release for Debian 13
diff -Nru flatpak-1.16.6/debian/patches/bwrap-Clarify-a-comment.patch flatpak-1.16.6/debian/patches/bwrap-Clarify-a-comment.patch
--- flatpak-1.16.6/debian/patches/bwrap-Clarify-a-comment.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/bwrap-Clarify-a-comment.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,29 @@
+From: Simon McVittie <smcv@collabora.com>
+Date: Sat, 11 Apr 2026 16:59:10 +0100
+Subject: bwrap: Clarify a comment
+
+Now that we're passing the app's /app and /usr down to bwrap as O_PATH
+file descriptors, it will be even more common to have non-seekable fds
+in the array.
+
+Signed-off-by: Simon McVittie <smcv@collabora.com>
+(cherry picked from commit dc9173b2d330f3ac411b135f261e81551db05f3f)
+Origin: upstream, 1.16.7, commit:57a5f51199ea8f35e1236857d962e4ac6cbe2a57
+---
+ common/flatpak-bwrap.c | 3 ++-
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+diff --git a/common/flatpak-bwrap.c b/common/flatpak-bwrap.c
+index a0eddfd..acec2ab 100644
+--- a/common/flatpak-bwrap.c
++++ b/common/flatpak-bwrap.c
+@@ -533,7 +533,8 @@ flatpak_bwrap_child_setup (GArray *fd_array,
+          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.16.6/debian/patches/dir-Use-flatpak_bwrap_child_setup_inherit_fds_cb-to-apply.patch flatpak-1.16.6/debian/patches/dir-Use-flatpak_bwrap_child_setup_inherit_fds_cb-to-apply.patch
--- flatpak-1.16.6/debian/patches/dir-Use-flatpak_bwrap_child_setup_inherit_fds_cb-to-apply.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/dir-Use-flatpak_bwrap_child_setup_inherit_fds_cb-to-apply.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,64 @@
+From: Simon McVittie <smcv@collabora.com>
+Date: Sat, 11 Apr 2026 17:06:03 +0100
+Subject: dir: Use flatpak_bwrap_child_setup_inherit_fds_cb() to apply
+ extra-data
+
+This is functionally equivalent to the local child_setup() deleted by
+this commit, except that it ignores lseek() errors, which can
+legitimately happen when inheriting a non-seekable file descriptor.
+Since commit ac62ebe "run: Use O_PATH fds for the runtime and app
+deploy directories", any extra-data helper that runs inside a runtime
+will receive a non-seekable O_PATH fd as its /usr.
+
+Resolves: https://github.com/flatpak/flatpak/issues/6608
+Signed-off-by: Simon McVittie <smcv@collabora.com>
+(cherry picked from commit 333459c8835ff95b245d97ee98cffec1c9f781d9)
+Origin: upstream, 1.16.7, commit:fb1ed94d3c37c072f5e501e5b02c34073081d464
+---
+ common/flatpak-dir.c | 26 +-------------------------
+ 1 file changed, 1 insertion(+), 25 deletions(-)
+
+diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c
+index e381b16..6e3f891 100644
+--- a/common/flatpak-dir.c
++++ b/common/flatpak-dir.c
+@@ -8324,30 +8324,6 @@ extract_extra_data (FlatpakDir   *self,
+   return TRUE;
+ }
+ 
+-static void
+-child_setup (gpointer user_data)
+-{
+-  GArray *fd_array = user_data;
+-  int i;
+-
+-  /* If no fd_array was specified, don't care. */
+-  if (fd_array == NULL)
+-    return;
+-
+-  /* Otherwise, mark not - close-on-exec all the fds in the array */
+-  for (i = 0; i < fd_array->len; i++)
+-    {
+-      int fd = g_array_index (fd_array, int, i);
+-
+-      /* 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");
+-
+-      fcntl (fd, F_SETFD, 0);
+-    }
+-}
+-
+ static gboolean
+ apply_extra_data (FlatpakDir   *self,
+                   GFile        *checkoutdir,
+@@ -8521,7 +8497,7 @@ apply_extra_data (FlatpakDir   *self,
+                      (char **) bwrap->argv->pdata,
+                      bwrap->envp,
+                      G_SPAWN_SEARCH_PATH,
+-                     child_setup, bwrap->fds,
++                     flatpak_bwrap_child_setup_inherit_fds_cb, bwrap->fds,
+                      NULL, NULL,
+                      &exit_status,
+                      error))
diff -Nru flatpak-1.16.6/debian/patches/GHSA-8688-9x26-hhxj/run-Harden-ensure_data_dir-and-var-setup-against-symlink-.patch flatpak-1.16.6/debian/patches/GHSA-8688-9x26-hhxj/run-Harden-ensure_data_dir-and-var-setup-against-symlink-.patch
--- flatpak-1.16.6/debian/patches/GHSA-8688-9x26-hhxj/run-Harden-ensure_data_dir-and-var-setup-against-symlink-.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-8688-9x26-hhxj/run-Harden-ensure_data_dir-and-var-setup-against-symlink-.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,132 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Tue, 16 Jun 2026 21:12:48 +0200
+Subject: run: Harden ensure_data_dir and /var setup against symlink attacks
+
+Replace path-based flatpak_mkdir_p calls in flatpak_ensure_data_dir
+with glnx_chase_and_mkdirat(RESOLVE_NO_SYMLINKS) to prevent an app
+from replacing subdirectories of its data dir with symlinks between
+runs and having them followed during the next sandbox setup.
+
+In flatpak_run_setup_base_argv, replace path-based --bind args for
+the app cache/data/config/tmp directories with --bind-fd using fds
+obtained via glnx_chaseat(RESOLVE_NO_SYMLINKS), preventing both
+symlink following and TOCTOU races when setting up these bind mounts.
+
+Assisted-by: Claude:opus-4.6
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-8688-9x26-hhxj
+Origin: upstream, 1.18.1
+---
+ common/flatpak-run.c | 88 ++++++++++++++++++++++++++++++++++------------------
+ 1 file changed, 57 insertions(+), 31 deletions(-)
+
+diff --git a/common/flatpak-run.c b/common/flatpak-run.c
+index 2793d1a..11e0748 100644
+--- a/common/flatpak-run.c
++++ b/common/flatpak-run.c
+@@ -761,30 +761,35 @@ flatpak_ensure_data_dir (GFile        *app_id_dir,
+                          GCancellable *cancellable,
+                          GError      **error)
+ {
+-  g_autoptr(GFile) data_dir = g_file_get_child (app_id_dir, "data");
+-  g_autoptr(GFile) cache_dir = g_file_get_child (app_id_dir, "cache");
+-  g_autoptr(GFile) fontconfig_cache_dir = g_file_get_child (cache_dir, "fontconfig");
+-  g_autoptr(GFile) tmp_dir = g_file_get_child (cache_dir, "tmp");
+-  g_autoptr(GFile) config_dir = g_file_get_child (app_id_dir, "config");
+-  g_autoptr(GFile) state_dir = g_file_get_child (app_id_dir, ".local/state");
+-
+-  if (!flatpak_mkdir_p (data_dir, cancellable, error))
+-    return FALSE;
+-
+-  if (!flatpak_mkdir_p (cache_dir, cancellable, error))
+-    return FALSE;
+-
+-  if (!flatpak_mkdir_p (fontconfig_cache_dir, cancellable, error))
+-    return FALSE;
++  glnx_autofd int app_id_dir_fd = -1;
++  static const char *const subdirs[] = {
++    "data",
++    "cache",
++    "cache/fontconfig",
++    "cache/tmp",
++    "config",
++    ".local/state",
++  };
+ 
+-  if (!flatpak_mkdir_p (tmp_dir, cancellable, error))
++  app_id_dir_fd = glnx_chase_and_mkdirat (AT_FDCWD,
++                                          flatpak_file_get_path_cached (app_id_dir),
++                                          GLNX_CHASE_DEFAULT,
++                                          0755,
++                                          error);
++  if (app_id_dir_fd < 0)
+     return FALSE;
+ 
+-  if (!flatpak_mkdir_p (config_dir, cancellable, error))
+-    return FALSE;
++  for (size_t i = 0; i < G_N_ELEMENTS (subdirs); i++)
++    {
++      glnx_autofd int fd = -1;
+ 
+-  if (!flatpak_mkdir_p (state_dir, cancellable, error))
+-    return FALSE;
++      fd = glnx_chase_and_mkdirat (app_id_dir_fd, subdirs[i],
++                                   GLNX_CHASE_RESOLVE_BENEATH,
++                                   0755,
++                                   error);
++      if (fd < 0)
++        return FALSE;
++    }
+ 
+   return TRUE;
+ }
+@@ -2441,18 +2446,39 @@ flatpak_run_setup_base_argv (FlatpakBwrap   *bwrap,
+ 
+   if (app_id_dir != NULL)
+     {
+-      g_autoptr(GFile) app_cache_dir = g_file_get_child (app_id_dir, "cache");
+-      g_autoptr(GFile) app_tmp_dir = g_file_get_child (app_cache_dir, "tmp");
+-      g_autoptr(GFile) app_data_dir = g_file_get_child (app_id_dir, "data");
+-      g_autoptr(GFile) app_config_dir = g_file_get_child (app_id_dir, "config");
++      glnx_autofd int app_id_dir_fd = -1;
++      static const struct
++        {
++          const char *src;
++          const char *dst;
++        }
++      mounts[] = {
++        { "cache", "/var/cache" },
++        { "data", "/var/data" },
++        { "config", "/var/config" },
++        { "cache/tmp", "/var/tmp" },
++      };
++
++      app_id_dir_fd = glnx_chaseat (AT_FDCWD,
++                                    flatpak_file_get_path_cached (app_id_dir),
++                                    GLNX_CHASE_MUST_BE_DIRECTORY,
++                                    error);
++      if (app_id_dir_fd < 0)
++        return FALSE;
+ 
+-      flatpak_bwrap_add_args (bwrap,
+-                              /* These are nice to have as a fixed path */
+-                              "--bind", flatpak_file_get_path_cached (app_cache_dir), "/var/cache",
+-                              "--bind", flatpak_file_get_path_cached (app_data_dir), "/var/data",
+-                              "--bind", flatpak_file_get_path_cached (app_config_dir), "/var/config",
+-                              "--bind", flatpak_file_get_path_cached (app_tmp_dir), "/var/tmp",
+-                              NULL);
++      for (i = 0; i < G_N_ELEMENTS (mounts); i++)
++        {
++          glnx_autofd int fd = -1;
++
++          fd = glnx_chase_and_mkdirat (app_id_dir_fd, mounts[i].src,
++                                       GLNX_CHASE_RESOLVE_BENEATH,
++                                       0755, error);
++          if (fd < 0)
++            return FALSE;
++
++          flatpak_bwrap_add_args_data_fd (bwrap, "--bind-fd",
++                                          g_steal_fd (&fd), mounts[i].dst);
++        }
+     }
+ 
+   flatpak_run_setup_usr_links (bwrap, runtime_fd, NULL);
diff -Nru flatpak-1.16.6/debian/patches/GHSA-8688-9x26-hhxj/test-run-Check-that-the-way-com.valvesoftware.Steam-behav.patch flatpak-1.16.6/debian/patches/GHSA-8688-9x26-hhxj/test-run-Check-that-the-way-com.valvesoftware.Steam-behav.patch
--- flatpak-1.16.6/debian/patches/GHSA-8688-9x26-hhxj/test-run-Check-that-the-way-com.valvesoftware.Steam-behav.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-8688-9x26-hhxj/test-run-Check-that-the-way-com.valvesoftware.Steam-behav.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,107 @@
+From: Simon McVittie <smcv@collabora.com>
+Date: Sun, 9 Aug 2026 12:08:24 +0100
+Subject: test-run: Check that the way com.valvesoftware.Steam behaves still
+ works
+
+This use of symlinks to ensure that the canonicalized paths of
+`$XDG_CACHE_HOME`, `$XDG_CONFIG_HOME`, `$XDG_DATA_HOME` are
+`~/.cache`, `~/.config`, `~/.local/share` is the sort of thing that
+could easily regress if not tested.
+
+Signed-off-by: Simon McVittie <smcv@collabora.com>
+[backported to 1.18.x: adjust test numbering]
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-8688-9x26-hhxj
+Origin: upstream, 1.18.1
+---
+ tests/test-run.sh | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-
+ 1 file changed, 70 insertions(+), 1 deletion(-)
+
+diff --git a/tests/test-run.sh b/tests/test-run.sh
+index d5bf37d..45abd56 100644
+--- a/tests/test-run.sh
++++ b/tests/test-run.sh
+@@ -24,7 +24,7 @@ set -euo pipefail
+ skip_without_bwrap
+ skip_revokefs_without_fuse
+ 
+-echo "1..27"
++echo "1..28"
+ 
+ # Use stable rather than master as the branch so we can test that the run
+ # command automatically finds the branch correctly
+@@ -595,6 +595,75 @@ assert_file_has_content "$HOME/.var/app/org.test.Hello/.persistrc" "can-persist"
+ 
+ ok "--persist=. persists all files"
+ 
++# com.valvesoftware.Steam is an example of an app that wants to use the
++# canonical paths ~/.cache, ~/.config and ~/.local/share, to work around
++# games that don't support the XDG basedir spec properly
++app_dir="$HOME/.var/app/org.test.Hello"
++rm -fr "$app_dir"
++mkdir -p "$app_dir"
++for subdir in .cache .config .local .local/share; do
++    mkdir -m700 "$app_dir/$subdir"
++done
++# This is the symlink that com.valvesoftware.Steam uses to achieve that
++ln -fns .cache "$app_dir/cache"
++ln -fns .config "$app_dir/config"
++ln -fns .local/share "$app_dir/data"
++run --command=sh --persist=. org.test.Hello -c '
++echo "HOME=$HOME"
++echo "XDG_CACHE_HOME=$XDG_CACHE_HOME"
++echo "XDG_CONFIG_HOME=$XDG_CONFIG_HOME"
++echo "XDG_DATA_HOME=$XDG_DATA_HOME"
++echo "via-HOME" > ~/via-HOME
++echo "via-var-app" > ~/.var/app/org.test.Hello/via-var-app
++echo "via-XDG_CACHE_HOME" > "$XDG_CACHE_HOME/via-XDG_CACHE_HOME"
++echo "via-XDG_CONFIG_HOME" > "$XDG_CONFIG_HOME/via-XDG_CONFIG_HOME"
++echo "via-XDG_DATA_HOME" > "$XDG_DATA_HOME/via-XDG_DATA_HOME"
++echo "via-.cache" > ~/.cache/via-.cache
++echo "via-.config" > ~/.config/via-.config
++echo "via-.local-share" > ~/.local/share/via-.local-share
++echo "via-cache" > ~/cache/via-cache
++echo "via-config" > ~/config/via-config
++echo "via-data" > ~/data/via-data
++echo "via-var-cache" > /var/cache/via-var-cache
++echo "via-var-config" > /var/config/via-var-config
++echo "via-var-data" > /var/data/via-var-data
++' >&2
++# $HOME and $app_dir in the sandbox are both the same as $app_dir outside
++assert_file_has_content "$app_dir/via-HOME" '^via-HOME$'
++assert_file_has_content "$app_dir/via-var-app" '^via-var-app$'
++# $app_dir/.cache, $app_dir/cache, /var/cache are the same place
++assert_file_has_content "$app_dir/cache/via-XDG_CACHE_HOME" '^via-XDG_CACHE_HOME$'
++assert_file_has_content "$app_dir/cache/via-.cache" '^via-\.cache$'
++assert_file_has_content "$app_dir/cache/via-cache" '^via-cache$'
++assert_file_has_content "$app_dir/cache/via-var-cache" '^via-var-cache$'
++# Same for config
++assert_file_has_content "$app_dir/config/via-XDG_CONFIG_HOME" '^via-XDG_CONFIG_HOME$'
++assert_file_has_content "$app_dir/config/via-.config" '^via-\.config$'
++assert_file_has_content "$app_dir/config/via-config" '^via-config$'
++assert_file_has_content "$app_dir/config/via-var-config" '^via-var-config$'
++# Same for data, except the canonical name is .local/share
++assert_file_has_content "$app_dir/data/via-XDG_DATA_HOME" '^via-XDG_DATA_HOME$'
++assert_file_has_content "$app_dir/data/via-.local-share" '^via-\.local-share$'
++assert_file_has_content "$app_dir/data/via-data" '^via-data$'
++assert_file_has_content "$app_dir/data/via-var-data" '^via-var-data$'
++# The realpath of $HOME/.cache canonicalizes to (realpath of $HOME)/.cache.
++# Experimentally, this is the arrangement that is least likely to break
++# non-spec-compliant game engines
++run --command=sh --persist=. org.test.Hello -c 'readlink -f ~/.cache' > "$app_dir/cache/realpath"
++echo "$(readlink -f "$HOME")/.cache" > "$app_dir/cache/expected-realpath"
++diff -u "$app_dir/cache/expected-realpath" "$app_dir/cache/realpath" >&2
++# Similarly $XDG_CONFIG_HOME canonicalizes to $HOME/.config
++run --command=sh --persist=. org.test.Hello -c 'readlink -f ~/.config' > "$app_dir/config/realpath"
++echo "$(readlink -f "$HOME")/.config" > "$app_dir/config/expected-realpath"
++diff -u "$app_dir/config/expected-realpath" "$app_dir/config/realpath" >&2
++# Similarly $XDG_DATA_HOME canonicalizes to $HOME/.local/share
++run --command=sh --persist=. org.test.Hello -c 'readlink -f ~/.local/share' > "$app_dir/data/realpath"
++echo "$(readlink -f "$HOME")/.local/share" > "$app_dir/data/expected-realpath"
++diff -u "$app_dir/data/expected-realpath" "$app_dir/data/realpath" >&2
++rm -fr "$app_dir"
++
++ok "--persist=. behaving like com.valvesoftware.Steam"
++
+ mkdir "${TEST_DATA_DIR}/inaccessible"
+ echo FOO > ${TEST_DATA_DIR}/inaccessible/secret-file
+ rm -fr "$HOME/.var/app/org.test.Hello"
diff -Nru flatpak-1.16.6/debian/patches/GHSA-8688-9x26-hhxj/tests-Add-a-regression-test-for-GHSA-8688-9x26-hhxj.patch flatpak-1.16.6/debian/patches/GHSA-8688-9x26-hhxj/tests-Add-a-regression-test-for-GHSA-8688-9x26-hhxj.patch
--- flatpak-1.16.6/debian/patches/GHSA-8688-9x26-hhxj/tests-Add-a-regression-test-for-GHSA-8688-9x26-hhxj.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-8688-9x26-hhxj/tests-Add-a-regression-test-for-GHSA-8688-9x26-hhxj.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,78 @@
+From: Simon McVittie <smcv@collabora.com>
+Date: Sun, 9 Aug 2026 12:09:12 +0100
+Subject: tests: Add a regression test for GHSA-8688-9x26-hhxj
+
+The original reporter used various tricks to find a way for the sandboxed
+app to overwrite these locations with symlinks, but for the purposes
+of this test, I'm doing the setup outside the sandbox instead: probably
+not all of these potential exploit routes are actually possible, but we
+defend against all of them symmetrically.
+
+Signed-off-by: Simon McVittie <smcv@collabora.com>
+[backported to 1.18.x: adjust test numbering]
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-8688-9x26-hhxj
+Origin: upstream, 1.18.1
+---
+ tests/test-run.sh | 42 +++++++++++++++++++++++++++++++++++++++++-
+ 1 file changed, 41 insertions(+), 1 deletion(-)
+
+diff --git a/tests/test-run.sh b/tests/test-run.sh
+index 45abd56..2e5cc5c 100644
+--- a/tests/test-run.sh
++++ b/tests/test-run.sh
+@@ -24,7 +24,7 @@ set -euo pipefail
+ skip_without_bwrap
+ skip_revokefs_without_fuse
+ 
+-echo "1..28"
++echo "1..29"
+ 
+ # Use stable rather than master as the branch so we can test that the run
+ # command automatically finds the branch correctly
+@@ -664,6 +664,46 @@ rm -fr "$app_dir"
+ 
+ ok "--persist=. behaving like com.valvesoftware.Steam"
+ 
++app_dir="$HOME/.var/app/org.test.Hello"
++inaccessible="${TEST_DATA_DIR}/inaccessible"
++
++for path in cache data config cache/tmp; do
++    rm -fr "$app_dir" "$inaccessible"
++    mkdir -p "$app_dir" "$inaccessible"
++    case "$path" in
++        (cache/tmp)
++            mkdir -m700 "$app_dir/cache"
++            escape=../../../../../inaccessible
++            test "$app_dir/cache/$escape" -ef "$inaccessible"
++            ;;
++        (*)
++            escape=../../../../inaccessible
++            test "$app_dir/$escape" -ef "$inaccessible"
++            ;;
++    esac
++    for target in "$inaccessible" "$escape"; do
++        # There are various ways that an app could manipulate the contents
++        # of its per-app directory so that the next run of the same app would
++        # have a sandbox escape, but for the purposes of this test we cheat
++        # by setting up the same situation from outside the sandbox.
++        ln -fns "$target" "$app_dir/$path"
++        # The symlink target has been chosen to make these paths,
++        # outside the container, equivalent
++        test "$app_dir/$path" -ef "$inaccessible"
++        # The "|| :" inside the command ensures that if the app runs, then
++        # it succeeds, and then the assert_fail assertion fails.
++        # This is done because in the exploitable situations, we want Flatpak
++        # to refuse to run the app at all
++        assert_fail run --command=sh org.test.Hello -c "echo pwned > ~/$path/pwned || :"
++        assert_fail test -e "$inaccessible/pwned"
++        assert_fail run --command=sh org.test.Hello -c "echo pwned > /var/${path#*/}/pwned || :"
++        assert_fail test -e "$inaccessible/pwned"
++    done
++done
++
++ok "various attempts to escape app directory via symlinks (GHSA-8688-9x26-hhxj)"
++
++rm -fr "${TEST_DATA_DIR}/inaccessible"
+ mkdir "${TEST_DATA_DIR}/inaccessible"
+ echo FOO > ${TEST_DATA_DIR}/inaccessible/secret-file
+ rm -fr "$HOME/.var/app/org.test.Hello"
diff -Nru flatpak-1.16.6/debian/patches/GHSA-99wv-m8rp-g58x/common-Fix-return-value-and-typos-in-flatpak_switch_symli.patch flatpak-1.16.6/debian/patches/GHSA-99wv-m8rp-g58x/common-Fix-return-value-and-typos-in-flatpak_switch_symli.patch
--- flatpak-1.16.6/debian/patches/GHSA-99wv-m8rp-g58x/common-Fix-return-value-and-typos-in-flatpak_switch_symli.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-99wv-m8rp-g58x/common-Fix-return-value-and-typos-in-flatpak_switch_symli.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,50 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Thu, 18 Jun 2026 17:58:10 +0200
+Subject: common: Fix return value and typos in
+ flatpak_switch_symlink_and_remove
+
+One error path returned -1 instead of FALSE. Since gboolean is gint,
+-1 is truthy and the caller would skip error handling.
+
+Assisted-by: Claude:opus-4.6
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-99wv-m8rp-g58x
+Origin: upstream, 1.18.1
+---
+ common/flatpak-utils.c | 8 ++++----
+ 1 file changed, 4 insertions(+), 4 deletions(-)
+
+diff --git a/common/flatpak-utils.c b/common/flatpak-utils.c
+index 09102fd..e1f7712 100644
+--- a/common/flatpak-utils.c
++++ b/common/flatpak-utils.c
+@@ -939,10 +939,10 @@ flatpak_str_is_alphanumeric (const char *arg)
+ }
+ 
+ /* This atomically replaces a symlink with a new value, removing the
+- * existing symlink target, if it exstis and is different from
++ * existing symlink target, if it exists and is different from
+  * @target. This is atomic in the sense that we're guaranteed to
+  * remove any existing symlink target (once), independent of how many
+- * processes do the same operation in parallele. However, it is still
++ * processes do the same operation in parallel. However, it is still
+  * possible that we remove the old and then fail to create the new
+  * 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
+@@ -994,7 +994,7 @@ flatpak_switch_symlink_and_remove (const char *symlink_path,
+           if (old_target == NULL)
+             return FALSE;
+ 
+-          /* Don't remove old file if its the same as the new one */
++          /* Don't remove old file if it's the same as the new one */
+           if (strcmp (old_target, target) != 0)
+             {
+               if (flatpak_str_is_alphanumeric (old_target))
+@@ -1014,7 +1014,7 @@ flatpak_switch_symlink_and_remove (const char *symlink_path,
+         {
+           glnx_set_error_from_errno (error);
+           unlink (tmp_path);
+-          return -1;
++          return FALSE;
+         }
+       unlink (tmp_path);
+ 
diff -Nru flatpak-1.16.6/debian/patches/GHSA-99wv-m8rp-g58x/common-Use-fd-based-operations-in-flatpak_switch_symlink_.patch flatpak-1.16.6/debian/patches/GHSA-99wv-m8rp-g58x/common-Use-fd-based-operations-in-flatpak_switch_symlink_.patch
--- flatpak-1.16.6/debian/patches/GHSA-99wv-m8rp-g58x/common-Use-fd-based-operations-in-flatpak_switch_symlink_.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-99wv-m8rp-g58x/common-Use-fd-based-operations-in-flatpak_switch_symlink_.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,193 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Thu, 18 Jun 2026 17:59:23 +0200
+Subject: common: Use fd-based operations in flatpak_switch_symlink_and_remove
+
+Replace path-based syscalls with fd-relative equivalents so callers
+can pin the directory identity and prevent symlink substitution by a
+concurrent process.
+
+Assisted-by: Claude:opus-4.6
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-99wv-m8rp-g58x
+Origin: upstream, 1.18.1
+---
+ common/flatpak-run.c           | 10 ++++-
+ common/flatpak-utils-private.h |  3 +-
+ common/flatpak-utils.c         | 84 +++++++++++++++++++++++-------------------
+ 3 files changed, 56 insertions(+), 41 deletions(-)
+
+diff --git a/common/flatpak-run.c b/common/flatpak-run.c
+index 7d46dd6..cc9edca 100644
+--- a/common/flatpak-run.c
++++ b/common/flatpak-run.c
+@@ -2801,7 +2801,7 @@ regenerate_ld_cache (GPtrArray    *base_argv_array,
+     }
+   else
+     {
+-      g_autoptr(GFile) active = g_file_get_child (ld_so_dir, "active");
++      glnx_autofd int ld_so_dir_fd = -1;
+ 
+       /* For app-dirs we keep one checksum alive, by pointing the active symlink to it */
+ 
+@@ -2812,7 +2812,13 @@ regenerate_ld_cache (GPtrArray    *base_argv_array,
+           return -1;
+         }
+ 
+-      if (!flatpak_switch_symlink_and_remove (flatpak_file_get_path_cached (active),
++      if (!glnx_opendirat (AT_FDCWD,
++                           flatpak_file_get_path_cached (ld_so_dir),
++                           FALSE, &ld_so_dir_fd,
++                           error))
++        return -1;
++
++      if (!flatpak_switch_symlink_and_remove (ld_so_dir_fd, "active",
+                                               checksum, error))
+         return -1;
+     }
+diff --git a/common/flatpak-utils-private.h b/common/flatpak-utils-private.h
+index 7553839..b071e7e 100644
+--- a/common/flatpak-utils-private.h
++++ b/common/flatpak-utils-private.h
+@@ -154,7 +154,8 @@ flatpak_auto_lock_helper (GMutex *mutex)
+   return mutex;
+ }
+ 
+-gboolean flatpak_switch_symlink_and_remove (const char *symlink_path,
++gboolean flatpak_switch_symlink_and_remove (int         dir_fd,
++                                            const char *symlink_name,
+                                             const char *target,
+                                             GError    **error);
+ 
+diff --git a/common/flatpak-utils.c b/common/flatpak-utils.c
+index e1f7712..2a4b002 100644
+--- a/common/flatpak-utils.c
++++ b/common/flatpak-utils.c
+@@ -938,34 +938,38 @@ flatpak_str_is_alphanumeric (const char *arg)
+   return TRUE;
+ }
+ 
+-/* This atomically replaces a symlink with a new value, removing the
+- * existing symlink target, if it exists and is different from
+- * @target. This is atomic in the sense that we're guaranteed to
+- * remove any existing symlink target (once), independent of how many
+- * processes do the same operation in parallel. However, it is still
+- * possible that we remove the old and then fail to create the new
+- * 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.
++/* Atomically replace the @symlink_name symlink in @dir_fd with one
++ * pointing to @target, removing the previous target file (if it
++ * exists and differs from @target).
++ *
++ * This is atomic in the sense that we're guaranteed to remove any
++ * existing symlink target (once), independent of how many processes
++ * do the same operation in parallel. However, it is still possible
++ * that we remove the old and then fail to create the new 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.
++ *
++ * All filesystem operations go through @dir_fd so the caller can
++ * guarantee the directory identity even if the path to it is
++ * attacker-controlled.
++ *
++ * @target shall only contain the characters a-zA-Z0-9 so that the
++ * old target file to be removed is always in the same directory.
+  */
+ gboolean
+-flatpak_switch_symlink_and_remove (const char *symlink_path,
++flatpak_switch_symlink_and_remove (int         dir_fd,
++                                   const char *symlink_name,
+                                    const char *target,
+                                    GError    **error)
+ {
+-  g_autofree char *symlink_dir = g_path_get_dirname (symlink_path);
+-  int try;
+-
+-  for (try = 0; try < 100; try++)
++  for (size_t try = 0; try < 100; try++)
+     {
+-      g_autofree char *tmp_path = NULL;
+-      int fd;
++      g_autofree char *tmp_name = NULL;
++      glnx_autofd int fd = -1;
+ 
+       /* Try to atomically create the symlink */
+-      if (TEMP_FAILURE_RETRY (symlink (target, symlink_path)) == 0)
++      if (TEMP_FAILURE_RETRY (symlinkat (target, dir_fd, symlink_name)) == 0)
+         return TRUE;
+ 
+       if (errno != EEXIST)
+@@ -975,22 +979,32 @@ flatpak_switch_symlink_and_remove (const char *symlink_path,
+           return FALSE;
+         }
+ 
+-      /* The symlink existed, move it to a temporary name atomically, and remove target
+-         if that succeeded. */
+-      tmp_path = g_build_filename (symlink_dir, ".switched-symlink-XXXXXX", NULL);
++      /* The symlink existed, move it to a temporary name atomically,
++       * and remove its target if that succeeded.
++       * This generated filename doesn't need to be securely unique:
++       * we're only protecting against accidental collisions if two
++       * processes try to regenerate ld.so.cache at the same time. */
++      tmp_name = g_strdup_printf (".switched-symlink-%08X", g_random_int ());
+ 
+-      fd = g_mkstemp_full (tmp_path, O_RDWR, 0644);
++      fd = openat (dir_fd, tmp_name,
++                   O_CREAT | O_WRONLY | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0644);
+       if (fd == -1)
+         {
++          if (errno == EEXIST)
++            continue;
++
+           glnx_set_error_from_errno (error);
+           return FALSE;
+         }
+-      close (fd);
++      g_clear_fd (&fd, NULL);
+ 
+-      if (TEMP_FAILURE_RETRY (rename (symlink_path, tmp_path)) == 0)
++      if (TEMP_FAILURE_RETRY (renameat (dir_fd, symlink_name,
++                                        dir_fd, tmp_name)) == 0)
+         {
+           /* The move succeeded, now we can remove the old target */
+-          g_autofree char *old_target = flatpak_readlink (tmp_path, error);
++          g_autofree char *old_target = NULL;
++
++          old_target = glnx_readlinkat_malloc (dir_fd, tmp_name, NULL, error);
+           if (old_target == NULL)
+             return FALSE;
+ 
+@@ -998,25 +1012,19 @@ flatpak_switch_symlink_and_remove (const char *symlink_path,
+           if (strcmp (old_target, target) != 0)
+             {
+               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);
+-                }
++                unlinkat (dir_fd, old_target, 0);
+               else
+-                {
+-                  g_warning ("Refusing to delete old link target %s", old_target);
+-                }
++                g_warning ("Refusing to delete old link target %s", old_target);
+             }
+         }
+       else if (errno != ENOENT)
+         {
+           glnx_set_error_from_errno (error);
+-          unlink (tmp_path);
++          unlinkat (dir_fd, tmp_name, 0);
+           return FALSE;
+         }
+-      unlink (tmp_path);
++
++      unlinkat (dir_fd, tmp_name, 0);
+ 
+       /* An old target was removed, try again */
+     }
diff -Nru flatpak-1.16.6/debian/patches/GHSA-99wv-m8rp-g58x/run-Harden-regenerate_ld_cache-against-symlink-attacks.patch flatpak-1.16.6/debian/patches/GHSA-99wv-m8rp-g58x/run-Harden-regenerate_ld_cache-against-symlink-attacks.patch
--- flatpak-1.16.6/debian/patches/GHSA-99wv-m8rp-g58x/run-Harden-regenerate_ld_cache-against-symlink-attacks.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-99wv-m8rp-g58x/run-Harden-regenerate_ld_cache-against-symlink-attacks.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,173 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Thu, 18 Jun 2026 17:59:47 +0200
+Subject: run: Harden regenerate_ld_cache against symlink attacks
+
+A sandboxed app can replace ~/.var/app/$appid/.ld.so with a symlink,
+causing regenerate_ld_cache to write files at an arbitrary location.
+A concurrent app instance makes this a TOCTOU even after the initial
+directory verification.
+
+Replace all path-based operations with fd-based equivalents using
+ld_so_dir_fd obtained via glnx_chase_and_mkdirat, and pass the
+directory to bwrap via --bind-fd instead of --bind.
+
+Assisted-by: Claude:opus-4.6
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-99wv-m8rp-g58x
+Origin: upstream, 1.18.1
+---
+ common/flatpak-run.c | 88 ++++++++++++++++++++++++++++++++++++----------------
+ 1 file changed, 61 insertions(+), 27 deletions(-)
+
+diff --git a/common/flatpak-run.c b/common/flatpak-run.c
+index cc9edca..2793d1a 100644
+--- a/common/flatpak-run.c
++++ b/common/flatpak-run.c
+@@ -2691,33 +2691,72 @@ regenerate_ld_cache (GPtrArray    *base_argv_array,
+ {
+   g_autoptr(FlatpakBwrap) bwrap = NULL;
+   g_autoptr(GArray) combined_fd_array = NULL;
+-  g_autoptr(GFile) ld_so_cache = NULL;
+-  g_autoptr(GFile) ld_so_cache_tmp = NULL;
+   g_autofree char *sandbox_cache_path = NULL;
+   g_autofree char *tmp_basename = NULL;
+   g_auto(GStrv) minimal_envp = NULL;
+   g_autofree char *commandline = NULL;
+   int exit_status;
+   glnx_autofd int ld_so_fd = -1;
+-  g_autoptr(GFile) ld_so_dir = NULL;
++  glnx_autofd int ld_so_dir_fd = -1;
++  g_autofree char *ld_so_dir_path = NULL;
+ 
+   if (app_id_dir)
+-    ld_so_dir = g_file_get_child (app_id_dir, ".ld.so");
++    {
++      glnx_autofd int app_id_dir_fd = -1;
++
++      app_id_dir_fd = glnx_chase_and_mkdirat (AT_FDCWD,
++                                              flatpak_file_get_path_cached (app_id_dir),
++                                              GLNX_CHASE_DEFAULT,
++                                              0700, error);
++      if (app_id_dir_fd < 0)
++        {
++          g_prefix_error (error, "cannot open %s: ",
++                          flatpak_file_get_path_cached (app_id_dir));
++          return -1;
++        }
++
++      ld_so_dir_fd = glnx_chase_and_mkdirat (app_id_dir_fd, ".ld.so",
++                                             GLNX_CHASE_RESOLVE_NO_SYMLINKS,
++                                             0755, error);
++
++      ld_so_dir_path = g_build_filename (flatpak_file_get_path_cached (app_id_dir),
++                                         ".ld.so",
++                                         NULL);
++    }
+   else
+     {
+-      g_autoptr(GFile) base_dir = g_file_new_for_path (g_get_user_cache_dir ());
+-      ld_so_dir = g_file_resolve_relative_path (base_dir, "flatpak/ld.so");
++      glnx_autofd int cache_dir_fd = -1;
++
++      cache_dir_fd = glnx_chase_and_mkdirat (AT_FDCWD,
++                                             g_get_user_cache_dir (),
++                                             GLNX_CHASE_DEFAULT,
++                                             0700, error);
++      if (cache_dir_fd < 0)
++        {
++          g_prefix_error (error, "cannot open %s: ",
++                          g_get_user_cache_dir ());
++          return -1;
++        }
++
++      ld_so_dir_fd = glnx_chase_and_mkdirat (cache_dir_fd, "flatpak/ld.so",
++                                             GLNX_CHASE_RESOLVE_NO_SYMLINKS,
++                                             0755, error);
++
++      ld_so_dir_path = g_build_filename (g_get_user_cache_dir (),
++                                         "flatpak/ld.so",
++                                         NULL);
+     }
+ 
+-  ld_so_cache = g_file_get_child (ld_so_dir, checksum);
+-  ld_so_fd = open (flatpak_file_get_path_cached (ld_so_cache), O_RDONLY);
+-  if (ld_so_fd >= 0)
+-    return g_steal_fd (&ld_so_fd);
++  if (ld_so_dir_fd < 0)
++    {
++      g_prefix_error (error, "cannot open %s: ", ld_so_dir_path);
++      return -1;
++    }
+ 
+-  g_info ("Regenerating ld.so.cache %s", flatpak_file_get_path_cached (ld_so_cache));
++  if (glnx_openat_rdonly (ld_so_dir_fd, checksum, FALSE, &ld_so_fd, NULL))
++    return g_steal_fd (&ld_so_fd);
+ 
+-  if (!flatpak_mkdir_p (ld_so_dir, cancellable, error))
+-    return FALSE;
++  g_info ("Regenerating ld.so.cache %s/%s", ld_so_dir_path, checksum);
+ 
+   minimal_envp = flatpak_run_get_minimal_env (FALSE, FALSE);
+   bwrap = flatpak_bwrap_new (minimal_envp);
+@@ -2740,7 +2779,6 @@ regenerate_ld_cache (GPtrArray    *base_argv_array,
+   glnx_gen_temp_name (tmp_basename);
+ 
+   sandbox_cache_path = g_build_filename ("/run/ld-so-cache-dir", tmp_basename, NULL);
+-  ld_so_cache_tmp = g_file_get_child (ld_so_dir, tmp_basename);
+ 
+   flatpak_bwrap_add_args (bwrap,
+                           "--unshare-pid",
+@@ -2748,8 +2786,13 @@ regenerate_ld_cache (GPtrArray    *base_argv_array,
+                           "--unshare-net",
+                           "--proc", "/proc",
+                           "--dev", "/dev",
+-                          "--bind", flatpak_file_get_path_cached (ld_so_dir), "/run/ld-so-cache-dir",
+                           NULL);
++
++  if (!flatpak_bwrap_add_args_data_fd_dup (bwrap, "--bind-fd",
++                                           ld_so_dir_fd, "/run/ld-so-cache-dir",
++                                           error))
++    return -1;
++
+   flatpak_bwrap_sort_envp (bwrap);
+   flatpak_bwrap_envp_to_args (bwrap);
+ 
+@@ -2787,8 +2830,7 @@ regenerate_ld_cache (GPtrArray    *base_argv_array,
+       return -1;
+     }
+ 
+-  ld_so_fd = open (flatpak_file_get_path_cached (ld_so_cache_tmp), O_RDONLY);
+-  if (ld_so_fd < 0)
++  if (!glnx_openat_rdonly (ld_so_dir_fd, tmp_basename, FALSE, &ld_so_fd, NULL))
+     {
+       flatpak_fail_error (error, FLATPAK_ERROR_SETUP_FAILED, _("Can't open generated ld.so.cache"));
+       return -1;
+@@ -2797,27 +2839,19 @@ regenerate_ld_cache (GPtrArray    *base_argv_array,
+   if (app_id_dir == NULL)
+     {
+       /* For runs without an app id dir we always regenerate the ld.so.cache */
+-      unlink (flatpak_file_get_path_cached (ld_so_cache_tmp));
++      unlinkat (ld_so_dir_fd, tmp_basename, 0);
+     }
+   else
+     {
+-      glnx_autofd int ld_so_dir_fd = -1;
+-
+       /* For app-dirs we keep one checksum alive, by pointing the active symlink to it */
+ 
+       /* Rename to known name, possibly overwriting existing ref if race */
+-      if (rename (flatpak_file_get_path_cached (ld_so_cache_tmp), flatpak_file_get_path_cached (ld_so_cache)) == -1)
++      if (renameat (ld_so_dir_fd, tmp_basename, ld_so_dir_fd, checksum) == -1)
+         {
+           glnx_set_error_from_errno (error);
+           return -1;
+         }
+ 
+-      if (!glnx_opendirat (AT_FDCWD,
+-                           flatpak_file_get_path_cached (ld_so_dir),
+-                           FALSE, &ld_so_dir_fd,
+-                           error))
+-        return -1;
+-
+       if (!flatpak_switch_symlink_and_remove (ld_so_dir_fd, "active",
+                                               checksum, error))
+         return -1;
diff -Nru flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/build-init-Use-fd-relative-operations-to-prevent-path-tra.patch flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/build-init-Use-fd-relative-operations-to-prevent-path-tra.patch
--- flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/build-init-Use-fd-relative-operations-to-prevent-path-tra.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/build-init-Use-fd-relative-operations-to-prevent-path-tra.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,341 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Mon, 29 Jun 2026 17:34:02 +0200
+Subject: build-init: Use fd-relative operations to prevent path traversal
+
+ensure_extensions() used g_file_resolve_relative_path() with
+ext->directory, which comes from runtime/SDK metadata and can contain
+".." components. This allowed a malicious extension to write outside
+the build directory.
+
+Replace the GFile-based path resolution with glnx_chase_and_mkdirat()
+using GLNX_CHASE_RESOLVE_BENEATH | GLNX_CHASE_RESOLVE_NO_SYMLINKS,
+which rejects any path that escapes the directory fd.
+
+Convert the rest of flatpak_builtin_build_init to fd-relative
+operations (mkdirat, symlinkat, glnx_opendirat, glnx_shutil_rm_rf_at,
+glnx_file_replace_contents_at, flatpak_cp_a_at) anchored to a
+base_dfd opened from the build directory.
+
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-8qxj-x646-phcm
+Origin: upstream, 1.18.1
+---
+ app/flatpak-builtins-build-init.c | 218 +++++++++++++++++++++++---------------
+ 1 file changed, 134 insertions(+), 84 deletions(-)
+
+diff --git a/app/flatpak-builtins-build-init.c b/app/flatpak-builtins-build-init.c
+index bcfa3be..ef6611d 100644
+--- a/app/flatpak-builtins-build-init.c
++++ b/app/flatpak-builtins-build-init.c
+@@ -72,7 +72,7 @@ static GOptionEntry options[] = {
+ 
+ static gboolean
+ ensure_extensions (FlatpakDeploy *src_deploy, const char *default_arch, const char *default_branch,
+-                   char *src_extensions[], GFile *top_dir, GCancellable *cancellable, GError **error)
++                   char *src_extensions[], int top_dfd, GCancellable *cancellable, GError **error)
+ {
+   g_autoptr(GKeyFile) metakey = flatpak_deploy_get_metadata (src_deploy);
+   GList *extensions = NULL, *l;
+@@ -118,22 +118,47 @@ ensure_extensions (FlatpakDeploy *src_deploy, const char *default_arch, const ch
+                     return flatpak_fail (error, _("Requested extension %s/%s/%s is only partially installed"), ext->installed_id, default_arch, default_branch);
+                 }
+ 
+-              if (top_dir)
++              if (top_dfd >= 0)
+                 {
+-                  g_autoptr(GFile) target = g_file_resolve_relative_path (top_dir, ext->directory);
+-                  g_autoptr(GFile) target_parent = g_file_get_parent (target);
+-                  g_autoptr(GFile) ext_deploy_files = g_file_new_for_path (ext->files_path);
++                  glnx_autofd int ext_src_dfd = -1;
++                  glnx_autofd int parent_dfd = -1;
++                  g_autofree char *dir_parent = g_path_get_dirname (ext->directory);
++                  const char *dir_basename = glnx_basename (ext->directory);
++
++                  if (strcmp (dir_parent, ".") != 0)
++                    {
++                      glnx_autofd int parent_opath_fd = -1;
++
++                      parent_opath_fd = glnx_chase_and_mkdirat (top_dfd, dir_parent,
++                                                                GLNX_CHASE_RESOLVE_BENEATH |
++                                                                GLNX_CHASE_RESOLVE_NO_SYMLINKS,
++                                                                0755, error);
++                      if (parent_opath_fd < 0)
++                        return FALSE;
++
++                      parent_dfd = glnx_fd_reopen (parent_opath_fd,
++                                                   O_RDONLY | O_DIRECTORY | O_CLOEXEC,
++                                                   error);
++                      if (parent_dfd < 0)
++                        return FALSE;
++                    }
++                  else
++                    {
++                      parent_dfd = dup (top_dfd);
++                      if (parent_dfd < 0)
++                        return glnx_throw_errno_prefix (error, "dup");
++                    }
+ 
+-                  if (!flatpak_mkdir_p (target_parent, cancellable, error))
++                  /* An extension overrides whatever is there before, so we clean up first */
++                  if (!glnx_shutil_rm_rf_at (parent_dfd, dir_basename, cancellable, error))
+                     return FALSE;
+ 
+-                  /* An extension overrides whatever is there before, so we clean up first */
+-                  if (!flatpak_rm_rf (target, cancellable, error))
++                  if (!glnx_opendirat (AT_FDCWD, ext->files_path, FALSE, &ext_src_dfd, error))
+                     return FALSE;
+ 
+-                  if (!flatpak_cp_a (ext_deploy_files, target,
+-                                     FLATPAK_CP_FLAGS_NO_CHOWN,
+-                                     cancellable, error))
++                  if (!flatpak_cp_a_at (ext_src_dfd, parent_dfd, dir_basename,
++                                        FLATPAK_CP_FLAGS_NO_CHOWN,
++                                        cancellable, error))
+                     return FALSE;
+                 }
+ 
+@@ -170,15 +195,8 @@ flatpak_builtin_build_init (int argc, char **argv, GCancellable *cancellable, GE
+   g_autoptr(GOptionContext) context = NULL;
+   g_autoptr(GFile) var_deploy_files = NULL;
+   g_autoptr(GFile) base = NULL;
+-  g_autoptr(GFile) files_dir = NULL;
+-  g_autoptr(GFile) usr_dir = NULL;
+-  g_autoptr(GFile) var_dir = NULL;
+-  g_autoptr(GFile) var_tmp_dir = NULL;
+-  g_autoptr(GFile) var_run_dir = NULL;
+-  g_autoptr(GFile) metadata_file = NULL;
+-  g_autoptr(GFile) gitignore_file = NULL;
++  glnx_autofd int base_dfd = -1;
+   g_autoptr(GString) metadata_contents = NULL;
+-  g_autoptr(GError) my_error = NULL;
+   g_autoptr(FlatpakDeploy) runtime_deploy = NULL;
+   g_autoptr(FlatpakDeploy) sdk_deploy = NULL;
+   const char *app_id;
+@@ -237,8 +255,12 @@ flatpak_builtin_build_init (int argc, char **argv, GCancellable *cancellable, GE
+   else
+     is_app = TRUE;
+ 
+-  if (!flatpak_is_valid_name (app_id, -1, &my_error))
+-    return flatpak_fail (error, _("'%s' is not a valid application name: %s"), app_id, my_error->message);
++  {
++    g_autoptr(GError) my_error = NULL;
++
++    if (!flatpak_is_valid_name (app_id, -1, &my_error))
++      return flatpak_fail (error, _("'%s' is not a valid application name: %s"), app_id, my_error->message);
++  }
+ 
+ 
+   kinds = FLATPAK_KINDS_RUNTIME;
+@@ -283,16 +305,18 @@ flatpak_builtin_build_init (int argc, char **argv, GCancellable *cancellable, GE
+   if (!flatpak_mkdir_p (base, cancellable, error))
+     return FALSE;
+ 
+-  files_dir = g_file_get_child (base, "files");
+-  var_dir = g_file_get_child (base, "var");
+-  var_tmp_dir = g_file_get_child (var_dir, "tmp");
+-  var_run_dir = g_file_get_child (var_dir, "run");
+-  metadata_file = g_file_get_child (base, "metadata");
+-  gitignore_file = g_file_get_child (base, ".gitignore");
++  if (!glnx_opendirat (AT_FDCWD, flatpak_file_get_path_cached (base), FALSE, &base_dfd, error))
++    return FALSE;
+ 
+-  if (!opt_update &&
+-      g_file_query_exists (files_dir, cancellable))
+-    return flatpak_fail (error, _("Build directory %s already initialized"), directory);
++  {
++    struct stat stbuf;
++
++    if (!glnx_fstatat_allow_noent (base_dfd, "files", &stbuf, AT_SYMLINK_NOFOLLOW, error))
++      return FALSE;
++
++    if (!opt_update && errno == 0)
++      return flatpak_fail (error, _("Build directory %s already initialized"), directory);
++  }
+ 
+   sdk_deploy = flatpak_dir_load_deployed (sdk_dir, sdk_ref, NULL, cancellable, error);
+   if (sdk_deploy == NULL)
+@@ -300,36 +324,41 @@ flatpak_builtin_build_init (int argc, char **argv, GCancellable *cancellable, GE
+ 
+   if (opt_writable_sdk || is_runtime)
+     {
++      const char *usr_dir_name = opt_sdk_dir ? opt_sdk_dir : "usr";
+       g_autoptr(GFile) sdk_deploy_files = NULL;
++      glnx_autofd int sdk_dfd = -1;
+ 
+-      if (opt_sdk_dir)
+-        usr_dir = g_file_get_child (base, opt_sdk_dir);
+-      else
+-        usr_dir = g_file_get_child (base, "usr");
+-
+-      if (!flatpak_rm_rf (usr_dir, NULL, &my_error))
+-        {
+-          if (!g_error_matches (my_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+-            {
+-              g_propagate_error (error, g_steal_pointer (&my_error));
+-              return FALSE;
+-            }
+-
+-          g_clear_error (&my_error);
+-        }
++      if (!glnx_shutil_rm_rf_at (base_dfd, usr_dir_name, cancellable, error))
++        return FALSE;
+ 
+       sdk_deploy_files = flatpak_deploy_get_files (sdk_deploy);
+-      if (!flatpak_cp_a (sdk_deploy_files, usr_dir, FLATPAK_CP_FLAGS_NO_CHOWN, cancellable, error))
++      if (!glnx_opendirat (AT_FDCWD, flatpak_file_get_path_cached (sdk_deploy_files),
++                           FALSE, &sdk_dfd, error))
++        return FALSE;
++      if (!flatpak_cp_a_at (sdk_dfd, base_dfd, usr_dir_name,
++                            FLATPAK_CP_FLAGS_NO_CHOWN, cancellable, error))
+         return FALSE;
+     }
+ 
+   sdk_branch = flatpak_decomposed_dup_branch (sdk_ref);
+   sdk_arch = flatpak_decomposed_dup_arch (sdk_ref);
+ 
+-  if (opt_sdk_extensions &&
+-      !ensure_extensions (sdk_deploy, sdk_arch, sdk_branch,
+-                          opt_sdk_extensions, usr_dir, cancellable, error))
+-    return FALSE;
++  if (opt_sdk_extensions)
++    {
++      glnx_autofd int usr_dfd = -1;
++
++      if (opt_writable_sdk || is_runtime)
++        {
++          const char *usr_dir_name = opt_sdk_dir ? opt_sdk_dir : "usr";
++
++          if (!glnx_opendirat (base_dfd, usr_dir_name, FALSE, &usr_dfd, error))
++            return FALSE;
++        }
++
++      if (!ensure_extensions (sdk_deploy, sdk_arch, sdk_branch,
++                              opt_sdk_extensions, usr_dfd, cancellable, error))
++        return FALSE;
++    }
+ 
+   if (opt_var)
+     {
+@@ -345,14 +374,15 @@ flatpak_builtin_build_init (int argc, char **argv, GCancellable *cancellable, GE
+   if (opt_update)
+     return TRUE;
+ 
+-  if (!g_file_make_directory (files_dir, cancellable, error))
+-    return FALSE;
++  if (TEMP_FAILURE_RETRY (mkdirat (base_dfd, "files", 0777)) != 0)
++    return glnx_throw_errno_prefix (error, "mkdirat(files)");
+ 
+   if (opt_base)
+     {
+       const char *base_branch;
+       g_autoptr(GFile) base_deploy_files = NULL;
+       g_autoptr(FlatpakDeploy) base_deploy = NULL;
++      glnx_autofd int base_src_dfd = -1;
+ 
+       base_branch = opt_base_version ? opt_base_version : "master";
+       base_ref = flatpak_build_app_ref (opt_base, base_branch, opt_arch);
+@@ -361,35 +391,55 @@ flatpak_builtin_build_init (int argc, char **argv, GCancellable *cancellable, GE
+         return FALSE;
+ 
+       base_deploy_files = flatpak_deploy_get_files (base_deploy);
+-      if (!flatpak_cp_a (base_deploy_files, files_dir,
+-                         FLATPAK_CP_FLAGS_MERGE | FLATPAK_CP_FLAGS_NO_CHOWN,
+-                         cancellable, error))
++      if (!glnx_opendirat (AT_FDCWD, flatpak_file_get_path_cached (base_deploy_files),
++                           FALSE, &base_src_dfd, error))
+         return FALSE;
+-
+-
+-      if (opt_base_extensions &&
+-          !ensure_extensions (base_deploy, opt_arch, base_branch,
+-                              opt_base_extensions, files_dir, cancellable, error))
++      if (!flatpak_cp_a_at (base_src_dfd, base_dfd, "files",
++                            FLATPAK_CP_FLAGS_MERGE | FLATPAK_CP_FLAGS_NO_CHOWN,
++                            cancellable, error))
+         return FALSE;
+-    }
+ 
+-  if (var_deploy_files)
+-    {
+-      if (!flatpak_cp_a (var_deploy_files, var_dir, FLATPAK_CP_FLAGS_NONE, cancellable, error))
+-        return FALSE;
+-    }
+-  else
+-    {
+-      if (!g_file_make_directory (var_dir, cancellable, error))
+-        return FALSE;
++      if (opt_base_extensions)
++        {
++          glnx_autofd int files_dfd = -1;
++          if (!glnx_opendirat (base_dfd, "files", FALSE, &files_dfd, error))
++            return FALSE;
++          if (!ensure_extensions (base_deploy, opt_arch, base_branch,
++                                  opt_base_extensions, files_dfd, cancellable, error))
++            return FALSE;
++        }
+     }
+ 
+-  if (!flatpak_mkdir_p (var_tmp_dir, cancellable, error))
+-    return FALSE;
++  {
++    glnx_autofd int var_dfd = -1;
+ 
+-  if (!g_file_query_exists (var_run_dir, cancellable) &&
+-      !g_file_make_symbolic_link (var_run_dir, "/run", cancellable, error))
+-    return FALSE;
++    if (var_deploy_files)
++      {
++        glnx_autofd int var_src_dfd = -1;
++
++        if (!glnx_opendirat (AT_FDCWD, flatpak_file_get_path_cached (var_deploy_files),
++                             FALSE, &var_src_dfd, error))
++          return FALSE;
++
++        if (!flatpak_cp_a_at (var_src_dfd, base_dfd, "var",
++                              FLATPAK_CP_FLAGS_NONE, cancellable, error))
++          return FALSE;
++      }
++    else
++      {
++        if (TEMP_FAILURE_RETRY (mkdirat (base_dfd, "var", 0777)) != 0)
++          return glnx_throw_errno_prefix (error, "mkdirat(var)");
++      }
++
++    if (!glnx_opendirat (base_dfd, "var", FALSE, &var_dfd, error))
++      return FALSE;
++
++    if (!glnx_shutil_mkdir_p_at (var_dfd, "tmp", 0777, cancellable, error))
++      return FALSE;
++
++    if (TEMP_FAILURE_RETRY (symlinkat ("/run", var_dfd, "run")) != 0 && errno != EEXIST)
++      return glnx_throw_errno_prefix (error, "symlinkat(var/run)");
++  }
+ 
+ 
+   metadata_contents = g_string_new ("");
+@@ -468,17 +518,17 @@ flatpak_builtin_build_init (int argc, char **argv, GCancellable *cancellable, GE
+ 
+   keyfile_data = g_key_file_to_data (keyfile, &keyfile_data_len, NULL);
+ 
+-  if (!g_file_replace_contents (metadata_file,
+-                                keyfile_data, keyfile_data_len, NULL, FALSE,
+-                                G_FILE_CREATE_REPLACE_DESTINATION,
+-                                NULL, cancellable, error))
++  if (!glnx_file_replace_contents_at (base_dfd, "metadata",
++                                      (const guint8 *) keyfile_data, keyfile_data_len,
++                                      GLNX_FILE_REPLACE_NODATASYNC,
++                                      cancellable, error))
+     return FALSE;
+ 
+-  if (!g_file_replace_contents (gitignore_file,
+-                                GIT_IGNORE_FILE, sizeof (GIT_IGNORE_FILE),
+-                                NULL, FALSE,
+-                                G_FILE_CREATE_REPLACE_DESTINATION,
+-                                NULL, cancellable, error))
++  if (!glnx_file_replace_contents_at (base_dfd, ".gitignore",
++                                      (const guint8 *) GIT_IGNORE_FILE,
++                                      sizeof (GIT_IGNORE_FILE),
++                                      GLNX_FILE_REPLACE_NODATASYNC,
++                                      cancellable, error))
+     return FALSE;
+ 
+   return TRUE;
diff -Nru flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/dir-Use-chaseat-in-apply_extra_data-to-prevent-path-trave.patch flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/dir-Use-chaseat-in-apply_extra_data-to-prevent-path-trave.patch
--- flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/dir-Use-chaseat-in-apply_extra_data-to-prevent-path-trave.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/dir-Use-chaseat-in-apply_extra_data-to-prevent-path-trave.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,255 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Fri, 7 Aug 2026 23:18:59 +0200
+Subject: dir: Use chaseat in apply_extra_data to prevent path traversal
+
+apply_extra_data() used GFile path-based operations that follow
+symlinks: --ro-bind/--bind with resolved paths, chmod on the extra
+directory path, and flatpak_cp_a for the extra/export merge.
+
+These are not independently exploitable: apply_extra_data only runs
+after extract_extra_data has already created files/extra as a real
+directory, so there is no symlink left to follow. The apply_extra
+script itself runs in a restricted sandbox with dropped caps and no
+/proc. The extra/export merge only adds to the app's own export
+directory, which is already app-controlled and whose desktop file
+Exec keys are rewritten to flatpak run.
+
+Replace with fd-relative operations as defense in depth:
+glnx_chaseat with GLNX_CHASE_RESOLVE_NO_SYMLINKS and
+GLNX_CHASE_RESOLVE_BENEATH for directory traversal,
+--ro-bind-fd/--bind-fd for bwrap mounts, fchmod on the directory fd,
+and flatpak_cp_a_at for the export merge.
+
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-fqx6-vh4p-42cg
+[smcv: Use glnx_fd_reopen() to reopen O_PATH fd as readable]
+Co-authored-by: Simon McVittie <smcv@collabora.com>
+Origin: upstream, 1.18.1
+---
+ common/flatpak-dir.c | 142 ++++++++++++++++++++++++++++++++++-----------------
+ 1 file changed, 94 insertions(+), 48 deletions(-)
+
+diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c
+index da5d887..5aa29ac 100644
+--- a/common/flatpak-dir.c
++++ b/common/flatpak-dir.c
+@@ -8352,11 +8352,10 @@ extract_extra_data (FlatpakDir   *self,
+ 
+ static gboolean
+ apply_extra_data (FlatpakDir   *self,
+-                  GFile        *checkoutdir,
++                  int           checkoutdir_dfd,
+                   GCancellable *cancellable,
+                   GError      **error)
+ {
+-  g_autoptr(GFile) metadata = NULL;
+   g_autofree char *metadata_contents = NULL;
+   gsize metadata_size;
+   g_autoptr(GKeyFile) metakey = NULL;
+@@ -8365,27 +8364,60 @@ apply_extra_data (FlatpakDir   *self,
+   g_autoptr(FlatpakDecomposed) runtime_ref = NULL;
+   g_autoptr(FlatpakDeploy) runtime_deploy = NULL;
+   g_autoptr(FlatpakBwrap) bwrap = NULL;
+-  g_autoptr(GFile) app_files = NULL;
+-  g_autoptr(GFile) apply_extra_file = NULL;
+-  g_autoptr(GFile) app_export_file = NULL;
+-  g_autoptr(GFile) extra_export_file = NULL;
+-  g_autoptr(GFile) extra_files = NULL;
+   g_autoptr(GFile) runtime_files = NULL;
+   g_autoptr(FlatpakContext) app_context = NULL;
+   g_auto(GStrv) minimal_envp = NULL;
+   g_autofree char *runtime_arch = NULL;
++  glnx_autofd int app_files_dfd = -1;
++  glnx_autofd int metadata_path_fd = -1;
++  glnx_autofd int metadata_read_fd = -1;
++  glnx_autofd int extra_dfd = -1;
++  glnx_autofd int usr_fd = -1;
+   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))
+-    return TRUE;
++  app_files_dfd = glnx_chaseat (checkoutdir_dfd, "files",
++                                GLNX_CHASE_RESOLVE_NO_SYMLINKS |
++                                GLNX_CHASE_MUST_BE_DIRECTORY,
++                                error);
++  if (app_files_dfd < 0)
++    return FALSE;
+ 
+-  metadata = g_file_get_child (checkoutdir, "metadata");
++  {
++    glnx_autofd int apply_extra_fd = -1;
+ 
+-  if (!g_file_load_contents (metadata, cancellable, &metadata_contents, &metadata_size, NULL, error))
++    apply_extra_fd = glnx_chaseat (app_files_dfd, "bin/apply_extra",
++                                   GLNX_CHASE_RESOLVE_BENEATH |
++                                   GLNX_CHASE_MUST_BE_REGULAR,
++                                   &local_error);
++    if (apply_extra_fd < 0)
++      {
++        if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
++          {
++            g_clear_error (&local_error);
++            return TRUE;
++          }
++        g_propagate_error (error, g_steal_pointer (&local_error));
++        return FALSE;
++      }
++  }
++
++  metadata_path_fd = glnx_chaseat (checkoutdir_dfd, "metadata",
++                                   GLNX_CHASE_RESOLVE_NO_SYMLINKS |
++                                   GLNX_CHASE_MUST_BE_REGULAR,
++                                   error);
++  if (metadata_path_fd < 0)
++    return FALSE;
++
++  metadata_read_fd = glnx_fd_reopen (metadata_path_fd, O_RDONLY, error);
++  if (metadata_read_fd < 0)
++    return FALSE;
++
++  metadata_contents = glnx_fd_readall_utf8 (metadata_read_fd, &metadata_size,
++                                            cancellable, error);
++  if (metadata_contents == NULL)
+     return FALSE;
+ 
+   metakey = g_key_file_new ();
+@@ -8438,24 +8470,36 @@ apply_extra_data (FlatpakDir   *self,
+       runtime_files = flatpak_deploy_get_files (runtime_deploy);
+     }
+ 
+-  app_files = g_file_get_child (checkoutdir, "files");
+-  app_export_file = g_file_get_child (checkoutdir, "export");
+-  extra_files = g_file_get_child (app_files, "extra");
+-  extra_export_file = g_file_get_child (extra_files, "export");
++  extra_dfd = glnx_chaseat (app_files_dfd, "extra",
++                            GLNX_CHASE_RESOLVE_BENEATH |
++                            GLNX_CHASE_MUST_BE_DIRECTORY,
++                            error);
++  if (extra_dfd < 0)
++    return FALSE;
+ 
+   minimal_envp = flatpak_run_get_minimal_env (FALSE, FALSE);
+   bwrap = flatpak_bwrap_new (minimal_envp);
+   flatpak_bwrap_add_args (bwrap, flatpak_get_bwrap (), NULL);
+ 
+   if (runtime_files)
+-    flatpak_bwrap_add_args (bwrap,
+-                            "--ro-bind", flatpak_file_get_path_cached (runtime_files), "/usr",
+-                            "--lock-file", "/usr/.ref",
+-                            NULL);
++    {
++      usr_fd = glnx_chaseat (AT_FDCWD, flatpak_file_get_path_cached (runtime_files),
++                             GLNX_CHASE_MUST_BE_DIRECTORY,
++                             error);
++      if (usr_fd < 0)
++        return FALSE;
++
++      if (!flatpak_bwrap_add_args_data_fd_dup (bwrap, "--ro-bind-fd", usr_fd, "/usr", error))
++        return FALSE;
++      flatpak_bwrap_add_args (bwrap, "--lock-file", "/usr/.ref", NULL);
++    }
++
++  if (!flatpak_bwrap_add_args_data_fd_dup (bwrap, "--ro-bind-fd", app_files_dfd, "/app", error))
++    return FALSE;
++  if (!flatpak_bwrap_add_args_data_fd_dup (bwrap, "--bind-fd", extra_dfd, "/app/extra", error))
++    return FALSE;
+ 
+   flatpak_bwrap_add_args (bwrap,
+-                          "--ro-bind", flatpak_file_get_path_cached (app_files), "/app",
+-                          "--bind", flatpak_file_get_path_cached (extra_files), "/app/extra",
+                           "--chdir", "/app/extra",
+                           /* We run as root in the system-helper case, so drop all caps */
+                           "--cap-drop", "ALL",
+@@ -8477,16 +8521,6 @@ apply_extra_data (FlatpakDir   *self,
+    * 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;
+@@ -8513,11 +8547,19 @@ apply_extra_data (FlatpakDir   *self,
+    * this runs as root in the system helper case. We canonicalize the permissions at the
+    * end, but to avoid non-canonical permissions leaking out before then we make the
+    * toplevel dir only accessible to the user */
+-  if (chmod (flatpak_file_get_path_cached (extra_files), 0700) != 0)
+-    {
+-      glnx_set_error_from_errno (error);
++  {
++    glnx_autofd int extra_real_dfd =
++      glnx_fd_reopen (extra_dfd, O_RDONLY | O_DIRECTORY, error);
++
++    if (extra_real_dfd < 0)
+       return FALSE;
+-    }
++
++    if (fchmod (extra_real_dfd, 0700) != 0)
++      {
++        glnx_set_error_from_errno (error);
++        return FALSE;
++      }
++  }
+ 
+   if (!g_spawn_sync (NULL,
+                      (char **) bwrap->argv->pdata,
+@@ -8529,7 +8571,7 @@ apply_extra_data (FlatpakDir   *self,
+                      error))
+     return FALSE;
+ 
+-  if (!flatpak_canonicalize_permissions (AT_FDCWD, flatpak_file_get_path_cached (extra_files),
++  if (!flatpak_canonicalize_permissions (app_files_dfd, "extra",
+                                          getuid () == 0 ? 0 : -1,
+                                          getuid () == 0 ? 0 : -1,
+                                          error))
+@@ -8542,16 +8584,20 @@ apply_extra_data (FlatpakDir   *self,
+       return FALSE;
+     }
+ 
+-  if (g_file_query_exists (extra_export_file, cancellable))
+-    {
+-      if (!flatpak_mkdir_p (app_export_file, cancellable, error))
+-        return FALSE;
+-      if (!flatpak_cp_a (extra_export_file,
+-                         app_export_file,
+-                         FLATPAK_CP_FLAGS_MERGE,
+-                         cancellable, error))
+-        return FALSE;
+-    }
++  {
++    glnx_autofd int extra_export_dfd = -1;
++
++    extra_export_dfd = glnx_chaseat (extra_dfd, "export",
++                                     GLNX_CHASE_RESOLVE_BENEATH |
++                                     GLNX_CHASE_MUST_BE_DIRECTORY,
++                                     NULL);
++    if (extra_export_dfd >= 0)
++      {
++        if (!flatpak_cp_a_at (extra_export_dfd, checkoutdir_dfd, "export",
++                              FLATPAK_CP_FLAGS_MERGE, cancellable, error))
++          return FALSE;
++      }
++  }
+ 
+   return TRUE;
+ }
+@@ -8942,7 +8988,7 @@ flatpak_dir_deploy (FlatpakDir          *self,
+ 
+   if (created_extra_data)
+     {
+-      if (!apply_extra_data (self, checkoutdir, cancellable, error))
++      if (!apply_extra_data (self, checkoutdir_dfd, cancellable, error))
+         {
+           g_prefix_error (error, _("While trying to apply extra data: "));
+           return FALSE;
diff -Nru flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/dir-Use-chaseat-in-extract_extra_data-to-prevent-path-tra.patch flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/dir-Use-chaseat-in-extract_extra_data-to-prevent-path-tra.patch
--- flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/dir-Use-chaseat-in-extract_extra_data-to-prevent-path-tra.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/dir-Use-chaseat-in-extract_extra_data-to-prevent-path-tra.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,159 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Fri, 7 Aug 2026 23:31:49 +0200
+Subject: dir: Use chaseat in extract_extra_data to prevent path traversal
+
+extract_extra_data() had two vulnerabilities:
+
+1. It resolved "files/extra" using g_file_resolve_relative_path()
+   which follows symlinks. A crafted OSTree commit with "files" as a
+   symlink causes extra-data blobs to be written at the symlink target.
+   On system installs this runs as root via the system helper, which
+   validates signatures and checksums but not tree structure.
+
+2. It used g_file_get_child(extradir, name) where name comes from
+   xa.extra-data-sources in the commit metadata. Names containing ".."
+   escape the extra/ directory. This is exploitable through the normal
+   build flow: flatpak build-export rejects "/" but not "..".
+
+Replace GFile path operations with fd-relative operations: open
+"files" with glnx_chaseat using GLNX_CHASE_RESOLVE_NO_SYMLINKS,
+create "extra" with glnx_chase_and_mkdirat using
+GLNX_CHASE_RESOLVE_BENEATH, validate extra-data names against ".",
+"..", and "/", and write with glnx_file_replace_contents_at anchored
+to the extra directory fd.
+
+[smcv: Open checkoutdir_dfd before trying to open its files subdir]
+Co-authored-by: Simon McVittie <smcv@collabora.com>
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-fqx6-vh4p-42cg
+Origin: upstream, 1.18.1
+---
+ common/flatpak-dir.c | 47 +++++++++++++++++++++++++++++++----------------
+ 1 file changed, 31 insertions(+), 16 deletions(-)
+
+diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c
+index 7273cda..da5d887 100644
+--- a/common/flatpak-dir.c
++++ b/common/flatpak-dir.c
+@@ -8208,7 +8208,7 @@ out:
+ static gboolean
+ extract_extra_data (FlatpakDir   *self,
+                     const char   *checksum,
+-                    GFile        *extradir,
++                    int           app_files_dfd,
+                     gboolean     *created_extra_data,
+                     GCancellable *cancellable,
+                     GError      **error)
+@@ -8217,6 +8217,7 @@ extract_extra_data (FlatpakDir   *self,
+   g_autoptr(GVariant) extra_data = NULL;
+   g_autoptr(GVariant) extra_data_sources = NULL;
+   g_autoptr(GError) local_error = NULL;
++  glnx_autofd int extra_dfd = -1;
+   gsize i, n_extra_data = 0;
+   gsize n_extra_data_sources;
+ 
+@@ -8239,7 +8240,7 @@ extract_extra_data (FlatpakDir   *self,
+   if (n_extra_data_sources == 0)
+     return TRUE;
+ 
+-  g_info ("extracting extra data to %s", flatpak_file_get_path_cached (extradir));
++  g_info ("extracting extra data");
+ 
+   if (!ostree_repo_read_commit_detached_metadata (self->repo, checksum, &detached_metadata,
+                                                   cancellable, error))
+@@ -8260,7 +8261,10 @@ extract_extra_data (FlatpakDir   *self,
+   if (n_extra_data < n_extra_data_sources)
+     return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Extra data missing in detached metadata"));
+ 
+-  if (!flatpak_mkdir_p (extradir, cancellable, error))
++  extra_dfd = glnx_chase_and_mkdirat (app_files_dfd, "extra",
++                                      GLNX_CHASE_RESOLVE_BENEATH,
++                                      0777, error);
++  if (extra_dfd < 0)
+     {
+       g_prefix_error (error, _("While creating extradir: "));
+       return FALSE;
+@@ -8295,7 +8299,6 @@ extract_extra_data (FlatpakDir   *self,
+       for (j = 0; j < n_extra_data; j++)
+         {
+           g_autoptr(GVariant) content = NULL;
+-          g_autoptr(GFile) dest = NULL;
+           g_autofree char *sha256 = NULL;
+           const char *extra_data_name = NULL;
+           const guchar *data;
+@@ -8308,6 +8311,13 @@ extract_extra_data (FlatpakDir   *self,
+           if (strcmp (extra_data_source_name, extra_data_name) != 0)
+             continue;
+ 
++          if (extra_data_name[0] == '\0' ||
++              strcmp (extra_data_name, ".") == 0 ||
++              strcmp (extra_data_name, "..") == 0 ||
++              strchr (extra_data_name, '/') != NULL)
++            return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA,
++                                       _("Invalid extra data filename '%s'"), extra_data_name);
++
+           data = g_variant_get_data (content);
+           len = g_variant_get_size (content);
+ 
+@@ -8318,12 +8328,10 @@ extract_extra_data (FlatpakDir   *self,
+           if (strcmp (sha256, extra_data_sha256) != 0)
+             return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Invalid checksum for extra data"));
+ 
+-          dest = g_file_get_child (extradir, extra_data_name);
+-          if (!g_file_replace_contents (dest,
+-                                        g_variant_get_data (content),
+-                                        g_variant_get_size (content),
+-                                        NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION,
+-                                        NULL, cancellable, error))
++          if (!glnx_file_replace_contents_at (extra_dfd, extra_data_name,
++                                              data, len,
++                                              GLNX_FILE_REPLACE_NODATASYNC,
++                                              cancellable, error))
+             {
+               g_prefix_error (error, _("While writing extra data file '%s': "), extra_data_name);
+               return FALSE;
+@@ -8755,6 +8763,7 @@ flatpak_dir_deploy (FlatpakDir          *self,
+   OstreeRepoCheckoutAtOptions options = { 0, };
+   const char *checksum;
+   glnx_autofd int checkoutdir_dfd = -1;
++  glnx_autofd int app_files_dfd = -1;
+   const char *xa_ref = NULL;
+   g_autofree char *checkout_basename = NULL;
+   gboolean created_extra_data = FALSE;
+@@ -8911,15 +8920,24 @@ flatpak_dir_deploy (FlatpakDir          *self,
+         }
+     }
+ 
++  if (!glnx_opendirat (deploy_base_dfd, checkoutdir_basename, FALSE, &checkoutdir_dfd, error))
++    return FALSE;
++
+   /* Extract any extra data */
+-  extradir = g_file_resolve_relative_path (checkoutdir, "files/extra");
+-  if (!flatpak_rm_rf (extradir, cancellable, error))
++  app_files_dfd = glnx_chaseat (checkoutdir_dfd, "files",
++                                GLNX_CHASE_RESOLVE_NO_SYMLINKS |
++                                GLNX_CHASE_MUST_BE_DIRECTORY,
++                                error);
++  if (app_files_dfd < 0)
++    return FALSE;
++
++  if (!glnx_shutil_rm_rf_at (app_files_dfd, "extra", cancellable, error))
+     {
+       g_prefix_error (error, _("While trying to remove existing extra dir: "));
+       return FALSE;
+     }
+ 
+-  if (!extract_extra_data (self, checksum, extradir, &created_extra_data, cancellable, error))
++  if (!extract_extra_data (self, checksum, app_files_dfd, &created_extra_data, cancellable, error))
+     return FALSE;
+ 
+   if (created_extra_data)
+@@ -9128,9 +9146,6 @@ flatpak_dir_deploy (FlatpakDir          *self,
+   if (!flatpak_bytes_save (deploy_data_file, deploy_data, cancellable, error))
+     return FALSE;
+ 
+-  if (!glnx_opendirat (deploy_base_dfd, checkoutdir_basename, TRUE, &checkoutdir_dfd, error))
+-    return FALSE;
+-
+   if (syncfs (checkoutdir_dfd) != 0)
+     {
+       glnx_set_error_from_errno (error);
diff -Nru flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/dir-Validate-extra-data-name.patch flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/dir-Validate-extra-data-name.patch
--- flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/dir-Validate-extra-data-name.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/dir-Validate-extra-data-name.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,42 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Fri, 7 Aug 2026 22:59:35 +0200
+Subject: dir: Validate extra-data name
+
+Reject extra-data names containing ".", "..", or "/" early during the
+download phase. The name is used to construct a local cache file path
+via flatpak_build_file() which uses g_file_resolve_relative_path(),
+so ".." components could traverse outside the cache directory.
+
+In practice this is not exploitable: the local cache path is only
+used for a g_file_query_exists() check followed by g_file_load_contents(),
+and the loaded content is verified against the expected sha256 from
+the commit metadata.
+
+Bug: https://github.com/flatpak/xdg-dbus-proxy/security/advisories/GHSA-fqx6-vh4p-42cg
+[smcv: Backport to 1.16.x where pull_extra_data_to_bytes() wasn't factored out]
+Origin: backport, 1.18.1
+---
+ common/flatpak-dir.c | 10 ++++++++--
+ 1 file changed, 8 insertions(+), 2 deletions(-)
+
+diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c
+index f4c3d27..7273cda 100644
+--- a/common/flatpak-dir.c
++++ b/common/flatpak-dir.c
+@@ -5810,8 +5810,14 @@ flatpak_dir_pull_extra_data (FlatpakDir          *self,
+ 
+       extra_data_sha256 = ostree_checksum_from_bytes (sha256_bytes);
+ 
+-      if (*extra_data_name == 0)
+-        return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Empty name for extra data uri %s"), extra_data_uri);
++      if (extra_data_name == NULL || *extra_data_name == '\0' ||
++          strcmp (extra_data_name, ".") == 0 ||
++          strcmp (extra_data_name, "..") == 0 ||
++          strchr (extra_data_name, '/') != NULL)
++        {
++          return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA,
++                                     _("Invalid extra data name '%s'"), extra_data_name);
++        }
+ 
+       /* Don't allow file uris here as that could read local files based on remote data */
+       if (!g_str_has_prefix (extra_data_uri, "http:") &&
diff -Nru flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/utils-Add-flatpak_cp_a_at-a-fd-based-flatpak_cp_a-variant.patch flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/utils-Add-flatpak_cp_a_at-a-fd-based-flatpak_cp_a-variant.patch
--- flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/utils-Add-flatpak_cp_a_at-a-fd-based-flatpak_cp_a-variant.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/utils-Add-flatpak_cp_a_at-a-fd-based-flatpak_cp_a-variant.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,127 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Mon, 29 Jun 2026 17:02:41 +0200
+Subject: utils: Add flatpak_cp_a_at, a fd-based flatpak_cp_a variant
+
+For all the usual reasons where an attacker might control a symlink.
+
+For simplicity this does not implement the unused FLATPAK_CP_FLAGS_MOVE
+flag.
+
+[smcv: Separated from a larger commit]
+
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-fqx6-vh4p-42cg
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-8qxj-x646-phcm
+Origin: upstream, 1.18.1
+---
+ common/flatpak-utils-private.h |  7 ++++
+ common/flatpak-utils.c         | 79 ++++++++++++++++++++++++++++++++++++++++++
+ 2 files changed, 86 insertions(+)
+
+diff --git a/common/flatpak-utils-private.h b/common/flatpak-utils-private.h
+index b071e7e..858aa13 100644
+--- a/common/flatpak-utils-private.h
++++ b/common/flatpak-utils-private.h
+@@ -205,6 +205,13 @@ gboolean   flatpak_cp_a (GFile         *src,
+                          GCancellable  *cancellable,
+                          GError       **error);
+ 
++gboolean   flatpak_cp_a_at (int            src_dfd,
++                            int            dest_parent_dfd,
++                            const char    *dest_name,
++                            FlatpakCpFlags flags,
++                            GCancellable  *cancellable,
++                            GError       **error);
++
+ gboolean flatpak_mkdir_p (GFile        *dir,
+                           GCancellable *cancellable,
+                           GError      **error);
+diff --git a/common/flatpak-utils.c b/common/flatpak-utils.c
+index 2a4b002..f39b4a0 100644
+--- a/common/flatpak-utils.c
++++ b/common/flatpak-utils.c
+@@ -1309,6 +1309,85 @@ out:
+   return ret;
+ }
+ 
++gboolean
++flatpak_cp_a_at (int            src_dfd,
++                 int            dest_parent_dfd,
++                 const char    *dest_name,
++                 FlatpakCpFlags flags,
++                 GCancellable  *cancellable,
++                 GError       **error)
++{
++  gboolean merge = (flags & FLATPAK_CP_FLAGS_MERGE) != 0;
++  gboolean no_chown = (flags & FLATPAK_CP_FLAGS_NO_CHOWN) != 0;
++  struct stat src_stbuf;
++  glnx_autofd int dest_dfd = -1;
++  g_auto(GLnxDirFdIterator) src_iter = { 0, };
++
++  if (fstat (src_dfd, &src_stbuf) != 0)
++    return glnx_throw_errno_prefix (error, "fstat");
++
++  if (TEMP_FAILURE_RETRY (mkdirat (dest_parent_dfd, dest_name, 0755)) != 0 &&
++      (!merge || errno != EEXIST))
++    return glnx_throw_errno_prefix (error, "mkdirat(%s)", dest_name);
++
++  if (!glnx_opendirat (dest_parent_dfd, dest_name, FALSE, &dest_dfd, error))
++    return FALSE;
++
++  if (!no_chown)
++    {
++      if (TEMP_FAILURE_RETRY (fchown (dest_dfd, src_stbuf.st_uid, src_stbuf.st_gid)) != 0)
++        return glnx_throw_errno_prefix (error, "fchown");
++    }
++
++  if (TEMP_FAILURE_RETRY (fchmod (dest_dfd, src_stbuf.st_mode & 07777)) != 0)
++    return glnx_throw_errno_prefix (error, "fchmod");
++
++  if (!glnx_dirfd_iterator_init_at (src_dfd, ".", FALSE, &src_iter, error))
++    return FALSE;
++
++  while (TRUE)
++    {
++      struct dirent *dent;
++      const char *name;
++
++      if (!glnx_dirfd_iterator_next_dent_ensure_dtype (&src_iter, &dent,
++                                                       cancellable, error))
++        return FALSE;
++
++      if (dent == NULL)
++        break;
++
++      name = dent->d_name;
++
++      if (dent->d_type == DT_DIR)
++        {
++          glnx_autofd int child_src_dfd = -1;
++
++          if (!glnx_opendirat (src_iter.fd, name, FALSE, &child_src_dfd, error))
++            return FALSE;
++
++          if (!flatpak_cp_a_at (child_src_dfd, dest_dfd,
++                                name, flags, cancellable, error))
++            return FALSE;
++        }
++      else
++        {
++          GLnxFileCopyFlags copyflags = GLNX_FILE_COPY_OVERWRITE;
++          if (no_chown)
++            copyflags |= GLNX_FILE_COPY_NOCHOWN;
++
++          (void) unlinkat (dest_dfd, name, 0);
++
++          if (!glnx_file_copy_at (src_iter.fd, name, NULL,
++                                  dest_dfd, name, copyflags,
++                                  cancellable, error))
++            return FALSE;
++        }
++    }
++
++  return TRUE;
++}
++
+ static gboolean
+ _flatpak_canonicalize_permissions (int         parent_dfd,
+                                    const char *rel_path,
diff -Nru flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/utils-Make-flatpak_cp_a-a-wrapper-around-flatpak_cp_a_at.patch flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/utils-Make-flatpak_cp_a-a-wrapper-around-flatpak_cp_a_at.patch
--- flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/utils-Make-flatpak_cp_a-a-wrapper-around-flatpak_cp_a_at.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/utils-Make-flatpak_cp_a-a-wrapper-around-flatpak_cp_a_at.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,196 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Mon, 29 Jun 2026 17:02:41 +0200
+Subject: utils: Make flatpak_cp_a a wrapper around flatpak_cp_a_at
+
+[smcv: Separated from a larger commit]
+
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-fqx6-vh4p-42cg
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-8qxj-x646-phcm
+Origin: upstream, 1.18.1
+---
+ common/flatpak-utils-private.h |   1 -
+ common/flatpak-utils.c         | 152 +++++++----------------------------------
+ 2 files changed, 24 insertions(+), 129 deletions(-)
+
+diff --git a/common/flatpak-utils-private.h b/common/flatpak-utils-private.h
+index 858aa13..5cdab1a 100644
+--- a/common/flatpak-utils-private.h
++++ b/common/flatpak-utils-private.h
+@@ -196,7 +196,6 @@ typedef enum {
+   FLATPAK_CP_FLAGS_NONE = 0,
+   FLATPAK_CP_FLAGS_MERGE = 1 << 0,
+   FLATPAK_CP_FLAGS_NO_CHOWN = 1 << 1,
+-  FLATPAK_CP_FLAGS_MOVE = 1 << 2,
+ } FlatpakCpFlags;
+ 
+ gboolean   flatpak_cp_a (GFile         *src,
+diff --git a/common/flatpak-utils.c b/common/flatpak-utils.c
+index f39b4a0..0f95931 100644
+--- a/common/flatpak-utils.c
++++ b/common/flatpak-utils.c
+@@ -1181,134 +1181,6 @@ flatpak_openat_noatime (int           dfd,
+     }
+ }
+ 
+-gboolean
+-flatpak_cp_a (GFile         *src,
+-              GFile         *dest,
+-              FlatpakCpFlags flags,
+-              GCancellable  *cancellable,
+-              GError       **error)
+-{
+-  gboolean ret = FALSE;
+-  GFileEnumerator *enumerator = NULL;
+-  GFileInfo *src_info = NULL;
+-  GFile *dest_child = NULL;
+-  int dest_dfd = -1;
+-  gboolean merge = (flags & FLATPAK_CP_FLAGS_MERGE) != 0;
+-  gboolean no_chown = (flags & FLATPAK_CP_FLAGS_NO_CHOWN) != 0;
+-  gboolean move = (flags & FLATPAK_CP_FLAGS_MOVE) != 0;
+-  g_autoptr(GFileInfo) child_info = NULL;
+-  GError *temp_error = NULL;
+-  int r;
+-
+-  enumerator = g_file_enumerate_children (src, "standard::type,standard::name,unix::uid,unix::gid,unix::mode",
+-                                          G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+-                                          cancellable, error);
+-  if (!enumerator)
+-    goto out;
+-
+-  src_info = g_file_query_info (src, "standard::name,unix::mode,unix::uid,unix::gid," \
+-                                     "time::modified,time::modified-usec,time::access,time::access-usec",
+-                                G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+-                                cancellable, error);
+-  if (!src_info)
+-    goto out;
+-
+-  do
+-    r = mkdir (flatpak_file_get_path_cached (dest), 0755);
+-  while (G_UNLIKELY (r == -1 && errno == EINTR));
+-  if (r == -1 &&
+-      (!merge || errno != EEXIST))
+-    {
+-      glnx_set_error_from_errno (error);
+-      goto out;
+-    }
+-
+-  if (!glnx_opendirat (AT_FDCWD, flatpak_file_get_path_cached (dest), TRUE,
+-                       &dest_dfd, error))
+-    goto out;
+-
+-  if (!no_chown)
+-    {
+-      do
+-        r = fchown (dest_dfd,
+-                    g_file_info_get_attribute_uint32 (src_info, "unix::uid"),
+-                    g_file_info_get_attribute_uint32 (src_info, "unix::gid"));
+-      while (G_UNLIKELY (r == -1 && errno == EINTR));
+-      if (r == -1)
+-        {
+-          glnx_set_error_from_errno (error);
+-          goto out;
+-        }
+-    }
+-
+-  do
+-    r = fchmod (dest_dfd, g_file_info_get_attribute_uint32 (src_info, "unix::mode"));
+-  while (G_UNLIKELY (r == -1 && errno == EINTR));
+-
+-  if (dest_dfd != -1)
+-    {
+-      (void) close (dest_dfd);
+-      dest_dfd = -1;
+-    }
+-
+-  while ((child_info = g_file_enumerator_next_file (enumerator, cancellable, &temp_error)))
+-    {
+-      const char *name = g_file_info_get_name (child_info);
+-      g_autoptr(GFile) src_child = g_file_get_child (src, name);
+-
+-      if (dest_child)
+-        g_object_unref (dest_child);
+-      dest_child = g_file_get_child (dest, name);
+-
+-      if (g_file_info_get_file_type (child_info) == G_FILE_TYPE_DIRECTORY)
+-        {
+-          if (!flatpak_cp_a (src_child, dest_child, flags,
+-                             cancellable, error))
+-            goto out;
+-        }
+-      else
+-        {
+-          (void) unlink (flatpak_file_get_path_cached (dest_child));
+-          GFileCopyFlags copyflags = G_FILE_COPY_OVERWRITE | G_FILE_COPY_NOFOLLOW_SYMLINKS;
+-          if (!no_chown)
+-            copyflags |= G_FILE_COPY_ALL_METADATA;
+-          if (move)
+-            {
+-              if (!g_file_move (src_child, dest_child, copyflags,
+-                                cancellable, NULL, NULL, error))
+-                goto out;
+-            }
+-          else
+-            {
+-              if (!g_file_copy (src_child, dest_child, copyflags,
+-                                cancellable, NULL, NULL, error))
+-                goto out;
+-            }
+-        }
+-
+-      g_clear_object (&child_info);
+-    }
+-
+-  if (temp_error != NULL)
+-    {
+-      g_propagate_error (error, temp_error);
+-      goto out;
+-    }
+-
+-  if (move &&
+-      !g_file_delete (src, NULL, error))
+-    goto out;
+-
+-  ret = TRUE;
+-out:
+-  if (dest_dfd != -1)
+-    (void) close (dest_dfd);
+-  g_clear_object (&src_info);
+-  g_clear_object (&enumerator);
+-  g_clear_object (&dest_child);
+-  return ret;
+-}
+-
+ gboolean
+ flatpak_cp_a_at (int            src_dfd,
+                  int            dest_parent_dfd,
+@@ -1388,6 +1260,30 @@ flatpak_cp_a_at (int            src_dfd,
+   return TRUE;
+ }
+ 
++gboolean
++flatpak_cp_a (GFile         *src,
++              GFile         *dest,
++              FlatpakCpFlags flags,
++              GCancellable  *cancellable,
++              GError       **error)
++{
++  const char *src_path = flatpak_file_get_path_cached (src);
++  g_autoptr(GFile) dest_parent = g_file_get_parent (dest);
++  const char *dest_parent_path = flatpak_file_get_path_cached (dest_parent);
++  const char *dest_name = glnx_basename (flatpak_file_get_path_cached (dest));
++  glnx_autofd int src_dfd = -1;
++  glnx_autofd int dest_parent_dfd = -1;
++
++  if (!glnx_opendirat (AT_FDCWD, src_path, FALSE, &src_dfd, error))
++    return FALSE;
++
++  if (!glnx_opendirat (AT_FDCWD, dest_parent_path, FALSE, &dest_parent_dfd, error))
++    return FALSE;
++
++  return flatpak_cp_a_at (src_dfd, dest_parent_dfd, dest_name, flags,
++                          cancellable, error);
++}
++
+ static gboolean
+ _flatpak_canonicalize_permissions (int         parent_dfd,
+                                    const char *rel_path,
diff -Nru flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/utils-Remove-unused-GFile-based-flatpak_cp_a.patch flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/utils-Remove-unused-GFile-based-flatpak_cp_a.patch
--- flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/utils-Remove-unused-GFile-based-flatpak_cp_a.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/utils-Remove-unused-GFile-based-flatpak_cp_a.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,67 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Mon, 29 Jun 2026 20:18:40 +0200
+Subject: utils: Remove unused GFile based flatpak_cp_a
+
+We use the fd based flatpak_cp_a_at now everywhere, so we can drop this
+one.
+
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-fqx6-vh4p-42cg
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-8qxj-x646-phcm
+Origin: upstream, 1.18.1
+---
+ common/flatpak-utils-private.h |  6 ------
+ common/flatpak-utils.c         | 24 ------------------------
+ 2 files changed, 30 deletions(-)
+
+diff --git a/common/flatpak-utils-private.h b/common/flatpak-utils-private.h
+index 5cdab1a..b56a354 100644
+--- a/common/flatpak-utils-private.h
++++ b/common/flatpak-utils-private.h
+@@ -198,12 +198,6 @@ typedef enum {
+   FLATPAK_CP_FLAGS_NO_CHOWN = 1 << 1,
+ } FlatpakCpFlags;
+ 
+-gboolean   flatpak_cp_a (GFile         *src,
+-                         GFile         *dest,
+-                         FlatpakCpFlags flags,
+-                         GCancellable  *cancellable,
+-                         GError       **error);
+-
+ gboolean   flatpak_cp_a_at (int            src_dfd,
+                             int            dest_parent_dfd,
+                             const char    *dest_name,
+diff --git a/common/flatpak-utils.c b/common/flatpak-utils.c
+index 0f95931..c549514 100644
+--- a/common/flatpak-utils.c
++++ b/common/flatpak-utils.c
+@@ -1260,30 +1260,6 @@ flatpak_cp_a_at (int            src_dfd,
+   return TRUE;
+ }
+ 
+-gboolean
+-flatpak_cp_a (GFile         *src,
+-              GFile         *dest,
+-              FlatpakCpFlags flags,
+-              GCancellable  *cancellable,
+-              GError       **error)
+-{
+-  const char *src_path = flatpak_file_get_path_cached (src);
+-  g_autoptr(GFile) dest_parent = g_file_get_parent (dest);
+-  const char *dest_parent_path = flatpak_file_get_path_cached (dest_parent);
+-  const char *dest_name = glnx_basename (flatpak_file_get_path_cached (dest));
+-  glnx_autofd int src_dfd = -1;
+-  glnx_autofd int dest_parent_dfd = -1;
+-
+-  if (!glnx_opendirat (AT_FDCWD, src_path, FALSE, &src_dfd, error))
+-    return FALSE;
+-
+-  if (!glnx_opendirat (AT_FDCWD, dest_parent_path, FALSE, &dest_parent_dfd, error))
+-    return FALSE;
+-
+-  return flatpak_cp_a_at (src_dfd, dest_parent_dfd, dest_name, flags,
+-                          cancellable, error);
+-}
+-
+ static gboolean
+ _flatpak_canonicalize_permissions (int         parent_dfd,
+                                    const char *rel_path,
diff -Nru flatpak-1.16.6/debian/patches/GHSA-jr92-2v97-wgvc/oci-Avoid-overflow-in-delta_read_data.patch flatpak-1.16.6/debian/patches/GHSA-jr92-2v97-wgvc/oci-Avoid-overflow-in-delta_read_data.patch
--- flatpak-1.16.6/debian/patches/GHSA-jr92-2v97-wgvc/oci-Avoid-overflow-in-delta_read_data.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-jr92-2v97-wgvc/oci-Avoid-overflow-in-delta_read_data.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,36 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Mon, 27 Jul 2026 16:00:52 +0200
+Subject: oci: Avoid overflow in delta_read_data
+MIME-Version: 1.0
+Content-Type: text/plain; charset="utf-8"
+Content-Transfer-Encoding: 8bit
+
+delta_read_data computed g_malloc(size + 1) where size came from the
+delta stream. If size equals G_MAXSIZE, size + 1 wraps to zero and
+g_malloc returns a minimal allocation, then g_input_stream_read_all
+writes size bytes into it — a heap buffer overflow.
+
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-jr92-2v97-wgvc
+Origin: upstream, 1.18.1
+---
+ common/flatpak-oci-registry.c | 7 ++++++-
+ 1 file changed, 6 insertions(+), 1 deletion(-)
+
+diff --git a/common/flatpak-oci-registry.c b/common/flatpak-oci-registry.c
+index 8490b60..f1e2f43 100644
+--- a/common/flatpak-oci-registry.c
++++ b/common/flatpak-oci-registry.c
+@@ -1830,7 +1830,12 @@ delta_read_data (GInputStream   *in,
+                  GCancellable   *cancellable,
+                  GError        **error)
+ {
+-  g_autofree guchar *buf = g_malloc (size+1);
++  g_autofree guchar *buf = NULL;
++
++  if (size > SIZE_MAX - 1)
++    return glnx_null_throw (error, _("Invalid delta file format"));
++
++  buf = g_malloc (size + 1);
+ 
+   if (!g_input_stream_read_all (in, buf, size, NULL, cancellable, error))
+     return NULL;
diff -Nru flatpak-1.16.6/debian/patches/GHSA-jr92-2v97-wgvc/oci-Limit-delta-path-length-to-PATH_MAX.patch flatpak-1.16.6/debian/patches/GHSA-jr92-2v97-wgvc/oci-Limit-delta-path-length-to-PATH_MAX.patch
--- flatpak-1.16.6/debian/patches/GHSA-jr92-2v97-wgvc/oci-Limit-delta-path-length-to-PATH_MAX.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-jr92-2v97-wgvc/oci-Limit-delta-path-length-to-PATH_MAX.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,66 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Mon, 6 Jul 2026 15:19:16 +0200
+Subject: oci: Limit delta path length to PATH_MAX
+
+The previous commit ensures that there isn't a heap overflow when
+reading a huge delta path, but we should also just reject unreasonably
+long paths. So we chose the arbitrary limit of PATH_MAX and assume that
+anything beyond that arbitrary limit is probably abusive.
+
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-jr92-2v97-wgvc
+Origin: upstream, 1.18.1
+---
+ common/flatpak-oci-registry.c | 25 +++++++++++++++----------
+ 1 file changed, 15 insertions(+), 10 deletions(-)
+
+diff --git a/common/flatpak-oci-registry.c b/common/flatpak-oci-registry.c
+index f1e2f43..6021f03 100644
+--- a/common/flatpak-oci-registry.c
++++ b/common/flatpak-oci-registry.c
+@@ -1933,8 +1933,6 @@ flatpak_oci_registry_apply_delta_stream (FlatpakOciRegistry    *self,
+     {
+       guint8 op;
+       gsize size;
+-      g_autofree char *path = NULL;
+-      g_autofree char *clean_path = NULL;
+       g_autoptr(GError) local_error = NULL;
+       gboolean eof;
+ 
+@@ -1957,22 +1955,29 @@ flatpak_oci_registry_apply_delta_stream (FlatpakOciRegistry    *self,
+           break;
+ 
+         case DELTA_OP_OPEN:
+-          path = (char *)delta_read_data (in, size, cancellable, error);
+-          if (path == NULL)
+-            return FALSE;
+-          clean_path = delta_clean_path (path);
+-
+-          g_clear_object (&content_file);
+-
+           {
+-            g_autoptr(GFile) child = g_file_resolve_relative_path (content_dir, clean_path);
++            g_autofree char *path = NULL;
++            g_autofree char *clean_path = NULL;
++            g_autoptr(GFile) child = NULL;
+             g_autoptr(GFileInputStream) child_in = NULL;
+ 
++            if (size > PATH_MAX)
++              return flatpak_fail (error, _("Invalid delta file format"));
++
++            path = (char *) delta_read_data (in, size, cancellable, error);
++            if (path == NULL)
++              return FALSE;
++
++            clean_path = delta_clean_path (path);
++
++            child = g_file_resolve_relative_path (content_dir, clean_path);
++
+             child_in = g_file_read (child, cancellable, error);
+             if (child_in == NULL)
+               return FALSE;
+ 
+             /* We can't seek in the ostree repo file, so copy it to temp file */
++            g_clear_object (&content_file);
+             content_file = copy_stream_to_file (self, G_INPUT_STREAM (child_in), cancellable, error);
+             if (content_file == NULL)
+               return FALSE;
diff -Nru flatpak-1.16.6/debian/patches/GHSA-jr92-2v97-wgvc/oci-Use-gsize-for-OCI-delta-sizes.patch flatpak-1.16.6/debian/patches/GHSA-jr92-2v97-wgvc/oci-Use-gsize-for-OCI-delta-sizes.patch
--- flatpak-1.16.6/debian/patches/GHSA-jr92-2v97-wgvc/oci-Use-gsize-for-OCI-delta-sizes.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-jr92-2v97-wgvc/oci-Use-gsize-for-OCI-delta-sizes.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,76 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Mon, 29 Jun 2026 22:42:30 +0200
+Subject: oci: Use gsize for OCI delta sizes
+
+The delta varint parser decoded into guint64 values which were then
+passed to GLib I/O and allocation functions that take gsize. On 32-bit
+systems where gsize is 32 bits this silently truncated the values.
+
+Change the varint output and all delta operation size parameters to
+gsize, and clamp the parsed value to G_MAXSIZE.
+
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-jr92-2v97-wgvc
+Origin: upstream, 1.18.1
+---
+ common/flatpak-oci-registry.c | 13 ++++++++-----
+ 1 file changed, 8 insertions(+), 5 deletions(-)
+
+diff --git a/common/flatpak-oci-registry.c b/common/flatpak-oci-registry.c
+index 942bb47..8490b60 100644
+--- a/common/flatpak-oci-registry.c
++++ b/common/flatpak-oci-registry.c
+@@ -1731,7 +1731,7 @@ delta_read_byte (GInputStream   *in,
+ 
+ static gboolean
+ delta_read_varuint (GInputStream   *in,
+-                    guint64        *out,
++                    gsize          *out,
+                     GCancellable   *cancellable,
+                     GError        **error)
+ {
+@@ -1755,6 +1755,9 @@ delta_read_varuint (GInputStream   *in,
+     }
+   while (more_data);
+ 
++  if (res > G_MAXSIZE)
++    return flatpak_fail (error, _("Invalid delta file format"));
++
+   *out = res;
+   return TRUE;
+ }
+@@ -1762,7 +1765,7 @@ delta_read_varuint (GInputStream   *in,
+ static gboolean
+ delta_copy_data (GInputStream   *in,
+                  GOutputStream  *out,
+-                 guint64         size,
++                 gsize           size,
+                  guchar         *buffer,
+                  GCancellable   *cancellable,
+                  GError        **error)
+@@ -1790,7 +1793,7 @@ static gboolean
+ delta_add_data (GInputStream   *in1,
+                 GInputStream   *in2,
+                 GOutputStream  *out,
+-                guint64         size,
++                gsize           size,
+                 guchar         *buffer1,
+                 guchar         *buffer2,
+                 GCancellable   *cancellable,
+@@ -1823,7 +1826,7 @@ delta_add_data (GInputStream   *in1,
+ 
+ static guchar *
+ delta_read_data (GInputStream   *in,
+-                 guint64         size,
++                 gsize           size,
+                  GCancellable   *cancellable,
+                  GError        **error)
+ {
+@@ -1924,7 +1927,7 @@ flatpak_oci_registry_apply_delta_stream (FlatpakOciRegistry    *self,
+   while (TRUE)
+     {
+       guint8 op;
+-      guint64 size;
++      gsize size;
+       g_autofree char *path = NULL;
+       g_autofree char *clean_path = NULL;
+       g_autoptr(GError) local_error = NULL;
diff -Nru flatpak-1.16.6/debian/patches/GHSA-q4gr-vc25-57m5/dir-Fall-back-to-the-deployed-ref-commit-when-up-downgrad.patch flatpak-1.16.6/debian/patches/GHSA-q4gr-vc25-57m5/dir-Fall-back-to-the-deployed-ref-commit-when-up-downgrad.patch
--- flatpak-1.16.6/debian/patches/GHSA-q4gr-vc25-57m5/dir-Fall-back-to-the-deployed-ref-commit-when-up-downgrad.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-q4gr-vc25-57m5/dir-Fall-back-to-the-deployed-ref-commit-when-up-downgrad.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,36 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Mon, 3 Aug 2026 18:17:00 +0200
+Subject: dir: Fall back to the deployed ref commit when up/downgrading
+
+Otherwise, if an unprivileged user manages to remove the remote ref, the
+downgrading protection is not in effect.
+
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-q4gr-vc25-57m5
+Origin: upstream, 1.18.1
+---
+ common/flatpak-dir.c | 12 ++++++++++++
+ 1 file changed, 12 insertions(+)
+
+diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c
+index 4378212..400ad3e 100644
+--- a/common/flatpak-dir.c
++++ b/common/flatpak-dir.c
+@@ -6384,6 +6384,18 @@ flatpak_dir_pull_untrusted_local (FlatpakDir          *self,
+                                  &current_checksum, NULL, error))
+     return FALSE;
+ 
++  /* The remote tracking ref may be absent (e.g. the remote was removed and
++   * re-added, the app was installed from a bundle, or the ref was deleted by
++   * ostree-prune/flatpak-repair).  Fall back to the deploy/ ref that flatpak
++   * always writes on every successful deploy so that our downgrade check can
++   * work. */
++  if (current_checksum == NULL)
++    {
++      g_autofree char *deploy_ref = g_strconcat ("deploy/", ref, NULL);
++      flatpak_repo_resolve_rev (self->repo, NULL, NULL, deploy_ref, TRUE,
++                                &current_checksum, NULL, NULL);
++    }
++
+   if (current_checksum != NULL &&
+       !ostree_repo_load_commit (self->repo, current_checksum, &old_commit, NULL, error))
+     return FALSE;
diff -Nru flatpak-1.16.6/debian/patches/GHSA-q4gr-vc25-57m5/system-dir-Prevent-removing-of-deployed-refs-via-the-syst.patch flatpak-1.16.6/debian/patches/GHSA-q4gr-vc25-57m5/system-dir-Prevent-removing-of-deployed-refs-via-the-syst.patch
--- flatpak-1.16.6/debian/patches/GHSA-q4gr-vc25-57m5/system-dir-Prevent-removing-of-deployed-refs-via-the-syst.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-q4gr-vc25-57m5/system-dir-Prevent-removing-of-deployed-refs-via-the-syst.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,193 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Mon, 3 Aug 2026 19:20:01 +0200
+Subject: system-dir: Prevent removing of deployed refs via the system-helper
+
+We have two use cases for removing refs: uninstalling, and pruning of
+undeployed refs. Pruning undeployed refs is something we want anyone to
+be able to do, because they can also pull updates and then not deploy
+them. Uninstalling arbitrary refs on the other hand is problematic, and
+its possible to remove the AppStream ref, and a deployed ref.
+
+So we split removing a ref into a function which removes any ref, and
+use it internally to implement e.g. uninstalling, and a function to
+remove refs which are safe to remove. Only the latter one will escalate
+through the system helper, making it harder for unprivileged users to
+get into a state which can be exploited.
+
+See the previous commit for such a state.
+
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-q4gr-vc25-57m5
+Origin: upstream, 1.18.1
+---
+ common/flatpak-dir-private.h          |  5 +++
+ common/flatpak-dir.c                  | 72 +++++++++++++++++++++++++++--------
+ common/flatpak-installation.c         |  8 ++--
+ system-helper/flatpak-system-helper.c |  2 +-
+ 4 files changed, 68 insertions(+), 19 deletions(-)
+
+diff --git a/common/flatpak-dir-private.h b/common/flatpak-dir-private.h
+index 1e25cb1..50793b2 100644
+--- a/common/flatpak-dir-private.h
++++ b/common/flatpak-dir-private.h
+@@ -772,6 +772,11 @@ gboolean              flatpak_dir_remove_ref                                (Fla
+                                                                              const char                    *ref,
+                                                                              GCancellable                  *cancellable,
+                                                                              GError                       **error);
++gboolean              flatpak_dir_remove_undeployed_ref                     (FlatpakDir                    *self,
++                                                                             const char                    *remote_name,
++                                                                             const char                    *ref,
++                                                                             GCancellable                  *cancellable,
++                                                                             GError                       **error);
+ gboolean              flatpak_dir_update_exports                            (FlatpakDir                    *self,
+                                                                              const char                    *app,
+                                                                              GCancellable                  *cancellable,
+diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c
+index 400ad3e..f4c3d27 100644
+--- a/common/flatpak-dir.c
++++ b/common/flatpak-dir.c
+@@ -11420,25 +11420,61 @@ flatpak_dir_undeploy_all (FlatpakDir        *self,
+  * flatpak_dir_remove_ref:
+  * @self: a #FlatpakDir
+  * @remote_name: the name of the remote
+- * @ref: the flatpak ref to remove
++ * @ref: the ref to remove
+  * @cancellable: (nullable) (optional): a #GCancellable
+  * @error: a #GError
+  *
+- * Remove the flatpak ref given by @remote_name:@ref from the underlying
+- * OSTree repo. Attempting to remove a ref that is currently deployed
+- * is an error, you need to uninstall the flatpak first. Note that this does
+- * not remove the objects bound to @ref from the disk, you will need to
+- * call flatpak_dir_prune() to do that.
++ * Remove the ref given by @remote_name:@ref from the underlying
++ * OSTree repo. Note that this does not remove the objects bound to
++ * @ref from the disk, you will need to call flatpak_dir_prune()
++ * to do that.
+  *
+  * Returns: %TRUE if removing the ref succeeded, %FALSE otherwise.
+  */
+ gboolean
+ flatpak_dir_remove_ref (FlatpakDir        *self,
+                         const char        *remote_name,
+-                        const char        *ref, /* NOTE: Not necessarily a app/runtime ref */
++                        const char        *ref,
+                         GCancellable      *cancellable,
+                         GError           **error)
+ {
++  if (!ostree_repo_set_ref_immediate (self->repo,
++                                      remote_name,
++                                      ref,
++                                      NULL,
++                                      cancellable,
++                                      error))
++    return FALSE;
++
++  return TRUE;
++}
++
++/**
++ * flatpak_dir_remove_undeployed_ref:
++ * @self: a #FlatpakDir
++ * @remote_name: the name of the remote
++ * @ref: the flatpak ref to remove
++ * @cancellable: (nullable) (optional): a #GCancellable
++ * @error: a #GError
++ *
++ * Remove the flatpak ref given by @remote_name:@ref from the underlying
++ * OSTree repo. The ref must be a valid app or runtime ref, and it must
++ * not be currently deployed. Note that this does not remove the objects
++ * bound to @ref from the disk, you will need to call
++ * flatpak_dir_prune() to do that.
++ *
++ * Returns: %TRUE if removing the ref succeeded, %FALSE otherwise.
++ */
++gboolean
++flatpak_dir_remove_undeployed_ref (FlatpakDir    *self,
++                                   const char    *remote_name,
++                                   const char    *ref,
++                                   GCancellable  *cancellable,
++                                   GError       **error)
++{
++  g_autoptr(FlatpakDecomposed) decomposed = NULL;
++  g_autoptr(GBytes) deploy_data = NULL;
++
+   if (flatpak_dir_use_system_helper (self, NULL))
+     {
+       const char *installation = flatpak_dir_get_id (self);
+@@ -11455,15 +11491,21 @@ flatpak_dir_remove_ref (FlatpakDir        *self,
+       return TRUE;
+     }
+ 
+-  if (!ostree_repo_set_ref_immediate (self->repo,
+-                                      remote_name,
+-                                      ref,
+-                                      NULL,
+-                                      cancellable,
+-                                      error))
++  decomposed = flatpak_decomposed_new_from_ref (ref, error);
++  if (decomposed == NULL)
+     return FALSE;
+ 
+-  return TRUE;
++  deploy_data = flatpak_dir_get_deploy_data (self, decomposed,
++                                             FLATPAK_DEPLOY_VERSION_ANY,
++                                             NULL, NULL);
++  if (deploy_data != NULL)
++    {
++      g_set_error (error, FLATPAK_ERROR, FLATPAK_ERROR_ALREADY_INSTALLED,
++                   _("%s is currently deployed, you need to uninstall it first"), ref);
++      return FALSE;
++    }
++
++  return flatpak_dir_remove_ref (self, remote_name, ref, cancellable, error);
+ }
+ 
+ gboolean
+@@ -13941,7 +13983,7 @@ flatpak_dir_cleanup_undeployed_refs (FlatpakDir   *self,
+       FlatpakDecomposed *ref = g_ptr_array_index (undeployed_refs, i);
+       g_autofree gchar *remote = flatpak_decomposed_dup_remote (ref);
+ 
+-      if (!flatpak_dir_remove_ref (self, remote, flatpak_decomposed_get_ref (ref), cancellable, error))
++      if (!flatpak_dir_remove_undeployed_ref (self, remote, flatpak_decomposed_get_ref (ref), cancellable, error))
+         return FALSE;
+     }
+ 
+diff --git a/common/flatpak-installation.c b/common/flatpak-installation.c
+index fe5e575..2e43697 100644
+--- a/common/flatpak-installation.c
++++ b/common/flatpak-installation.c
+@@ -2911,8 +2911,10 @@ flatpak_installation_list_remote_related_refs_for_installed_sync (FlatpakInstall
+  * @error: return location for a #GError
+  *
+  * Remove the OSTree ref given by @remote_name:@ref from the local flatpak
+- * repository. The next time the underlying OSTree repo is pruned, objects
+- * which were attached to that ref will be removed. This is useful if you
++ * repository. The ref must not be currently deployed; attempting to remove
++ * a deployed ref will return an error. Use flatpak_installation_uninstall_full()
++ * to remove deployed refs. The next time the underlying OSTree repo is pruned,
++ * objects which were attached to that ref will be removed. This is useful if you
+  * pulled a flatpak ref using flatpak_installation_install_full() and
+  * specified %FLATPAK_INSTALL_FLAGS_NO_DEPLOY but then decided not to
+  * deploy the ref later on and want to remove the local ref to prevent it
+@@ -2936,7 +2938,7 @@ flatpak_installation_remove_local_ref_sync (FlatpakInstallation *self,
+   if (dir == NULL)
+     return FALSE;
+ 
+-  return flatpak_dir_remove_ref (dir, remote_name, ref, cancellable, error);
++  return flatpak_dir_remove_undeployed_ref (dir, remote_name, ref, cancellable, error);
+ }
+ 
+ /**
+diff --git a/system-helper/flatpak-system-helper.c b/system-helper/flatpak-system-helper.c
+index de56e84..f9ff1aa 100644
+--- a/system-helper/flatpak-system-helper.c
++++ b/system-helper/flatpak-system-helper.c
+@@ -1318,7 +1318,7 @@ handle_remove_local_ref (FlatpakSystemHelper   *object,
+       return G_DBUS_METHOD_INVOCATION_HANDLED;
+     }
+ 
+-  if (!flatpak_dir_remove_ref (system, arg_remote, arg_ref, NULL, &error))
++  if (!flatpak_dir_remove_undeployed_ref (system, arg_remote, arg_ref, NULL, &error))
+     {
+       flatpak_invocation_return_error (invocation, error, "Error removing ref");
+       return G_DBUS_METHOD_INVOCATION_HANDLED;
diff -Nru flatpak-1.16.6/debian/patches/GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-in-chmod.patch flatpak-1.16.6/debian/patches/GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-in-chmod.patch
--- flatpak-1.16.6/debian/patches/GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-in-chmod.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-in-chmod.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,114 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Wed, 29 Jul 2026 21:10:32 +0200
+Subject: revokefs: Avoid symlink path traversal in chmod()
+
+Use glnx_chaseat to ensure that all the attacker-controlled paths end up
+inside the basefd directory, and then AT_SYMLINK_NOFOLLOW to make sure
+that the last path component won't escape from the basefd.
+
+On kernels >= 6.6 we can now use fchmodat() with AT_SYMLINK_NOFOLLOW,
+but on older kernels that didn't work, so if necessary fall back to
+opening the file with O_NOFOLLOW and then calling fchmod() on it.
+
+Because this is the last syscall that used the previous (flawed)
+validation mechanism, we can now remove the validation helpers and be
+sure that everything is using the glnx-chaseat()-based replacements.
+
+[smcv: Separated from a larger commit for better reviewability]
+Co-authored-by: Simon McVittie <smcv@collabora.com>
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-qrwq-7qwx-q9rp
+Origin: upstream, 1.18.1
+---
+ revokefs/writer.c | 69 +++++++++++++++++--------------------------------------
+ 1 file changed, 21 insertions(+), 48 deletions(-)
+
+diff --git a/revokefs/writer.c b/revokefs/writer.c
+index f312c21..e30ade8 100644
+--- a/revokefs/writer.c
++++ b/revokefs/writer.c
+@@ -290,46 +290,6 @@ chase_request_any_and_path (RevokefsRequest *request,
+                           data_size - request->arg1, out_name);
+ }
+ 
+-static gboolean
+-validate_path (char *path)
+-{
+-  char *end_segment;
+-
+-  /* No absolute or empty paths */
+-  if (*path == '/' || *path == 0)
+-    return FALSE;
+-
+-  while (*path != 0)
+-    {
+-      end_segment = strchr (path, '/');
+-      if (end_segment == NULL)
+-        end_segment = path + strlen (path);
+-
+-      if (strncmp (path, "..", 2) == 0)
+-        return FALSE;
+-
+-      path = end_segment;
+-      while (*path == '/')
+-        path++;
+-    }
+-
+-  return TRUE;
+-}
+-
+-static char *
+-get_valid_path (guchar *data, size_t len)
+-{
+-  char *path = g_strndup ((const char *) data, len);
+-
+-  if (!validate_path (path))
+-    {
+-      g_printerr ("Invalid path argument %s\n", path);
+-      exit (1);
+-    }
+-
+-  return path;
+-}
+-
+ static int
+ mask_mode (int mode)
+ {
+@@ -496,17 +456,30 @@ handle_chmod (RevokefsRequest *request,
+               gsize data_size,
+               RevokefsResponse *response)
+ {
+-  g_autofree char *path = get_valid_path (request->data, data_size);
++  g_autofree char *name = NULL;
++  glnx_autofd int parent_fd = chase_request_path (request, data_size, &name);
++  glnx_autofd int fd = -1;
+   int mode = request->arg1;
+ 
+-  /* Note we can't use AT_SYMLINK_NOFOLLOW yet;
+-   * https://marc.info/?l=linux-kernel&m=148830147803162&w=2
+-   * https://marc.info/?l=linux-fsdevel&m=149193779929561&w=2
+-   */
+-  if (fchmodat (basefd, path, mask_mode (mode), 0) != 0)
+-    response->result = -errno;
++  if (fchmodat (parent_fd, name, mask_mode (mode), AT_SYMLINK_NOFOLLOW) == 0)
++    {
++      response->result = 0;
++    }
++  else if (errno == ENOTSUP)
++    {
++      /* Linux < 6.6 didn't implement fchmodat() with AT_SYMLINK_NOFOLLOW */
++      fd = openat (parent_fd, name, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
++      if (fd == -1)
++        response->result = -errno;
++      else if (fchmod (fd, mask_mode (mode)) != 0)
++        response->result = -errno;
++      else
++        response->result = 0;
++    }
+   else
+-    response->result = 0;
++    {
++      response->result = -errno;
++    }
+ 
+   return 0;
+ }
diff -Nru flatpak-1.16.6/debian/patches/GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-in-link-rename.patch flatpak-1.16.6/debian/patches/GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-in-link-rename.patch
--- flatpak-1.16.6/debian/patches/GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-in-link-rename.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-in-link-rename.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,111 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Wed, 29 Jul 2026 21:10:32 +0200
+Subject: revokefs: Avoid symlink path traversal in link(), rename()
+
+Again, use glnx_chaseat to ensure that all the attacker-controlled paths
+end up inside the basefd directory. These two syscalls are a bit more
+complicated, and take two paths.
+
+[smcv: Separated from a larger commit for better reviewability]
+Co-authored-by: Simon McVittie <smcv@collabora.com>
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-qrwq-7qwx-q9rp
+Origin: upstream, 1.18.1
+---
+ revokefs/writer.c | 57 ++++++++++++++++++++++++++++++++-----------------------
+ 1 file changed, 33 insertions(+), 24 deletions(-)
+
+diff --git a/revokefs/writer.c b/revokefs/writer.c
+index a380539..2f07f2d 100644
+--- a/revokefs/writer.c
++++ b/revokefs/writer.c
+@@ -256,6 +256,23 @@ chase_request_path (RevokefsRequest *request,
+   return chase_parent (request->data, data_size, out_name);
+ }
+ 
++static void
++chase_request_2path (RevokefsRequest *request,
++                     gsize data_size,
++                     int *out_fd1, char **out_name1,
++                     int *out_fd2, char **out_name2)
++{
++  if (request->arg1 >= data_size)
++    {
++      g_printerr ("Invalid path1 size\n");
++      exit (1);
++    }
++
++  *out_fd1 = chase_parent (request->data, request->arg1, out_name1);
++  *out_fd2 = chase_parent (request->data + request->arg1,
++                           data_size - request->arg1, out_name2);
++}
++
+ static gboolean
+ validate_path (char *path)
+ {
+@@ -319,22 +336,6 @@ get_any_path_and_valid_path (RevokefsRequest *request,
+   *valid_path2 = get_valid_path (request->data + request->arg1, data_size - request->arg1);
+ }
+ 
+-static void
+-get_valid_2path (RevokefsRequest *request,
+-                 gsize data_size,
+-                 char **path1,
+-                 char **path2)
+-{
+-  if (request->arg1 >= data_size)
+-    {
+-      g_printerr ("Invalid path1 size\n");
+-      exit (1);
+-    }
+-
+-  *path1 = get_valid_path (request->data, request->arg1);
+-  *path2 = get_valid_path (request->data + request->arg1, data_size - request->arg1);
+-}
+-
+ static ssize_t
+ handle_mkdir (RevokefsRequest *request,
+               gsize data_size,
+@@ -432,12 +433,16 @@ handle_link (RevokefsRequest *request,
+              gsize data_size,
+              RevokefsResponse *response)
+ {
+-  g_autofree char *from = NULL;
+-  g_autofree char *to = NULL;
++  g_autofree char *from_name = NULL;
++  g_autofree char *to_name = NULL;
++  glnx_autofd int from_parent_fd = -1;
++  glnx_autofd int to_parent_fd = -1;
+ 
+-  get_valid_2path (request, data_size,  &from, &to);
++  chase_request_2path (request, data_size,
++                       &from_parent_fd, &from_name,
++                       &to_parent_fd, &to_name);
+ 
+-  if (linkat (basefd, from, basefd, to, 0) == -1)
++  if (linkat (from_parent_fd, from_name, to_parent_fd, to_name, 0) == -1)
+     response->result = -errno;
+   else
+     response->result = 0;
+@@ -456,14 +461,18 @@ handle_rename (RevokefsRequest *request,
+              gsize data_size,
+              RevokefsResponse *response)
+ {
+-  g_autofree char *from = NULL;
+-  g_autofree char *to = NULL;
++  g_autofree char *from_name = NULL;
++  g_autofree char *to_name = NULL;
++  glnx_autofd int from_parent_fd = -1;
++  glnx_autofd int to_parent_fd = -1;
+   unsigned int flags;
+ 
+-  get_valid_2path (request, data_size,  &from, &to);
++  chase_request_2path (request, data_size,
++                       &from_parent_fd, &from_name,
++                       &to_parent_fd, &to_name);
+   flags = (unsigned int)request->arg2;
+ 
+-  if (renameat2 (basefd, from, basefd, to, flags) == -1)
++  if (renameat2 (from_parent_fd, from_name, to_parent_fd, to_name, flags) == -1)
+     response->result = -errno;
+   else
+     response->result = 0;
diff -Nru flatpak-1.16.6/debian/patches/GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-in-symlink.patch flatpak-1.16.6/debian/patches/GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-in-symlink.patch
--- flatpak-1.16.6/debian/patches/GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-in-symlink.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-in-symlink.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,87 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Wed, 29 Jul 2026 21:10:32 +0200
+Subject: revokefs: Avoid symlink path traversal in symlink()
+
+Use glnx_chaseat to ensure that the attacker-controlled symlink name
+ends up inside the basefd directory. Note that the symlink *target* is
+also attacker-controlled, but it's OK for them to be able to set any
+target of their choice: that can't immediately cause traversal outside
+the base directory.
+
+[smcv: Separated from a larger commit for better reviewability]
+Co-authored-by: Simon McVittie <smcv@collabora.com>
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-qrwq-7qwx-q9rp
+Origin: upstream, 1.18.1
+---
+ revokefs/writer.c | 42 ++++++++++++++++++++++--------------------
+ 1 file changed, 22 insertions(+), 20 deletions(-)
+
+diff --git a/revokefs/writer.c b/revokefs/writer.c
+index 2f07f2d..f312c21 100644
+--- a/revokefs/writer.c
++++ b/revokefs/writer.c
+@@ -273,6 +273,23 @@ chase_request_2path (RevokefsRequest *request,
+                            data_size - request->arg1, out_name2);
+ }
+ 
++static void
++chase_request_any_and_path (RevokefsRequest *request,
++                            gsize data_size,
++                            char **out_any_path,
++                            int *out_fd, char **out_name)
++{
++  if (request->arg1 >= data_size)
++    {
++      g_printerr ("Invalid path1 size\n");
++      exit (1);
++    }
++
++  *out_any_path = g_strndup ((const char *) request->data, request->arg1);
++  *out_fd = chase_parent (request->data + request->arg1,
++                          data_size - request->arg1, out_name);
++}
++
+ static gboolean
+ validate_path (char *path)
+ {
+@@ -320,22 +337,6 @@ mask_mode (int mode)
+   return mode & ~S_ISUID & ~S_ISGID & ~(S_IWGRP | S_IWOTH);
+ }
+ 
+-static void
+-get_any_path_and_valid_path (RevokefsRequest *request,
+-                             gsize data_size,
+-                             char **any_path1,
+-                             char **valid_path2)
+-{
+-  if (request->arg1 >= data_size)
+-    {
+-      g_printerr ("Invalid path1 size\n");
+-      exit (1);
+-    }
+-
+-  *any_path1 = g_strndup ((const char *) request->data, request->arg1);
+-  *valid_path2 = get_valid_path (request->data + request->arg1, data_size - request->arg1);
+-}
+-
+ static ssize_t
+ handle_mkdir (RevokefsRequest *request,
+               gsize data_size,
+@@ -409,12 +410,13 @@ handle_symlink (RevokefsRequest *request,
+                 RevokefsResponse *response)
+ {
+   g_autofree char *from = NULL;
+-  g_autofree char *to = NULL;
++  g_autofree char *to_name = NULL;
++  glnx_autofd int to_parent_fd = -1;
+ 
+-  /* from doesn't have to be a valid path, it can be absolute or whatever */
+-  get_any_path_and_valid_path (request, data_size,  &from, &to);
++  chase_request_any_and_path (request, data_size,
++                              &from, &to_parent_fd, &to_name);
+ 
+-  if (symlinkat (from, basefd, to) == -1)
++  if (symlinkat (from, to_parent_fd, to_name) == -1)
+     response->result = -errno;
+   else
+     response->result = 0;
diff -Nru flatpak-1.16.6/debian/patches/GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-out-of-basefd-in-si.patch flatpak-1.16.6/debian/patches/GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-out-of-basefd-in-si.patch
--- flatpak-1.16.6/debian/patches/GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-out-of-basefd-in-si.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-out-of-basefd-in-si.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,219 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Wed, 29 Jul 2026 21:10:32 +0200
+Subject: revokefs: Avoid symlink path traversal out of basefd in simple cases
+
+Use glnx_chaseat to ensure that all the attacker-controlled paths end up
+inside the basefd directory.
+
+This requires some new helper functions, which will be used in more
+complicated syscalls' implementations in subsequent commits.
+
+[smcv: Separated from a larger commit for better reviewability]
+Co-authored-by: Simon McVittie <smcv@collabora.com>
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-qrwq-7qwx-q9rp
+Origin: upstream, 1.18.1
+---
+ revokefs/writer.c | 106 +++++++++++++++++++++++++++++++++++++++++++-----------
+ 1 file changed, 85 insertions(+), 21 deletions(-)
+
+diff --git a/revokefs/writer.c b/revokefs/writer.c
+index 2861487..a380539 100644
+--- a/revokefs/writer.c
++++ b/revokefs/writer.c
+@@ -198,6 +198,64 @@ request_path_path (int writer_socket, RevokefsOps op, const char *path1, const c
+   return request_path_data (writer_socket, op, path1, path2, strlen(path2), 0);
+ }
+ 
++/* Return an fd to the parent directory of path guaranteed to be under
++ * basefd, and set *out_name to the final path component. */
++static int
++chase_parent (guchar *data, size_t len, char **out_name)
++{
++  g_autofree char *path = g_strndup ((const char *) data, len);
++  g_autofree char *parent = NULL;
++  g_autofree char *name = NULL;
++  const char *last_slash;
++  int fd;
++
++  if (*path == '/' || *path == 0)
++    {
++      g_printerr ("Invalid path: %s\n", path);
++      exit (1);
++    }
++
++  last_slash = strrchr (path, '/');
++  if (last_slash)
++    {
++      parent = g_strndup (path, last_slash - path);
++      name = g_strdup (last_slash + 1);
++    }
++  else
++    {
++      parent = g_strdup (".");
++      name = g_strdup (path);
++    }
++
++  if (name[0] == 0 || strcmp (name, ".") == 0 || strcmp (name, "..") == 0)
++    {
++      g_printerr ("Invalid path: %s\n", path);
++      exit (1);
++    }
++
++  fd = glnx_chaseat (basefd, parent,
++                      GLNX_CHASE_RESOLVE_BENEATH |
++                      GLNX_CHASE_MUST_BE_DIRECTORY,
++                      NULL);
++  if (fd < 0)
++    {
++      g_printerr ("Path escapes base directory: %s\n", path);
++      exit (1);
++    }
++
++  if (out_name != NULL)
++    *out_name = g_steal_pointer (&name);
++  return fd;
++}
++
++static int
++chase_request_path (RevokefsRequest *request,
++                    gsize data_size,
++                    char **out_name)
++{
++  return chase_parent (request->data, data_size, out_name);
++}
++
+ static gboolean
+ validate_path (char *path)
+ {
+@@ -282,10 +340,11 @@ handle_mkdir (RevokefsRequest *request,
+               gsize data_size,
+               RevokefsResponse *response)
+ {
+-  g_autofree char *path = get_valid_path (request->data, data_size);
++  g_autofree char *name = NULL;
++  glnx_autofd int parent_fd = chase_request_path (request, data_size, &name);
+   int mode = request->arg1;
+ 
+-  if (mkdirat (basefd, path, mask_mode (mode)) == -1)
++  if (mkdirat (parent_fd, name, mask_mode (mode)) == -1)
+     response->result = -errno;
+   else
+     response->result = 0;
+@@ -304,9 +363,10 @@ handle_rmdir (RevokefsRequest *request,
+               gsize data_size,
+               RevokefsResponse *response)
+ {
+-  g_autofree char *path = get_valid_path (request->data, data_size);
++  g_autofree char *name = NULL;
++  glnx_autofd int parent_fd = chase_request_path (request, data_size, &name);
+ 
+-  if (unlinkat (basefd, path, AT_REMOVEDIR) == -1)
++  if (unlinkat (parent_fd, name, AT_REMOVEDIR) == -1)
+     response->result = -errno;
+   else
+     response->result = 0;
+@@ -325,9 +385,10 @@ handle_unlink (RevokefsRequest *request,
+               gsize data_size,
+               RevokefsResponse *response)
+ {
+-  g_autofree char *path = get_valid_path (request->data, data_size);
++  g_autofree char *name = NULL;
++  glnx_autofd int parent_fd = chase_request_path (request, data_size, &name);
+ 
+-  if (unlinkat (basefd, path, 0) == -1)
++  if (unlinkat (parent_fd, name, 0) == -1)
+     response->result = -errno;
+   else
+     response->result = 0;
+@@ -450,11 +511,12 @@ handle_chown (RevokefsRequest *request,
+               gsize data_size,
+               RevokefsResponse *response)
+ {
+-  g_autofree char *path = get_valid_path (request->data, data_size);
++  g_autofree char *name = NULL;
++  glnx_autofd int parent_fd = chase_request_path (request, data_size, &name);
+   uid_t uid = request->arg1;
+   gid_t gid = request->arg2;
+ 
+-  if (fchownat (basefd, path, uid, gid, AT_SYMLINK_NOFOLLOW) != 0)
++  if (fchownat (parent_fd, name, uid, gid, AT_SYMLINK_NOFOLLOW) != 0)
+     response->result = -errno;
+   else
+     response->result = 0;
+@@ -473,10 +535,12 @@ handle_truncate (RevokefsRequest *request,
+                  gsize data_size,
+                  RevokefsResponse *response)
+ {
+-  g_autofree char *path = get_valid_path (request->data, data_size);
++  g_autofree char *name = NULL;
++  glnx_autofd int parent_fd = chase_request_path (request, data_size, &name);
+   off_t size = request->arg1;
++  glnx_autofd int fd = -1;
+ 
+-  glnx_autofd int fd = openat (basefd, path, O_NOFOLLOW|O_WRONLY);
++  fd = openat (parent_fd, name, O_NOFOLLOW | O_WRONLY);
+   if (fd == -1)
+     response->result = -errno;
+   else
+@@ -501,7 +565,8 @@ handle_utimens (RevokefsRequest *request,
+                 gsize data_size,
+                 RevokefsResponse *response)
+ {
+-  g_autofree char *path = NULL;
++  g_autofree char *name = NULL;
++  glnx_autofd int parent_fd = -1;
+   struct timespec *tv;
+ 
+   if (request->arg1 + sizeof (struct timespec) * 2 != data_size)
+@@ -510,10 +575,10 @@ handle_utimens (RevokefsRequest *request,
+       exit (1);
+     }
+ 
+-  path = get_valid_path (request->data, request->arg1);
++  parent_fd = chase_request_path (request, request->arg1, &name);
+   tv = (struct timespec *)(request->data + request->arg1);
+ 
+-  if (utimensat (basefd, path, tv, AT_SYMLINK_NOFOLLOW) == -1)
++  if (utimensat (parent_fd, name, tv, AT_SYMLINK_NOFOLLOW) == -1)
+     response->result = -errno;
+   else
+     response->result = 0;
+@@ -533,16 +598,14 @@ handle_open (RevokefsRequest *request,
+              gsize data_size,
+              RevokefsResponse *response)
+ {
+-  g_autofree char *path = get_valid_path (request->data, data_size);
++  g_autofree char *name = NULL;
++  glnx_autofd int parent_fd = chase_request_path (request, data_size, &name);
+   int mode = request->arg1;
+   int flags = request->arg2;
+   int fd;
+ 
+-
+-  /* We need to specially handle O_TRUNC. Also, Fuse should have already
+-   * resolved symlinks, but use O_NOFOLLOW to be safe to avoid following
+-   * symlinks to some other filesystem. */
+-  fd = openat (basefd, path, (flags & ~O_TRUNC) | O_NOFOLLOW, mask_mode (mode));
++  /* We handle O_TRUNC separately, a bit later */
++  fd = openat (parent_fd, name, (flags & ~O_TRUNC) | O_NOFOLLOW, mask_mode (mode));
+   if (fd == -1)
+     response->result = -errno;
+   else
+@@ -747,14 +810,15 @@ handle_access (RevokefsRequest *request,
+                gsize data_size,
+                RevokefsResponse *response)
+ {
+-  g_autofree char *path = get_valid_path (request->data, data_size);
++  g_autofree char *name = NULL;
++  glnx_autofd int parent_fd = chase_request_path (request, data_size, &name);
+   int mode = request->arg1;
+ 
+   /* Apparently at least GNU coreutils rm calls `faccessat(W_OK)`
+    * before trying to do an unlink.  So...we'll just lie about
+    * writable access here.
+    */
+-  if (faccessat (basefd, path, mode, AT_SYMLINK_NOFOLLOW) == -1)
++  if (faccessat (parent_fd, name, mode, AT_SYMLINK_NOFOLLOW) == -1)
+     response->result = -errno;
+   else
+     response->result = 0;
diff -Nru flatpak-1.16.6/debian/patches/GHSA-r7hp-698j-2h6c/run-dbus-Correct-broadcast-rules-for-the-AT-SPI-bus.patch flatpak-1.16.6/debian/patches/GHSA-r7hp-698j-2h6c/run-dbus-Correct-broadcast-rules-for-the-AT-SPI-bus.patch
--- flatpak-1.16.6/debian/patches/GHSA-r7hp-698j-2h6c/run-dbus-Correct-broadcast-rules-for-the-AT-SPI-bus.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-r7hp-698j-2h6c/run-dbus-Correct-broadcast-rules-for-the-AT-SPI-bus.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,34 @@
+From: Simon McVittie <smcv@collabora.com>
+Date: Sun, 26 Jul 2026 15:23:32 +0100
+Subject: run-dbus: Correct --broadcast rules for the AT-SPI bus
+
+These had the *INTERFACE*.*METHOD* where the bus name should have been,
+and a blank interface/method (meaning match any interface/method).
+Because there's no bus name of that name on the AT-SPI bus, the only reason
+why Flatpak apps were able to receive these broadcasts is that there was
+a vulnerability in xdg-dbus-proxy, tracked as GHSA-r7hp-698j-2h6c. To
+avoid regressions in Flatpak when the x-d-p vulnerability is fixed,
+we need to correct these rules to have the intended bus name.
+
+Signed-off-by: Simon McVittie <smcv@collabora.com>
+Bug: https://github.com/flatpak/xdg-dbus-proxy/security/advisories/GHSA-r7hp-698j-2h6c
+Origin: upstream, 1.18.1
+---
+ common/flatpak-run-dbus.c | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/common/flatpak-run-dbus.c b/common/flatpak-run-dbus.c
+index 20f0231..1d60c14 100644
+--- a/common/flatpak-run-dbus.c
++++ b/common/flatpak-run-dbus.c
+@@ -451,8 +451,8 @@ flatpak_run_add_a11y_dbus_args (FlatpakBwrap    *app_bwrap,
+   flatpak_bwrap_add_args (proxy_arg_bwrap,
+                           a11y_address,
+                           proxy_socket, "--filter", "--sloppy-names",
+-                          "--broadcast=org.a11y.atspi.Registry.EventListenerRegistered=@/org/a11y/atspi/registry",
+-                          "--broadcast=org.a11y.atspi.Registry.EventListenerDeregistered=@/org/a11y/atspi/registry",
++                          "--broadcast=org.a11y.atspi.Registry=org.a11y.atspi.Registry.EventListenerRegistered@/org/a11y/atspi/registry",
++                          "--broadcast=org.a11y.atspi.Registry=org.a11y.atspi.Registry.EventListenerDeregistered@/org/a11y/atspi/registry",
+                           "--call=org.a11y.atspi.Registry=org.a11y.atspi.Socket.Embed@/org/a11y/atspi/accessible/root",
+                           "--call=org.a11y.atspi.Registry=org.a11y.atspi.Socket.Unembed@/org/a11y/atspi/accessible/root",
+                           "--call=org.a11y.atspi.Registry=org.a11y.atspi.Registry.GetRegisteredEvents@/org/a11y/atspi/registry",
diff -Nru flatpak-1.16.6/debian/patches/GHSA-v2gw-v9h5-9q4x/ref-utils-Make-remote-name-validation-public-and-take-an-.patch flatpak-1.16.6/debian/patches/GHSA-v2gw-v9h5-9q4x/ref-utils-Make-remote-name-validation-public-and-take-an-.patch
--- flatpak-1.16.6/debian/patches/GHSA-v2gw-v9h5-9q4x/ref-utils-Make-remote-name-validation-public-and-take-an-.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-v2gw-v9h5-9q4x/ref-utils-Make-remote-name-validation-public-and-take-an-.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,96 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Mon, 3 Aug 2026 22:04:03 +0200
+Subject: ref-utils: Make remote name validation public and take an error
+
+We should be consistent with the validation in the session-helper, so
+let's make this validation function available to it.
+
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-v2gw-v9h5-9q4x
+Origin: upstream, 1.18.1
+---
+ common/flatpak-ref-utils-private.h |  3 +++
+ common/flatpak-ref-utils.c         | 43 ++++++++++++++++++++++++++------------
+ 2 files changed, 33 insertions(+), 13 deletions(-)
+
+diff --git a/common/flatpak-ref-utils-private.h b/common/flatpak-ref-utils-private.h
+index 74d91bd..7f3e363 100644
+--- a/common/flatpak-ref-utils-private.h
++++ b/common/flatpak-ref-utils-private.h
+@@ -33,6 +33,9 @@ gboolean flatpak_is_valid_branch (const char *string,
+ gboolean flatpak_is_valid_arch (const char *string,
+                                 gssize      len,
+                                 GError    **error);
++gboolean flatpak_is_valid_remote_name (const char *string,
++                                       gssize      len,
++                                       GError    **error);
+ gboolean flatpak_has_name_prefix (const char *string,
+                                   const char *name);
+ gboolean flatpak_name_matches_one_wildcard_prefix (const char         *string,
+diff --git a/common/flatpak-ref-utils.c b/common/flatpak-ref-utils.c
+index 3e3b015..aabc8f5 100644
+--- a/common/flatpak-ref-utils.c
++++ b/common/flatpak-ref-utils.c
+@@ -567,25 +567,42 @@ is_valid_remote_name_character (gint c)
+     c == '.';
+ }
+ 
+-static gboolean
+-is_valid_remote_name (const char *remote,
+-                      gsize len)
++gboolean
++flatpak_is_valid_remote_name (const char *string,
++                              gssize      len,
++                              GError    **error)
+ {
+   const char *end;
+ 
+-  if (len == 0)
+-    return FALSE;
++  if (len < 0)
++    len = strlen (string);
+ 
+-  end = remote + len;
++  if (G_UNLIKELY (len == 0))
++    {
++      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME,
++                          _("Remote name can't be empty"));
++      return FALSE;
++    }
+ 
+-  if (!is_valid_initial_remote_name_character (*remote++))
+-    return FALSE;
++  end = string + len;
+ 
+-  while (remote < end)
++  if (G_UNLIKELY (!is_valid_initial_remote_name_character (*string)))
+     {
+-      char c = *remote++;
+-      if (!is_valid_remote_name_character (c))
+-        return FALSE;
++      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME,
++                          _("Remote name can't start with %c"), *string);
++      return FALSE;
++    }
++  string++;
++
++  while (string < end)
++    {
++      if (G_UNLIKELY (!is_valid_remote_name_character (*string)))
++        {
++          flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME,
++                              _("Remote name can't contain %c"), *string);
++          return FALSE;
++        }
++      string++;
+     }
+ 
+   return TRUE;
+@@ -622,7 +639,7 @@ _flatpak_decomposed_new (char            *ref,
+       const char *colon = strchr (p, ':');
+       if (colon != NULL)
+         {
+-          if (!is_valid_remote_name (ref, colon - ref))
++          if (!flatpak_is_valid_remote_name (ref, colon - ref, NULL))
+             {
+               flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Invalid remote name"));
+               return NULL;
diff -Nru flatpak-1.16.6/debian/patches/GHSA-v2gw-v9h5-9q4x/system-helper-Validate-each-remote-name-and-architecture-.patch flatpak-1.16.6/debian/patches/GHSA-v2gw-v9h5-9q4x/system-helper-Validate-each-remote-name-and-architecture-.patch
--- flatpak-1.16.6/debian/patches/GHSA-v2gw-v9h5-9q4x/system-helper-Validate-each-remote-name-and-architecture-.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-v2gw-v9h5-9q4x/system-helper-Validate-each-remote-name-and-architecture-.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,139 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Sun, 9 Aug 2026 12:10:11 +0100
+Subject: system-helper: Validate each remote name and architecture argument
+
+We pass them on to internal functions which assume that they are valid,
+and specifically also create paths which contain those strings which can
+be used for path traversal attacks.
+
+Let's simply consistently validate all of those arguments.
+
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-v2gw-v9h5-9q4x
+[backported to 1.18.x: previous incomplete validity checks weren't on this branch]
+Origin: upstream, 1.18.1
+---
+ system-helper/flatpak-system-helper.c | 59 +++++++++++++++++++++++++----------
+ 1 file changed, 43 insertions(+), 16 deletions(-)
+
+diff --git a/system-helper/flatpak-system-helper.c b/system-helper/flatpak-system-helper.c
+index f709f94..de56e84 100644
+--- a/system-helper/flatpak-system-helper.c
++++ b/system-helper/flatpak-system-helper.c
+@@ -432,6 +432,19 @@ handle_deploy (FlatpakSystemHelper   *object,
+       return G_DBUS_METHOD_INVOCATION_HANDLED;
+     }
+ 
++  ref = flatpak_decomposed_new_from_ref (arg_ref, &error);
++  if (ref == NULL)
++    {
++      g_dbus_method_invocation_return_gerror (invocation, error);
++      return G_DBUS_METHOD_INVOCATION_HANDLED;
++    }
++
++  if (!flatpak_is_valid_remote_name (arg_origin, -1, &error))
++    {
++      g_dbus_method_invocation_return_gerror (invocation, error);
++      return G_DBUS_METHOD_INVOCATION_HANDLED;
++    }
++
+   if (strlen (arg_repo_path) > 0)
+     {
+       g_autoptr(GError) local_error = NULL;
+@@ -475,13 +488,6 @@ handle_deploy (FlatpakSystemHelper   *object,
+         }
+     }
+ 
+-  ref = flatpak_decomposed_new_from_ref (arg_ref, &error);
+-  if (ref == NULL)
+-    {
+-      g_dbus_method_invocation_return_gerror (invocation, error);
+-      return G_DBUS_METHOD_INVOCATION_HANDLED;
+-    }
+-
+   no_deploy = (arg_flags & FLATPAK_HELPER_DEPLOY_FLAGS_NO_DEPLOY) != 0;
+   local_pull = (arg_flags & FLATPAK_HELPER_DEPLOY_FLAGS_LOCAL_PULL) != 0;
+   reinstall = (arg_flags & FLATPAK_HELPER_DEPLOY_FLAGS_REINSTALL) != 0;
+@@ -783,6 +789,18 @@ handle_deploy_appstream (FlatpakSystemHelper   *object,
+       return G_DBUS_METHOD_INVOCATION_HANDLED;
+     }
+ 
++  if (!flatpak_is_valid_remote_name (arg_origin, -1, &error))
++    {
++      g_dbus_method_invocation_return_gerror (invocation, error);
++      return G_DBUS_METHOD_INVOCATION_HANDLED;
++    }
++
++  if (!flatpak_is_valid_arch (arg_arch, -1, &error))
++    {
++      g_dbus_method_invocation_return_gerror (invocation, error);
++      return G_DBUS_METHOD_INVOCATION_HANDLED;
++    }
++
+   if (strlen (arg_repo_path) > 0)
+     {
+       g_autoptr(GFile) repo_file = g_file_new_for_path (arg_repo_path);
+@@ -1012,6 +1030,12 @@ handle_install_bundle (FlatpakSystemHelper   *object,
+       return G_DBUS_METHOD_INVOCATION_HANDLED;
+     }
+ 
++  if (!flatpak_is_valid_remote_name (arg_remote, -1, &error))
++    {
++      g_dbus_method_invocation_return_gerror (invocation, error);
++      return G_DBUS_METHOD_INVOCATION_HANDLED;
++    }
++
+   if (!g_file_query_exists (bundle_file, NULL))
+     {
+       g_dbus_method_invocation_return_error (invocation, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
+@@ -1057,10 +1081,9 @@ handle_configure_remote (FlatpakSystemHelper   *object,
+       return G_DBUS_METHOD_INVOCATION_HANDLED;
+     }
+ 
+-  if (*arg_remote == 0 || strchr (arg_remote, '/') != NULL)
++  if (!flatpak_is_valid_remote_name (arg_remote, -1, &error))
+     {
+-      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+-                                             "Invalid remote name: %s", arg_remote);
++      g_dbus_method_invocation_return_gerror (invocation, error);
+       return G_DBUS_METHOD_INVOCATION_HANDLED;
+     }
+ 
+@@ -1198,10 +1221,9 @@ handle_update_remote (FlatpakSystemHelper   *object,
+       return G_DBUS_METHOD_INVOCATION_HANDLED;
+     }
+ 
+-  if (*arg_remote == 0 || strchr (arg_remote, '/') != NULL)
++  if (!flatpak_is_valid_remote_name (arg_remote, -1, &error))
+     {
+-      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+-                                             "Invalid remote name: %s", arg_remote);
++      g_dbus_method_invocation_return_gerror (invocation, error);
+       return G_DBUS_METHOD_INVOCATION_HANDLED;
+     }
+ 
+@@ -1284,10 +1306,9 @@ handle_remove_local_ref (FlatpakSystemHelper   *object,
+       return G_DBUS_METHOD_INVOCATION_HANDLED;
+     }
+ 
+-  if (*arg_remote == 0 || strchr (arg_remote, '/') != NULL)
++  if (!flatpak_is_valid_remote_name (arg_remote, -1, &error))
+     {
+-      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+-                                             "Invalid remote name: %s", arg_remote);
++      g_dbus_method_invocation_return_gerror (invocation, error);
+       return G_DBUS_METHOD_INVOCATION_HANDLED;
+     }
+ 
+@@ -1851,6 +1872,12 @@ handle_generate_oci_summary (FlatpakSystemHelper   *object,
+       return G_DBUS_METHOD_INVOCATION_HANDLED;
+     }
+ 
++  if (!flatpak_is_valid_remote_name (arg_origin, -1, &error))
++    {
++      g_dbus_method_invocation_return_gerror (invocation, error);
++      return G_DBUS_METHOD_INVOCATION_HANDLED;
++    }
++
+   only_cached = (arg_flags & FLATPAK_HELPER_GENERATE_OCI_SUMMARY_FLAGS_ONLY_CACHED) != 0;
+ 
+   if (!flatpak_dir_ensure_repo (system, NULL, &error))
diff -Nru flatpak-1.16.6/debian/patches/GHSA-v2gw-v9h5-9q4x/tests-Add-some-unit-tests-for-ref-utils-name-component-va.patch flatpak-1.16.6/debian/patches/GHSA-v2gw-v9h5-9q4x/tests-Add-some-unit-tests-for-ref-utils-name-component-va.patch
--- flatpak-1.16.6/debian/patches/GHSA-v2gw-v9h5-9q4x/tests-Add-some-unit-tests-for-ref-utils-name-component-va.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-v2gw-v9h5-9q4x/tests-Add-some-unit-tests-for-ref-utils-name-component-va.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,394 @@
+From: Simon McVittie <smcv@collabora.com>
+Date: Tue, 4 Aug 2026 11:21:20 +0100
+Subject: tests: Add some unit tests for ref-utils name component validation
+
+In the fix for GHSA-v2gw-v9h5-9q4x these functions are used to avoid
+path traversal, so we'd better assert that they do detect and avoid it.
+
+Signed-off-by: Simon McVittie <smcv@collabora.com>
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-v2gw-v9h5-9q4x
+Origin: upstream, 1.18.1
+---
+ tests/meson.build      |   1 +
+ tests/test-ref-utils.c | 360 +++++++++++++++++++++++++++++++++++++++++++++++++
+ 2 files changed, 361 insertions(+)
+ create mode 100644 tests/test-ref-utils.c
+
+diff --git a/tests/meson.build b/tests/meson.build
+index 464ed0b..8a9db82 100644
+--- a/tests/meson.build
++++ b/tests/meson.build
+@@ -127,6 +127,7 @@ c_tests = [
+       portal_gdbus[1],
+     ],
+   }],
++  ['test-ref-utils', {}],
+ ]
+ 
+ foreach testcase : c_tests
+diff --git a/tests/test-ref-utils.c b/tests/test-ref-utils.c
+new file mode 100644
+index 0000000..845e41f
+--- /dev/null
++++ b/tests/test-ref-utils.c
+@@ -0,0 +1,360 @@
++/* vi:set et sw=2 sts=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e-s:
++ * Copyright © 2020-2026 Collabora Ltd.
++ * SPDX-License-Identifier: LGPL-2.1-or-later
++ */
++
++#include "config.h"
++
++#include <glib.h>
++
++#include "flatpak.h"
++#include "flatpak-ref-utils-private.h"
++
++#include "tests/testlib.h"
++
++static void
++test_valid_arch (void)
++{
++  static const char * const good[] =
++  {
++    "i386",
++    "mips64el",
++    "x86_64",
++  };
++  static const char * const bad[] =
++  {
++    "",
++    "--overrule-some-option",
++    ".",
++    "..",
++    "a/b",
++    "x86-64",
++    "../path_traversal",
++    "path/../traversal",
++    "path_traversal/..",
++    "\xc3\xa1",   /* U+00E1 LATIN SMALL LETTER A WITH ACUTE */
++    "a\xc3\xa1",
++  };
++  static const struct
++  {
++    const char *name;
++    int len;
++  }
++  initially_good[] =
++  {
++    { "abc\xc3\xa1", 3 },
++    { "_/", 1 },
++  };
++
++  for (size_t i = 0; i < G_N_ELEMENTS (good); i++)
++    {
++      g_autoptr(GError) local_error = NULL;
++      gboolean ok = flatpak_is_valid_arch (good[i], -1, &local_error);
++
++      g_test_message ("good architecture \"%s\" -> %s",
++                      good[i], ok ? "OK" : local_error->message);
++      g_assert_no_error (local_error);
++      g_assert_true (ok);
++    }
++
++  for (size_t i = 0; i < G_N_ELEMENTS (initially_good); i++)
++    {
++      g_autoptr(GError) local_error = NULL;
++      gboolean ok = flatpak_is_valid_arch (initially_good[i].name,
++                                           initially_good[i].len,
++                                           &local_error);
++
++      g_test_message ("good architecture \"%.*s\" -> %s",
++                      initially_good[i].len,
++                      initially_good[i].name,
++                      ok ? "OK" : local_error->message);
++      g_assert_no_error (local_error);
++      g_assert_true (ok);
++    }
++
++  for (size_t i = 0; i < G_N_ELEMENTS (bad); i++)
++    {
++      g_autoptr(GError) local_error = NULL;
++      gboolean ok = flatpak_is_valid_arch (bad[i], -1, &local_error);
++
++      g_test_message ("bad architecture \"%s\" -> %s",
++                      bad[i], ok ? "wrongly accepted" : local_error->message);
++      g_assert_nonnull (local_error);
++      g_assert_false (ok);
++    }
++
++    {
++      g_autoptr(GError) local_error = NULL;
++      gboolean ok = flatpak_is_valid_arch (good[0], 0, &local_error);
++
++      g_test_message ("empty architecture -> %s",
++                      ok ? "wrongly accepted" : local_error->message);
++      g_assert_nonnull (local_error);
++      g_assert_false (ok);
++    }
++}
++
++static void
++test_valid_branch (void)
++{
++  static const char * const good[] =
++  {
++    "stable",
++    "private-beta",
++    "public_beta",
++    "1.0",
++    "--whatever--",
++    "x",
++  };
++  static const char * const bad[] =
++  {
++    "",
++    ".",
++    "..",
++    "a/b",
++    "../path-traversal",
++    "path-traversal/..",
++    "path/../traversal",
++    "\xc3\xa1",   /* U+00E1 LATIN SMALL LETTER A WITH ACUTE */
++    "a\xc3\xa1",
++  };
++  static const struct
++  {
++    const char *name;
++    int len;
++  }
++  initially_good[] =
++  {
++    { "abc\xc3\xa1", 3 },
++    { "x/", 1 },
++  };
++
++  for (size_t i = 0; i < G_N_ELEMENTS (good); i++)
++    {
++      g_autoptr(GError) local_error = NULL;
++      gboolean ok = flatpak_is_valid_branch (good[i], -1, &local_error);
++
++      g_test_message ("good branch \"%s\" -> %s",
++                      good[i], ok ? "OK" : local_error->message);
++      g_assert_no_error (local_error);
++      g_assert_true (ok);
++    }
++
++  for (size_t i = 0; i < G_N_ELEMENTS (initially_good); i++)
++    {
++      g_autoptr(GError) local_error = NULL;
++      gboolean ok = flatpak_is_valid_branch (initially_good[i].name,
++                                             initially_good[i].len,
++                                             &local_error);
++
++      g_test_message ("good branch \"%.*s\" -> %s",
++                      initially_good[i].len,
++                      initially_good[i].name,
++                      ok ? "OK" : local_error->message);
++      g_assert_no_error (local_error);
++      g_assert_true (ok);
++    }
++
++  for (size_t i = 0; i < G_N_ELEMENTS (bad); i++)
++    {
++      g_autoptr(GError) local_error = NULL;
++      gboolean ok = flatpak_is_valid_branch (bad[i], -1, &local_error);
++
++      g_test_message ("bad branch \"%s\" -> %s",
++                      bad[i], ok ? "wrongly accepted" : local_error->message);
++      g_assert_nonnull (local_error);
++      g_assert_false (ok);
++    }
++
++    {
++      g_autoptr(GError) local_error = NULL;
++      gboolean ok = flatpak_is_valid_branch (good[0], 0, &local_error);
++
++      g_test_message ("empty branch -> %s",
++                      ok ? "wrongly accepted" : local_error->message);
++      g_assert_nonnull (local_error);
++      g_assert_false (ok);
++    }
++}
++
++#define NAME_CHAR_x16 "z12345678.abcdef"
++#define NAME_CHAR_x32 NAME_CHAR_x16 NAME_CHAR_x16
++#define NAME_CHAR_x64 NAME_CHAR_x32 NAME_CHAR_x32
++#define NAME_CHAR_x128 NAME_CHAR_x64 NAME_CHAR_x64
++#define NAME_CHAR_x256 NAME_CHAR_x128 NAME_CHAR_x128
++
++static void
++test_valid_name (void)
++{
++  static const char * const good[] =
++  {
++    "com.example.Foo",
++    "a.b.c",
++    "org._7_zip.Archiver",
++    "org._7_zip._7-zip",    /* "-" in last element discouraged but allowed */
++    "uk.co.pseudorandom.name.has.many.components",
++  };
++  static const char * const bad[] =
++  {
++    "",
++    "a.b",        /* not enough elements */
++    "org.7_zip.Archiver",
++    "org._7-zip.Archiver",
++    "\xc3\xa1",   /* U+00E1 LATIN SMALL LETTER A WITH ACUTE */
++    "a\xc3\xa1",
++    NAME_CHAR_x256,
++  };
++  static const struct
++  {
++    const char *name;
++    int len;
++  }
++  initially_good[] =
++  {
++    { "a.b.c\xc3\xa1", 5 },
++    { NAME_CHAR_x256, 255 },
++  };
++
++  for (size_t i = 0; i < G_N_ELEMENTS (good); i++)
++    {
++      g_autoptr(GError) local_error = NULL;
++      gboolean ok = flatpak_is_valid_name (good[i], -1, &local_error);
++
++      g_test_message ("good app name \"%s\" -> %s",
++                      good[i], ok ? "OK" : local_error->message);
++      g_assert_no_error (local_error);
++      g_assert_true (ok);
++    }
++
++  for (size_t i = 0; i < G_N_ELEMENTS (initially_good); i++)
++    {
++      g_autoptr(GError) local_error = NULL;
++      gboolean ok = flatpak_is_valid_name (initially_good[i].name,
++                                           initially_good[i].len,
++                                           &local_error);
++
++      g_test_message ("good app name \"%.*s\" -> %s",
++                      initially_good[i].len,
++                      initially_good[i].name,
++                      ok ? "OK" : local_error->message);
++      g_assert_no_error (local_error);
++      g_assert_true (ok);
++    }
++
++  for (size_t i = 0; i < G_N_ELEMENTS (bad); i++)
++    {
++      g_autoptr(GError) local_error = NULL;
++      gboolean ok = flatpak_is_valid_name (bad[i], -1, &local_error);
++
++      g_test_message ("bad app name \"%s\" -> %s",
++                      bad[i], ok ? "wrongly accepted" : local_error->message);
++      g_assert_nonnull (local_error);
++      g_assert_false (ok);
++    }
++
++    {
++      g_autoptr(GError) local_error = NULL;
++      gboolean ok = flatpak_is_valid_name (good[0], 0, &local_error);
++
++      g_test_message ("empty app name -> %s",
++                      ok ? "wrongly accepted" : local_error->message);
++      g_assert_nonnull (local_error);
++      g_assert_false (ok);
++    }
++}
++
++static void
++test_valid_remote_name (void)
++{
++  static const char * const good[] =
++  {
++    "7zip",
++    "_abc123",
++    "a",
++    "a_b",
++    "remote-1.0",
++  };
++  static const char * const bad[] =
++  {
++    "",
++    "--overrule-some-option",
++    ".",
++    "..",
++    "a/b",
++    "../path-traversal",
++    "path/../traversal",
++    "path-traversal/..",
++    "\xc3\xa1",   /* U+00E1 LATIN SMALL LETTER A WITH ACUTE */
++    "a\xc3\xa1",
++  };
++  static const struct
++  {
++    const char *name;
++    int len;
++  }
++  initially_good[] =
++  {
++    { "abc\xc3\xa1", 3 },
++    { "x/", 1 },
++  };
++
++  for (size_t i = 0; i < G_N_ELEMENTS (good); i++)
++    {
++      g_autoptr(GError) local_error = NULL;
++      gboolean ok = flatpak_is_valid_remote_name (good[i], -1, &local_error);
++
++      g_test_message ("good remote name \"%s\" -> %s",
++                      good[i], ok ? "OK" : local_error->message);
++      g_assert_no_error (local_error);
++      g_assert_true (ok);
++    }
++
++  for (size_t i = 0; i < G_N_ELEMENTS (initially_good); i++)
++    {
++      g_autoptr(GError) local_error = NULL;
++      gboolean ok = flatpak_is_valid_remote_name (initially_good[i].name,
++                                                  initially_good[i].len,
++                                                  &local_error);
++
++      g_test_message ("good remote name \"%.*s\" -> %s",
++                      initially_good[i].len,
++                      initially_good[i].name,
++                      ok ? "OK" : local_error->message);
++      g_assert_no_error (local_error);
++      g_assert_true (ok);
++    }
++
++  for (size_t i = 0; i < G_N_ELEMENTS (bad); i++)
++    {
++      g_autoptr(GError) local_error = NULL;
++      gboolean ok = flatpak_is_valid_remote_name (bad[i], -1, &local_error);
++
++      g_test_message ("bad remote name \"%s\" -> %s",
++                      bad[i], ok ? "wrongly accepted" : local_error->message);
++      g_assert_nonnull (local_error);
++      g_assert_false (ok);
++    }
++
++    {
++      g_autoptr(GError) local_error = NULL;
++      gboolean ok = flatpak_is_valid_remote_name (good[0], 0, &local_error);
++
++      g_test_message ("empty remote name -> %s",
++                      ok ? "wrongly accepted" : local_error->message);
++      g_assert_nonnull (local_error);
++      g_assert_false (ok);
++    }
++}
++
++int
++main (int argc, char *argv[])
++{
++  g_test_init (&argc, &argv, NULL);
++
++  g_test_add_func ("/ref-utils/valid-arch", test_valid_arch);
++  g_test_add_func ("/ref-utils/valid-branch", test_valid_branch);
++  g_test_add_func ("/ref-utils/valid-name", test_valid_name);
++  g_test_add_func ("/ref-utils/valid-remote-name", test_valid_remote_name);
++
++  return g_test_run ();
++}
diff -Nru flatpak-1.16.6/debian/patches/GHSA-w69g-9x8j-7p8f/dir-utils-Reject-path-traversal-in-extension-metadata.patch flatpak-1.16.6/debian/patches/GHSA-w69g-9x8j-7p8f/dir-utils-Reject-path-traversal-in-extension-metadata.patch
--- flatpak-1.16.6/debian/patches/GHSA-w69g-9x8j-7p8f/dir-utils-Reject-path-traversal-in-extension-metadata.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-w69g-9x8j-7p8f/dir-utils-Reject-path-traversal-in-extension-metadata.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,92 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Mon, 29 Jun 2026 21:51:46 +0200
+Subject: dir-utils: Reject path traversal in extension metadata
+
+Validate extension directory, subdir_suffix, add_ld_path, and
+merge_dirs at parse time, rejecting absolute paths and paths with
+.. components that escape the intended mount point. This prevents
+extension metadata from mounting content at unintended locations
+inside the sandbox.
+
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-w69g-9x8j-7p8f
+Origin: upstream, 1.18.1
+---
+ common/flatpak-dir-utils.c | 51 ++++++++++++++++++++++++++++++++++++++++++++--
+ 1 file changed, 49 insertions(+), 2 deletions(-)
+
+diff --git a/common/flatpak-dir-utils.c b/common/flatpak-dir-utils.c
+index e34b950..807fbc5 100644
+--- a/common/flatpak-dir-utils.c
++++ b/common/flatpak-dir-utils.c
+@@ -24,6 +24,7 @@
+ #include "flatpak-dir-private.h"
+ #include "flatpak-metadata-private.h"
+ #include "flatpak-utils-private.h"
++#include "flatpak-utils-base-private.h"
+ 
+ char **
+ flatpak_list_deployed_refs (const char   *type,
+@@ -393,6 +394,21 @@ flatpak_extension_new (const char        *id,
+   return ext;
+ }
+ 
++static gboolean
++relative_path_has_no_traversal (const char *path)
++{
++  if (path == NULL || *path == '\0' || *path == '/')
++    return FALSE;
++
++  if (strcmp (path, "..") == 0 ||
++      g_str_has_prefix (path, "../") ||
++      strstr (path, "/../") != NULL ||
++      g_str_has_suffix (path, "/.."))
++    return FALSE;
++
++  return TRUE;
++}
++
+ static GList *
+ add_extension (GKeyFile   *metakey,
+                const char *group,
+@@ -423,8 +439,39 @@ add_extension (GKeyFile   *metakey,
+   g_autoptr(GFile) deploy_dir = NULL;
+   g_autoptr(FlatpakDir) dir = NULL;
+ 
+-  if (directory == NULL)
+-    return res;
++  if (directory == NULL || !relative_path_has_no_traversal (directory))
++    {
++      g_warning ("Extension %s key %s is missing or has directory traversal, ignoring",
++                 extension, FLATPAK_METADATA_KEY_DIRECTORY);
++      return res;
++    }
++
++  if (subdir_suffix != NULL && !relative_path_has_no_traversal (subdir_suffix))
++    {
++      g_warning ("Extension %s key %s is missing or has directory traversal, ignoring",
++                 extension, FLATPAK_METADATA_KEY_SUBDIRECTORY_SUFFIX);
++      return res;
++    }
++
++  if (add_ld_path != NULL && !relative_path_has_no_traversal (add_ld_path))
++    {
++      g_warning ("Extension %s key %s is missing or has directory traversal, ignoring",
++                 extension, FLATPAK_METADATA_KEY_ADD_LD_PATH);
++      return res;
++    }
++
++  if (merge_dirs != NULL)
++    {
++      for (size_t i = 0; merge_dirs[i] != NULL; i++)
++        {
++          if (!relative_path_has_no_traversal (merge_dirs[i]))
++            {
++              g_warning ("Extension %s key %s is missing or has directory traversal, ignoring",
++                         extension, FLATPAK_METADATA_KEY_MERGE_DIRS);
++              return res;
++            }
++        }
++    }
+ 
+   ref = flatpak_decomposed_new_from_parts (FLATPAK_KINDS_RUNTIME, extension, arch, branch, NULL);
+   if (ref == NULL)
diff -Nru flatpak-1.16.6/debian/patches/GHSA-w69g-9x8j-7p8f/run-Use-RESOLVE_BENEATH-for-host-side-extension-file-acce.patch flatpak-1.16.6/debian/patches/GHSA-w69g-9x8j-7p8f/run-Use-RESOLVE_BENEATH-for-host-side-extension-file-acce.patch
--- flatpak-1.16.6/debian/patches/GHSA-w69g-9x8j-7p8f/run-Use-RESOLVE_BENEATH-for-host-side-extension-file-acce.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/GHSA-w69g-9x8j-7p8f/run-Use-RESOLVE_BENEATH-for-host-side-extension-file-acce.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,106 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Mon, 29 Jun 2026 22:10:41 +0200
+Subject: run: Use RESOLVE_BENEATH for host-side extension file access
+
+Extension content is controlled by the extension developer. When setting
+up the sandbox, Flatpak accesses the .ref file and iterates merge_dirs
+within the extension's files directory on the host using path-based
+operations that follow symlinks. A malicious extension can place symlinks
+that escape the extension directory, causing Flatpak's host process to
+access arbitrary host paths. This discloses host directory listings to
+the sandboxed application via the resulting merge symlinks.
+
+Use glnx_chaseat with GLNX_CHASE_RESOLVE_BENEATH to confine path
+resolution to the extension's files directory.
+
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-w69g-9x8j-7p8f
+Origin: upstream, 1.18.1
+---
+ common/flatpak-run.c | 48 +++++++++++++++++++++++++++++++++++++++++-------
+ 1 file changed, 41 insertions(+), 7 deletions(-)
+
+diff --git a/common/flatpak-run.c b/common/flatpak-run.c
+index 11e0748..2ba1099 100644
+--- a/common/flatpak-run.c
++++ b/common/flatpak-run.c
+@@ -149,7 +149,8 @@ flatpak_run_add_extension_args (FlatpakBwrap      *bwrap,
+       g_autofree char *directory = g_build_filename (target_path, ext->directory, NULL);
+       g_autofree char *full_directory = g_build_filename (directory, ext->subdir_suffix, NULL);
+       g_autofree char *ref_file = g_build_filename (full_directory, ".ref", NULL);
+-      g_autofree char *real_ref = g_build_filename (ext->files_path, ext->directory, ".ref", NULL);
++      g_autofree char *ref_subpath = g_build_filename (ext->directory, ".ref", NULL);
++      glnx_autofd int ext_files_dfd = -1;
+ 
+       if (ext->needs_tmpfs)
+         {
+@@ -168,10 +169,16 @@ flatpak_run_add_extension_args (FlatpakBwrap      *bwrap,
+                               "--ro-bind", ext->files_path, full_directory,
+                               NULL);
+ 
+-      if (g_file_test (real_ref, G_FILE_TEST_EXISTS))
+-        flatpak_bwrap_add_args (bwrap,
+-                                "--lock-file", ref_file,
+-                                NULL);
++      if (glnx_opendirat (AT_FDCWD, ext->files_path, FALSE, &ext_files_dfd, NULL))
++        {
++          glnx_autofd int ref_fd = glnx_chaseat (ext_files_dfd, ref_subpath,
++                                                 GLNX_CHASE_RESOLVE_BENEATH,
++                                                 NULL);
++          if (ref_fd >= 0)
++            flatpak_bwrap_add_args (bwrap,
++                                    "--lock-file", ref_file,
++                                    NULL);
++        }
+     }
+ 
+   g_list_free (path_sorted_extensions);
+@@ -183,8 +190,16 @@ flatpak_run_add_extension_args (FlatpakBwrap      *bwrap,
+       FlatpakExtension *ext = l->data;
+       g_autofree char *directory = g_build_filename (target_path, ext->directory, NULL);
+       g_autofree char *full_directory = g_build_filename (directory, ext->subdir_suffix, NULL);
++      glnx_autofd int files_dfd = -1;
+       int i;
+ 
++      if (!glnx_opendirat (AT_FDCWD, ext->files_path, FALSE, &files_dfd, NULL))
++        {
++          return flatpak_fail_error (error, FLATPAK_ERROR,
++                                     "Failed to open extension %s files",
++                                     ext->installed_id);
++        }
++
+       if (used_extensions->len > 0)
+         g_string_append (used_extensions, ";");
+       g_string_append (used_extensions, ext->installed_id);
+@@ -221,11 +236,30 @@ flatpak_run_add_extension_args (FlatpakBwrap      *bwrap,
+         {
+           g_autofree char *parent = g_path_get_dirname (directory);
+           g_autofree char *merge_dir = g_build_filename (parent, ext->merge_dirs[i], NULL);
+-          g_autofree char *source_dir = g_build_filename (ext->files_path, ext->merge_dirs[i], NULL);
++          glnx_autofd int source_dfd = -1;
++          glnx_autofd int source_read_dfd = -1;
+           g_auto(GLnxDirFdIterator) source_iter = { 0 };
+           struct dirent *dent;
+ 
+-          if (glnx_dirfd_iterator_init_at (AT_FDCWD, source_dir, TRUE, &source_iter, NULL))
++          source_dfd = glnx_chaseat (files_dfd, ext->merge_dirs[i],
++                                     GLNX_CHASE_RESOLVE_BENEATH |
++                                     GLNX_CHASE_MUST_BE_DIRECTORY,
++                                     NULL);
++          if (source_dfd < 0)
++            {
++              if (errno == ENOENT || errno == ENOTDIR)
++                continue;
++
++              return flatpak_fail_error (error, FLATPAK_ERROR,
++                                         "Extension %s has invalid merge-dirs",
++                                         ext->installed_id);
++            }
++
++          source_read_dfd = glnx_fd_reopen (source_dfd, O_RDONLY, error);
++          if (source_read_dfd < 0)
++            return FALSE;
++
++          if (glnx_dirfd_iterator_init_take_fd (&source_read_dfd, &source_iter, NULL))
+             {
+               while (glnx_dirfd_iterator_next_dent (&source_iter, &dent, NULL, NULL) && dent != NULL)
+                 {
diff -Nru flatpak-1.16.6/debian/patches/hardening/dir-Harden-confining-subpath-deploy-to-the-deploy-base.patch flatpak-1.16.6/debian/patches/hardening/dir-Harden-confining-subpath-deploy-to-the-deploy-base.patch
--- flatpak-1.16.6/debian/patches/hardening/dir-Harden-confining-subpath-deploy-to-the-deploy-base.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/hardening/dir-Harden-confining-subpath-deploy-to-the-deploy-base.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,62 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Mon, 29 Jun 2026 23:14:37 +0200
+Subject: dir: Harden confining subpath deploy to the deploy base
+
+Subpaths passed via D-Bus to the system helper are not validated, so
+a value containing ".." components is accepted.
+
+The only reason why this isn't exploitable is because we resolve the
+path relative to `root` via g_file_resolve_relative_path, where root is
+a OstreeRepoFile which will return FALSE when it encounters ".." or
+symlinks.
+
+Harden the code by using glnx_chase_and_mkdirat with RESOLVE_BENEATH to
+create parent directories, and check out into the resulting fd.
+
+Origin: upstream, 1.18.1
+---
+ common/flatpak-dir.c | 19 +++++++++----------
+ 1 file changed, 9 insertions(+), 10 deletions(-)
+
+diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c
+index 6a9c192..4378212 100644
+--- a/common/flatpak-dir.c
++++ b/common/flatpak-dir.c
+@@ -8863,28 +8863,27 @@ flatpak_dir_deploy (FlatpakDir          *self,
+       for (i = 0; subpaths[i] != NULL; i++)
+         {
+           g_autofree char *subpath = g_build_filename ("files", subpaths[i], NULL);
+-          g_autofree char *dstpath = g_build_filename (checkoutdirpath, "/files", subpaths[i], NULL);
+-          g_autofree char *dstpath_parent = g_path_get_dirname (dstpath);
+-          g_autofree char *dstpath_relative_to_deploy_base = g_build_filename (checkoutdir_basename, "/files", subpaths[i], NULL);
++          g_autofree char *dstpath = g_build_filename (checkoutdir_basename, subpath, NULL);
++          g_autofree char *dstdir = g_path_get_dirname (dstpath);
++          g_autofree char *dstbase = g_path_get_basename (dstpath);
++          glnx_autofd int parent_dfd = -1;
+           g_autoptr(GFile) child = NULL;
+ 
+           child = g_file_resolve_relative_path (root, subpath);
+-
+           if (!g_file_query_exists (child, cancellable))
+             {
+               g_info ("subpath %s not in tree", subpaths[i]);
+               continue;
+             }
+ 
+-          if (g_mkdir_with_parents (dstpath_parent, 0755))
+-            {
+-              glnx_set_error_from_errno (error);
+-              return FALSE;
+-            }
++          parent_dfd = glnx_chase_and_mkdirat (deploy_base_dfd, dstdir,
++                                               GLNX_CHASE_RESOLVE_BENEATH, 0755, error);
++          if (parent_dfd < 0)
++            return FALSE;
+ 
+           options.subpath = subpath;
+           if (!ostree_repo_checkout_at (self->repo, &options,
+-                                        deploy_base_dfd, dstpath_relative_to_deploy_base,
++                                        parent_dfd, dstbase,
+                                         checksum,
+                                         cancellable, error))
+             {
diff -Nru flatpak-1.16.6/debian/patches/hardening/dir-Use-fd-based-traversal-for-marker-files-in-deploy-dir.patch flatpak-1.16.6/debian/patches/hardening/dir-Use-fd-based-traversal-for-marker-files-in-deploy-dir.patch
--- flatpak-1.16.6/debian/patches/hardening/dir-Use-fd-based-traversal-for-marker-files-in-deploy-dir.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/hardening/dir-Use-fd-based-traversal-for-marker-files-in-deploy-dir.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,100 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Fri, 3 Jul 2026 16:36:48 +0200
+Subject: dir: Use fd-based traversal for marker files in deploy directories
+
+The paths up to files/ are not attacker-controlled, and
+g_file_replace_contents with G_FILE_CREATE_REPLACE_DESTINATION already
+replaces symlinks rather than following them, so this is not a security
+fix. The only reason why is that the file is in the root of the attacker
+controlled directory.
+
+This is a bit subtle, so lets do the right thing and concistently use
+fd-relative operations when doing anything inside attacker/app
+controlled directories.
+
+Bug: https://github.com/flatpak/flatpak/security/advisories/GHSA-w69g-9x8j-7p8f
+Origin: upstream, 1.18.1
+---
+ common/flatpak-dir.c | 48 ++++++++++++++++++++++++++++++------------------
+ 1 file changed, 30 insertions(+), 18 deletions(-)
+
+diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c
+index 6e3f891..6a9c192 100644
+--- a/common/flatpak-dir.c
++++ b/common/flatpak-dir.c
+@@ -8726,7 +8726,6 @@ flatpak_dir_deploy (FlatpakDir          *self,
+   g_autofree char *checkoutdirpath = NULL;
+   const char *checkoutdir_basename;
+   g_autoptr(GFile) real_checkoutdir = NULL;
+-  g_autoptr(GFile) dotref = NULL;
+   g_autoptr(GFile) files_etc = NULL;
+   g_autoptr(GFile) deploy_data_file = NULL;
+   g_autoptr(GVariant) commit_data = NULL;
+@@ -8986,10 +8985,19 @@ flatpak_dir_deploy (FlatpakDir          *self,
+                                  metadata_contents, metadata_size, error))
+     return FALSE;
+ 
+-  dotref = g_file_resolve_relative_path (checkoutdir, "files/.ref");
+-  if (!g_file_replace_contents (dotref, "", 0, NULL, FALSE,
+-                                G_FILE_CREATE_REPLACE_DESTINATION, NULL, cancellable, error))
+-    return FALSE;
++  {
++    g_autofree char *files_path = g_build_filename (checkoutdir_basename, "files", NULL);
++    glnx_autofd int files_dfd = -1;
++
++    if (!glnx_opendirat (deploy_base_dfd, files_path, FALSE, &files_dfd, error))
++      return FALSE;
++
++    if (!glnx_file_replace_contents_at (files_dfd, ".ref",
++                                        (const guint8 *) "", 0,
++                                        GLNX_FILE_REPLACE_NODATASYNC,
++                                        cancellable, error))
++      return FALSE;
++  }
+ 
+   export = g_file_get_child (checkoutdir, "export");
+ 
+@@ -11211,7 +11219,6 @@ flatpak_dir_undeploy (FlatpakDir        *self,
+   g_autofree char *id = NULL;
+   g_autofree char *dirname = NULL;
+   g_autofree char *current_active = NULL;
+-  g_autoptr(GFile) change_file = NULL;
+   g_autoptr(GError) child_error = NULL;
+   int i, retry;
+ 
+@@ -11303,18 +11310,23 @@ flatpak_dir_undeploy (FlatpakDir        *self,
+     }
+ 
+ 
+-  if (is_update)
+-    change_file = g_file_resolve_relative_path (removed_subdir, "files/.updated");
+-  else
+-    change_file = g_file_resolve_relative_path (removed_subdir, "files/.removed");
+-
+-  if (!g_file_replace_contents (change_file, "", 0, NULL, FALSE,
+-                                G_FILE_CREATE_REPLACE_DESTINATION, NULL, NULL, &child_error))
+-    {
+-      g_autofree gchar *path = g_file_get_path (change_file);
+-      g_warning ("Unable to clear %s: %s", path, child_error->message);
+-      g_clear_error (&child_error);
+-    }
++  {
++    glnx_autofd int files_dfd = -1;
++    const char *marker = is_update ? ".updated" : ".removed";
++    g_autofree char *files_path = g_build_filename (flatpak_file_get_path_cached (removed_subdir), "files", NULL);
++
++    if (!glnx_opendirat (AT_FDCWD, files_path,
++                         FALSE, &files_dfd, &child_error) ||
++        !glnx_file_replace_contents_at (files_dfd, marker,
++                                        (const guint8 *) "", 0,
++                                        GLNX_FILE_REPLACE_NODATASYNC,
++                                        NULL, &child_error))
++      {
++        g_warning ("Unable to write %s marker: %s", marker,
++                   child_error ? child_error->message : g_strerror (errno));
++        g_clear_error (&child_error);
++      }
++  }
+ 
+   if (force_remove || !dir_is_locked (removed_subdir))
+     {
diff -Nru flatpak-1.16.6/debian/patches/libglnx/chase-Add-glnx_chase_and_mkdirat.patch flatpak-1.16.6/debian/patches/libglnx/chase-Add-glnx_chase_and_mkdirat.patch
--- flatpak-1.16.6/debian/patches/libglnx/chase-Add-glnx_chase_and_mkdirat.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/libglnx/chase-Add-glnx_chase_and_mkdirat.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,232 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Fri, 12 Jun 2026 13:45:22 +0200
+Subject: chase: Add glnx_chase_and_mkdirat
+
+We found that there is a common use case where we need to get a
+subdirectory (potentially multiple levels) which might not exist yet.
+Adding another flag for this to GlnxChaseFlags is what systemd has done,
+but creating a directory takes a mode, so the flag creates directories
+with a fixed mode. This approach instead takes the mode as argument.
+
+[This is required for various later security fixes in Flatpak -smcv]
+
+(cherry-picked from commit ae7355612a68d39472ff3699104845d2cc5dfb4d)
+
+Origin: upstream, 1.18.1, commit:ae7355612a68d39472ff3699104845d2cc5dfb4d
+---
+ subprojects/libglnx/glnx-chase.c               | 87 ++++++++++++++++++++++++++
+ subprojects/libglnx/glnx-chase.h               |  6 ++
+ subprojects/libglnx/tests/test-libglnx-chase.c | 69 ++++++++++++++++++++
+ 3 files changed, 162 insertions(+)
+
+diff --git a/subprojects/libglnx/glnx-chase.c b/subprojects/libglnx/glnx-chase.c
+index d7edf34..2251cb7 100644
+--- a/subprojects/libglnx/glnx-chase.c
++++ b/subprojects/libglnx/glnx-chase.c
+@@ -17,6 +17,7 @@
+ #include <unistd.h>
+ 
+ #include <glnx-backports.h>
++#include <glnx-dirfd.h>
+ #include <glnx-errors.h>
+ #include <glnx-fdio.h>
+ #include <glnx-local-alloc.h>
+@@ -823,3 +824,89 @@ glnx_chase_and_statxat (int                 dirfd,
+ 
+   return g_steal_fd (&fd);
+ }
++
++static int
++chase_and_mkdir (int                 next_fd,
++                 int                 current_fd,
++                 const char         *segment,
++                 G_GNUC_UNUSED int   open_tree_flags,
++                 gpointer            user_data,
++                 GError            **error)
++{
++  mode_t mode = GPOINTER_TO_INT (user_data);
++  glnx_autofd int new_fd = -1;
++
++  /* if chase managed to get the next segment, we already got our answer */
++  if (next_fd >= 0)
++    return next_fd;
++
++  /* if the problem isn't that the file doesn't exist, we propagate the error */
++  g_assert (*error != NULL);
++  if (!g_error_matches (*error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
++    return -1;
++  g_clear_error (error);
++
++  /* create the directory with the specified mode */
++  if (!glnx_ensure_dir (current_fd, segment, mode, error))
++    return -1;
++
++  /* Get a fd to the created dir.
++   * This is racy, meaning another process can modify the filesystem and we
++   * might open a directory that we did not create and might have a different
++   * mode. There isn't a kernel API which returns a fd or an inode which makes
++   * this race unavoidable. This is fine though because the semantics of
++   * `glnx_chase_and_mkdirat` accept arbitrary directories (even with different
++   * modes) when chasing the path.
++   */
++  new_fd = openat (current_fd, segment,
++                   O_CLOEXEC | O_PATH | O_DIRECTORY | O_NOFOLLOW);
++  if (new_fd < 0)
++    return glnx_fd_throw_errno_prefix (error, "opening created dir failed");
++
++  return g_steal_fd (&new_fd);
++}
++
++/**
++ * glnx_chase_and_mkdirat:
++ * @dirfd: a directory file descriptor
++ * @path: a path
++ * @flags: restricted combination of GlnxChaseFlags flags
++ * @mode: the mode for new directories
++ * @error: a #GError
++ *
++ * Same as glnx_chase with `GLNX_CHASE_MUST_BE_DIRECTORY`, but when a path
++ * segment does not exist, a directory is created for the segment with the mode
++ * @mode.
++ * This essentially implement a fd-relative equivalent of g_mkdir_with_parents()
++ * or `mkdir -p`.
++ * Note, that directories in the path which already exist can have arbitrary
++ * modes.
++ *
++ * See glnx_chaseat for the meaning of @dirfd and @path.
++ *
++ * The @flags argument is the same as in glnx_chaseat, but setting
++ * `GLNX_CHASE_MUST_BE_REGULAR`, `GLNX_CHASE_MUST_BE_SOCKET`, or
++ * `GLNX_CHASE_MUST_BE_DIRECTORY` is an error.
++ *
++ * Returns: the chased file, or -1 with @error set on error
++ */
++int
++glnx_chase_and_mkdirat (int              dirfd,
++                        const char      *path,
++                        GlnxChaseFlags   flags,
++                        mode_t           mode,
++                        GError         **error)
++{
++  g_return_val_if_fail ((flags & ~(GLNX_CHASE_ALL_FLAGS)) == 0, -1);
++  g_return_val_if_fail ((flags & (GLNX_CHASE_MUST_BE_REGULAR |
++                                  GLNX_CHASE_MUST_BE_SOCKET |
++                                  GLNX_CHASE_MUST_BE_DIRECTORY)) == 0, -1);
++  g_return_val_if_fail ((mode & ~((mode_t) 07777)) == 0, -1);
++
++  /* This function always implies GLNX_CHASE_MUST_BE_DIRECTORY */
++  flags |= GLNX_CHASE_MUST_BE_DIRECTORY;
++
++  return glnx_chaseat_full (dirfd, path, flags,
++                            chase_and_mkdir, GINT_TO_POINTER (mode),
++                            error);
++}
+diff --git a/subprojects/libglnx/glnx-chase.h b/subprojects/libglnx/glnx-chase.h
+index 05dac12..af5f2bf 100644
+--- a/subprojects/libglnx/glnx-chase.h
++++ b/subprojects/libglnx/glnx-chase.h
+@@ -48,4 +48,10 @@ int glnx_chase_and_statxat (int                 dirfd,
+                             struct glnx_statx  *statbuf,
+                             GError            **error);
+ 
++int glnx_chase_and_mkdirat (int              dirfd,
++                            const char      *path,
++                            GlnxChaseFlags   flags,
++                            mode_t           mode,
++                            GError         **error);
++
+ G_END_DECLS
+diff --git a/subprojects/libglnx/tests/test-libglnx-chase.c b/subprojects/libglnx/tests/test-libglnx-chase.c
+index b0ce1b4..0a7a655 100644
+--- a/subprojects/libglnx/tests/test-libglnx-chase.c
++++ b/subprojects/libglnx/tests/test-libglnx-chase.c
+@@ -50,6 +50,18 @@ path_get_ino (const char *path)
+   return st.st_ino;
+ }
+ 
++static mode_t
++get_mode (int fd)
++{
++  int r;
++  struct stat st;
++
++  r = fstatat (fd, "", &st, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW);
++  g_assert_cmpint (r, >=, 0);
++
++  return st.st_mode & 0777;
++}
++
+ static char *
+ get_abspath (int         dfd,
+              const char *path)
+@@ -586,6 +598,62 @@ test_chase_and_statxat_permissions (void)
+   g_clear_fd (&chase_fd, NULL);
+ }
+ 
++static void
++test_chase_and_mkdir (void)
++{
++  g_autoptr(GError) error = NULL;
++  glnx_autofd int dfd = -1;
++  glnx_autofd int file_fd = -1;
++  glnx_autofd int chase_fd = -1;
++  ino_t expected_ino;
++
++  g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "d1", 0755,
++                                              &dfd,
++                                              NULL, &error));
++  g_assert_no_error (error);
++
++  chase_fd = glnx_chase_and_mkdirat (AT_FDCWD, "d1/d2/d3",
++                                     GLNX_CHASE_DEFAULT,
++                                     0755,
++                                     &error);
++  g_assert_cmpint (chase_fd, >=, 0);
++  g_assert_no_error (error);
++
++  file_fd = openat (AT_FDCWD, "d1/d2/d3", O_PATH | O_CLOEXEC | O_NOFOLLOW);
++  g_assert_cmpint (file_fd, >=, 0);
++  expected_ino = get_ino (file_fd);
++
++  g_assert_cmpint (get_ino (chase_fd), ==, expected_ino);
++
++  g_assert_cmpint (get_mode (dfd), ==, 0755);
++  g_assert_cmpint (get_mode (chase_fd), ==, 0755);
++
++  g_clear_fd (&chase_fd, NULL);
++  g_clear_fd (&file_fd, NULL);
++
++  chase_fd = glnx_chase_and_mkdirat (AT_FDCWD, "d1/d4/d5",
++                                     GLNX_CHASE_DEFAULT,
++                                     0700,
++                                     &error);
++  g_assert_cmpint (chase_fd, >=, 0);
++  g_assert_no_error (error);
++
++  file_fd = openat (AT_FDCWD, "d1", O_PATH | O_CLOEXEC | O_NOFOLLOW);
++  g_assert_cmpint (file_fd, >=, 0);
++  g_assert_cmpint (get_mode (file_fd), ==, 0755);
++  g_clear_fd (&file_fd, NULL);
++
++  file_fd = openat (AT_FDCWD, "d1/d4", O_PATH | O_CLOEXEC | O_NOFOLLOW);
++  g_assert_cmpint (file_fd, >=, 0);
++  g_assert_cmpint (get_mode (file_fd), ==, 0700);
++  g_clear_fd (&file_fd, NULL);
++
++  file_fd = openat (AT_FDCWD, "d1/d4/d5", O_PATH | O_CLOEXEC | O_NOFOLLOW);
++  g_assert_cmpint (file_fd, >=, 0);
++  g_assert_cmpint (get_mode (file_fd), ==, 0700);
++  g_clear_fd (&file_fd, NULL);
++}
++
+ int main (int argc, char **argv)
+ {
+   _GLNX_TEST_SCOPED_TEMP_DIR;
+@@ -602,6 +670,7 @@ int main (int argc, char **argv)
+   g_test_add_func ("/chase-and-statxat-basic", test_chase_and_statxat_basic);
+   g_test_add_func ("/chase-and-statxat-symlink", test_chase_and_statxat_symlink);
+   g_test_add_func ("/chase-and-statxat-permissions", test_chase_and_statxat_permissions);
++  g_test_add_func ("/chase-and-mkdir", test_chase_and_mkdir);
+ 
+   ret = g_test_run();
+ 
diff -Nru flatpak-1.16.6/debian/patches/libglnx/chase-Add-internal-glnx_chaseat_full-for-a-strategic-call.patch flatpak-1.16.6/debian/patches/libglnx/chase-Add-internal-glnx_chaseat_full-for-a-strategic-call.patch
--- flatpak-1.16.6/debian/patches/libglnx/chase-Add-internal-glnx_chaseat_full-for-a-strategic-call.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/libglnx/chase-Add-internal-glnx_chaseat_full-for-a-strategic-call.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,201 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Fri, 12 Jun 2026 13:42:13 +0200
+Subject: chase: Add internal glnx_chaseat_full for a strategic callback
+
+It takes a callback which gets called every time we try to open the next
+segment of the path. This allows implementing more specific and advanced
+use cases to be implemented without adding more complexity to the chase
+algorithm itself.
+
+[This is required for various later security fixes in Flatpak -smcv]
+
+(cherry-picked from commit 4f8674709dce57e7f5422a475f61556162f930d8)
+
+Origin: upstream, 1.18.1, commit:4f8674709dce57e7f5422a475f61556162f930d8
+---
+ subprojects/libglnx/glnx-chase.c | 110 ++++++++++++++++++++++++++-------------
+ 1 file changed, 73 insertions(+), 37 deletions(-)
+
+diff --git a/subprojects/libglnx/glnx-chase.c b/subprojects/libglnx/glnx-chase.c
+index 9ad2fe3..d7edf34 100644
+--- a/subprojects/libglnx/glnx-chase.c
++++ b/subprojects/libglnx/glnx-chase.c
+@@ -46,6 +46,13 @@
+ #define GLNX_CHASE_ALL_FLAGS \
+   (GLNX_CHASE_ALL_DEBUG_FLAGS | GLNX_CHASE_ALL_REGULAR_FLAGS)
+ 
++typedef int (* GlnxChaseCallback)(int          next_fd,
++                                  int          current_fd,
++                                  const char  *segment,
++                                  int          open_tree_flags,
++                                  gpointer     user_data,
++                                  GError     **error);
++
+ typedef GQueue GlnxStatxQueue;
+ 
+ static void
+@@ -328,10 +335,12 @@ extract_next_segment (const char **remaining,
+  * we're in full control over the resolving.
+  */
+ static int
+-chase_manual (int              dirfd,
+-              const char      *path,
+-              GlnxChaseFlags   flags,
+-              GError         **error)
++chase_manual (int                 dirfd,
++              const char         *path,
++              GlnxChaseFlags      flags,
++              GlnxChaseCallback   callback,
++              gpointer            user_data,
++              GError            **error)
+ {
+   gboolean is_absolute;
+   g_autofree char *buffer = NULL;
+@@ -349,6 +358,7 @@ chase_manual (int              dirfd,
+    * - 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)
++   * - there is no callback
+    *
+    * TODO: if we have a guarantee that the open_tree syscall works, we can
+    * shortcut even without GLNX_CHASE_NO_AUTOMOUNT
+@@ -356,7 +366,8 @@ chase_manual (int              dirfd,
+   if ((flags & (GLNX_CHASE_NO_AUTOMOUNT |
+                 GLNX_CHASE_RESOLVE_BENEATH |
+                 GLNX_CHASE_RESOLVE_IN_ROOT |
+-                GLNX_CHASE_RESOLVE_NO_SYMLINKS)) == GLNX_CHASE_NO_AUTOMOUNT)
++                GLNX_CHASE_RESOLVE_NO_SYMLINKS)) == GLNX_CHASE_NO_AUTOMOUNT &&
++      callback == NULL)
+     {
+       GlnxChaseFlags open_tree_flags =
+         (flags & (GLNX_CHASE_NOFOLLOW | GLNX_CHASE_ALL_DEBUG_FLAGS));
+@@ -466,10 +477,25 @@ chase_manual (int              dirfd,
+         GlnxChaseFlags open_tree_flags =
+           GLNX_CHASE_NOFOLLOW |
+           (flags & (GLNX_CHASE_NO_AUTOMOUNT | GLNX_CHASE_ALL_DEBUG_FLAGS));
++        g_autoptr(GError) local_error = NULL;
++
++        next_fd = chase_open_tree (fd, segment, open_tree_flags, &local_error);
++
++        /* Note that the callback can be called with next_fd < 0.
++         * If so, the error is already set, but may be cleared by
++         * the callback if it can recover from an error that already
++         * occurred. */
++        if (callback)
++          {
++            next_fd = callback (next_fd, fd, segment, open_tree_flags,
++                                user_data, &local_error);
++          }
+ 
+-        next_fd = chase_open_tree (fd, segment, open_tree_flags, error);
+         if (next_fd < 0)
+-          return -1;
++          {
++            g_propagate_error (error, g_steal_pointer (&local_error));
++            return -1;
++          }
+       }
+ 
+       if (!glnx_chase_statx (next_fd, no_automount, &st, error))
+@@ -602,30 +628,13 @@ chase_manual (int              dirfd,
+   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 int
++glnx_chaseat_full (int                 dirfd,
++                   const char         *path,
++                   GlnxChaseFlags      flags,
++                   GlnxChaseCallback   callback,
++                   gpointer            user_data,
++                   GError            **error)
+ {
+   static gboolean can_openat2 = TRUE;
+   glnx_autofd int fd = -1;
+@@ -643,15 +652,14 @@ glnx_chaseat (int              dirfd,
+     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)
++  if (can_openat2 &&
++      (flags & GLNX_CHASE_NO_AUTOMOUNT) == 0 &&
++      (flags & GLNX_CHASE_DEBUG_NO_OPENAT2) == 0 &&
++      callback == NULL)
+     {
+       uint64_t openat2_flags = 0;
+       uint64_t openat2_resolve = 0;
+@@ -690,7 +698,7 @@ glnx_chaseat (int              dirfd,
+ 
+   if (fd < 0)
+     {
+-      fd = chase_manual (dirfd, path, flags, error);
++      fd = chase_manual (dirfd, path, flags, callback, user_data, error);
+       if (fd < 0)
+         return -1;
+     }
+@@ -744,6 +752,34 @@ glnx_chaseat (int              dirfd,
+   return g_steal_fd (&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)
++{
++  return glnx_chaseat_full (dirfd, path, flags, NULL, NULL, error);
++}
++
+ /**
+  * glnx_chase_and_statxat:
+  * @dirfd: a directory file descriptor
diff -Nru flatpak-1.16.6/debian/patches/portal-Actually-use-the-AppInfo-hash-table.patch flatpak-1.16.6/debian/patches/portal-Actually-use-the-AppInfo-hash-table.patch
--- flatpak-1.16.6/debian/patches/portal-Actually-use-the-AppInfo-hash-table.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/portal-Actually-use-the-AppInfo-hash-table.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,41 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Mon, 13 Apr 2026 13:37:55 +0200
+Subject: portal: Actually use the AppInfo hash table
+
+Before this change, the hash table was populated with the keyfile as key
+and value, but the lookup and invalidate was done on the sender as the
+key.
+
+We never found a previous sender, and never invalidated one.
+
+Fix this by actually using the sender as the key.
+
+(cherry picked from commit 993cc143d84aa08410979bf49358eba47cab1066):
+Origin: upstream, 1.16.7, commit:6a98dceb56edff29234a7493ae07d9607661f13b
+---
+ portal/flatpak-portal-app-info.c | 5 +++--
+ 1 file changed, 3 insertions(+), 2 deletions(-)
+
+diff --git a/portal/flatpak-portal-app-info.c b/portal/flatpak-portal-app-info.c
+index f6c8162..9dd741f 100644
+--- a/portal/flatpak-portal-app-info.c
++++ b/portal/flatpak-portal-app-info.c
+@@ -33,7 +33,8 @@ ensure_app_infos (void)
+ {
+   if (app_infos == NULL)
+     app_infos = g_hash_table_new_full (g_str_hash, g_str_equal,
+-                                       NULL, (GDestroyNotify) g_key_file_unref);
++                                       g_free,
++                                       (GDestroyNotify) g_key_file_unref);
+ }
+ 
+ static GKeyFile *
+@@ -62,7 +63,7 @@ static void
+ add_cached_app_info_by_sender (const char *sender, GKeyFile *keyfile)
+ {
+   G_LOCK (app_infos);
+-  g_hash_table_add (app_infos, g_key_file_ref (keyfile));
++  g_hash_table_insert (app_infos, g_strdup (sender), g_key_file_ref (keyfile));
+   G_UNLOCK (app_infos);
+ }
+ 
diff -Nru flatpak-1.16.6/debian/patches/series flatpak-1.16.6/debian/patches/series
--- flatpak-1.16.6/debian/patches/series	2026-04-10 20:03:53.000000000 +0100
+++ flatpak-1.16.6/debian/patches/series	2026-08-11 14:03:38.000000000 +0100
@@ -1 +1,39 @@
 debian/system-helper-Set-Debian-s-canonical-home-directory-for-s.patch
+subprojects-Ignore-.wraplock-file-generated-by-recent-Mes.patch
+bwrap-Clarify-a-comment.patch
+dir-Use-flatpak_bwrap_child_setup_inherit_fds_cb-to-apply.patch
+subprojects-Update-dbus-proxy.wrap-to-v0.1.7.patch
+portal-Actually-use-the-AppInfo-hash-table.patch
+tests/tests-Fix-checks-where-we-expect-a-command-to-fail.patch
+libglnx/chase-Add-internal-glnx_chaseat_full-for-a-strategic-call.patch
+libglnx/chase-Add-glnx_chase_and_mkdirat.patch
+GHSA-99wv-m8rp-g58x/common-Fix-return-value-and-typos-in-flatpak_switch_symli.patch
+GHSA-99wv-m8rp-g58x/common-Use-fd-based-operations-in-flatpak_switch_symlink_.patch
+GHSA-99wv-m8rp-g58x/run-Harden-regenerate_ld_cache-against-symlink-attacks.patch
+GHSA-8688-9x26-hhxj/test-run-Check-that-the-way-com.valvesoftware.Steam-behav.patch
+GHSA-8688-9x26-hhxj/run-Harden-ensure_data_dir-and-var-setup-against-symlink-.patch
+GHSA-8688-9x26-hhxj/tests-Add-a-regression-test-for-GHSA-8688-9x26-hhxj.patch
+GHSA-jr92-2v97-wgvc/oci-Use-gsize-for-OCI-delta-sizes.patch
+GHSA-jr92-2v97-wgvc/oci-Avoid-overflow-in-delta_read_data.patch
+GHSA-jr92-2v97-wgvc/oci-Limit-delta-path-length-to-PATH_MAX.patch
+GHSA-r7hp-698j-2h6c/run-dbus-Correct-broadcast-rules-for-the-AT-SPI-bus.patch
+GHSA-v2gw-v9h5-9q4x/ref-utils-Make-remote-name-validation-public-and-take-an-.patch
+GHSA-v2gw-v9h5-9q4x/system-helper-Validate-each-remote-name-and-architecture-.patch
+GHSA-v2gw-v9h5-9q4x/tests-Add-some-unit-tests-for-ref-utils-name-component-va.patch
+GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-out-of-basefd-in-si.patch
+GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-in-link-rename.patch
+GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-in-symlink.patch
+GHSA-qrwq-7qwx-q9rp/revokefs-Avoid-symlink-path-traversal-in-chmod.patch
+GHSA-w69g-9x8j-7p8f/dir-utils-Reject-path-traversal-in-extension-metadata.patch
+GHSA-w69g-9x8j-7p8f/run-Use-RESOLVE_BENEATH-for-host-side-extension-file-acce.patch
+hardening/dir-Use-fd-based-traversal-for-marker-files-in-deploy-dir.patch
+hardening/dir-Harden-confining-subpath-deploy-to-the-deploy-base.patch
+GHSA-q4gr-vc25-57m5/dir-Fall-back-to-the-deployed-ref-commit-when-up-downgrad.patch
+GHSA-q4gr-vc25-57m5/system-dir-Prevent-removing-of-deployed-refs-via-the-syst.patch
+GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/utils-Add-flatpak_cp_a_at-a-fd-based-flatpak_cp_a-variant.patch
+GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/utils-Make-flatpak_cp_a-a-wrapper-around-flatpak_cp_a_at.patch
+GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/build-init-Use-fd-relative-operations-to-prevent-path-tra.patch
+GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/dir-Validate-extra-data-name.patch
+GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/dir-Use-chaseat-in-extract_extra_data-to-prevent-path-tra.patch
+GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/dir-Use-chaseat-in-apply_extra_data-to-prevent-path-trave.patch
+GHSA-fqx6-vh4p-42cg-GHSA-8qxj-x646-phcm/utils-Remove-unused-GFile-based-flatpak_cp_a.patch
diff -Nru flatpak-1.16.6/debian/patches/subprojects-Ignore-.wraplock-file-generated-by-recent-Mes.patch flatpak-1.16.6/debian/patches/subprojects-Ignore-.wraplock-file-generated-by-recent-Mes.patch
--- flatpak-1.16.6/debian/patches/subprojects-Ignore-.wraplock-file-generated-by-recent-Mes.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/subprojects-Ignore-.wraplock-file-generated-by-recent-Mes.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,19 @@
+From: Simon McVittie <smcv@collabora.com>
+Date: Sat, 11 Apr 2026 17:31:48 +0100
+Subject: subprojects: Ignore .wraplock file generated by recent Meson
+
+Signed-off-by: Simon McVittie <smcv@collabora.com>
+(cherry picked from commit 0337cfde00886d7377da1ded7d2c8520f1d61415)
+Origin: upstream, 1.16.7, commit:a554bae3b2474c1ae8c086a953a1d65be03a380b
+---
+ subprojects/.gitignore | 1 +
+ 1 file changed, 1 insertion(+)
+
+diff --git a/subprojects/.gitignore b/subprojects/.gitignore
+index f71f340..6078eec 100644
+--- a/subprojects/.gitignore
++++ b/subprojects/.gitignore
+@@ -1,2 +1,3 @@
++/.wraplock
+ bubblewrap/
+ dbus-proxy/
diff -Nru flatpak-1.16.6/debian/patches/subprojects-Update-dbus-proxy.wrap-to-v0.1.7.patch flatpak-1.16.6/debian/patches/subprojects-Update-dbus-proxy.wrap-to-v0.1.7.patch
--- flatpak-1.16.6/debian/patches/subprojects-Update-dbus-proxy.wrap-to-v0.1.7.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/subprojects-Update-dbus-proxy.wrap-to-v0.1.7.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,25 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Mon, 13 Apr 2026 15:53:16 +0200
+Subject: subprojects: Update dbus-proxy.wrap to v0.1.7
+
+Versions up to 0.1.6 contain a security vulnerability, so let's bump it
+to a version which does not.
+
+(cherry picked from commit 2b942431d28c71b4046c34010525f400a6cf4ac7):
+Origin: upstream, 1.16.7, commit:e39581a798987d96ed786854fca75606d13fe5d7
+---
+ subprojects/dbus-proxy.wrap | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/subprojects/dbus-proxy.wrap b/subprojects/dbus-proxy.wrap
+index 50127f3..29dcdce 100644
+--- a/subprojects/dbus-proxy.wrap
++++ b/subprojects/dbus-proxy.wrap
+@@ -1,5 +1,5 @@
+ [wrap-git]
+ url = https://github.com/flatpak/xdg-dbus-proxy
+-# 0.1.6
+-revision = 1c1989e56f94b9eb3b7567f8a6e8a0aa16cba496
++# 0.1.7
++revision = 6a170fa77e3cbecb48f9dd2478fe5c0a119eb467
+ depth = 1
diff -Nru flatpak-1.16.6/debian/patches/tests/tests-Fix-checks-where-we-expect-a-command-to-fail.patch flatpak-1.16.6/debian/patches/tests/tests-Fix-checks-where-we-expect-a-command-to-fail.patch
--- flatpak-1.16.6/debian/patches/tests/tests-Fix-checks-where-we-expect-a-command-to-fail.patch	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/debian/patches/tests/tests-Fix-checks-where-we-expect-a-command-to-fail.patch	2026-08-11 14:03:38.000000000 +0100
@@ -0,0 +1,133 @@
+From: Sebastian Wick <sebastian.wick@redhat.com>
+Date: Tue, 14 Apr 2026 21:16:21 +0200
+Subject: tests: Fix checks where we expect a command to fail
+
+I was convinced that the pattern `! command` with -e aborts when
+`command` fails. This is not the case (the result of `false` is the same
+as `! true` but somehow this doesn't matter).
+
+Fix the tests and use the newly introduced `assert_fail` function. One
+could also use `command && assert_not_reached "message"` but who has
+time to write error messages for all the cases.
+
+[We need this for some of the tests for later security fixes -smcv]
+
+(cherry-picked from commit 68343e938f66051462e0ae3003d0acaca9a76a72)
+
+Origin: upstream, 1.17.7, commit:68343e938f66051462e0ae3003d0acaca9a76a72
+---
+ tests/libtest.sh         |  9 +++++++++
+ tests/test-run-custom.sh | 22 +++++++++++-----------
+ 2 files changed, 20 insertions(+), 11 deletions(-)
+
+diff --git a/tests/libtest.sh b/tests/libtest.sh
+index 7004f2d..058e7d9 100644
+--- a/tests/libtest.sh
++++ b/tests/libtest.sh
+@@ -282,6 +282,15 @@ assert_remote_has_no_config () {
+     } 3> /dev/null
+ }
+ 
++assert_fail () {
++    if "$@"; then
++        { { local BASH_XTRACEFD=3; } 2> /dev/null
++            echo "Command '$*' should not have succeeded at $(basename ${BASH_SOURCE[1]}):${BASH_LINENO[0]}" >&2
++            exit 1
++        } 3> /dev/null
++    fi
++}
++
+ export FL_GPG_HOMEDIR=${TEST_DATA_DIR}/gpghome
+ export FL_GPG_HOMEDIR2=${TEST_DATA_DIR}/gpghome2
+ mkdir -p ${FL_GPG_HOMEDIR}
+diff --git a/tests/test-run-custom.sh b/tests/test-run-custom.sh
+index 4868b8b..7736866 100755
+--- a/tests/test-run-custom.sh
++++ b/tests/test-run-custom.sh
+@@ -51,7 +51,7 @@ 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
++assert_fail 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$'
+@@ -59,7 +59,7 @@ 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
++assert_fail run --app-path="" --command=/run/parent/usr/bin/runtime_hello.sh org.test.Hello > /dev/null
+ 
+ ok "empty app path"
+ 
+@@ -72,11 +72,11 @@ 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
++assert_fail 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
++assert_fail run --app-path=path-which-does-not-exist org.test.Hello > /dev/null
+ 
+ ok "bad custom app path"
+ 
+@@ -85,7 +85,7 @@ 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
++assert_fail run --app-fd=3 --command=/app/bin/hello.sh org.test.Hello > /dev/null
+ 
+ ok "custom app fd"
+ 
+@@ -95,14 +95,14 @@ 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
++assert_fail 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
++assert_fail run --usr-path=path-which-does-not-exist org.test.Hello > /dev/null
+ 
+ ok "bad custom usr path"
+ 
+@@ -116,7 +116,7 @@ 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
++assert_fail run --usr-fd=3 --command=/app/bin/hello.sh org.test.Hello > /dev/null
+ 
+ ok "custom usr fd"
+ 
+@@ -138,8 +138,8 @@ 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
++assert_fail 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
+@@ -173,7 +173,7 @@ 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
++assert_fail 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"
diff -Nru flatpak-1.16.6/portal/flatpak-portal-app-info.c flatpak-1.16.6/portal/flatpak-portal-app-info.c
--- flatpak-1.16.6/portal/flatpak-portal-app-info.c	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/portal/flatpak-portal-app-info.c	2026-08-11 14:32:06.000000000 +0100
@@ -33,7 +33,8 @@
 {
   if (app_infos == NULL)
     app_infos = g_hash_table_new_full (g_str_hash, g_str_equal,
-                                       NULL, (GDestroyNotify) g_key_file_unref);
+                                       g_free,
+                                       (GDestroyNotify) g_key_file_unref);
 }
 
 static GKeyFile *
@@ -62,7 +63,7 @@
 add_cached_app_info_by_sender (const char *sender, GKeyFile *keyfile)
 {
   G_LOCK (app_infos);
-  g_hash_table_add (app_infos, g_key_file_ref (keyfile));
+  g_hash_table_insert (app_infos, g_strdup (sender), g_key_file_ref (keyfile));
   G_UNLOCK (app_infos);
 }
 
diff -Nru flatpak-1.16.6/revokefs/writer.c flatpak-1.16.6/revokefs/writer.c
--- flatpak-1.16.6/revokefs/writer.c	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/revokefs/writer.c	2026-08-11 14:32:06.000000000 +0100
@@ -198,58 +198,69 @@
   return request_path_data (writer_socket, op, path1, path2, strlen(path2), 0);
 }
 
-static gboolean
-validate_path (char *path)
+/* Return an fd to the parent directory of path guaranteed to be under
+ * basefd, and set *out_name to the final path component. */
+static int
+chase_parent (guchar *data, size_t len, char **out_name)
 {
-  char *end_segment;
+  g_autofree char *path = g_strndup ((const char *) data, len);
+  g_autofree char *parent = NULL;
+  g_autofree char *name = NULL;
+  const char *last_slash;
+  int fd;
 
-  /* No absolute or empty paths */
   if (*path == '/' || *path == 0)
-    return FALSE;
-
-  while (*path != 0)
     {
-      end_segment = strchr (path, '/');
-      if (end_segment == NULL)
-        end_segment = path + strlen (path);
-
-      if (strncmp (path, "..", 2) == 0)
-        return FALSE;
-
-      path = end_segment;
-      while (*path == '/')
-        path++;
+      g_printerr ("Invalid path: %s\n", path);
+      exit (1);
     }
 
-  return TRUE;
-}
+  last_slash = strrchr (path, '/');
+  if (last_slash)
+    {
+      parent = g_strndup (path, last_slash - path);
+      name = g_strdup (last_slash + 1);
+    }
+  else
+    {
+      parent = g_strdup (".");
+      name = g_strdup (path);
+    }
 
-static char *
-get_valid_path (guchar *data, size_t len)
-{
-  char *path = g_strndup ((const char *) data, len);
+  if (name[0] == 0 || strcmp (name, ".") == 0 || strcmp (name, "..") == 0)
+    {
+      g_printerr ("Invalid path: %s\n", path);
+      exit (1);
+    }
 
-  if (!validate_path (path))
+  fd = glnx_chaseat (basefd, parent,
+                      GLNX_CHASE_RESOLVE_BENEATH |
+                      GLNX_CHASE_MUST_BE_DIRECTORY,
+                      NULL);
+  if (fd < 0)
     {
-      g_printerr ("Invalid path argument %s\n", path);
+      g_printerr ("Path escapes base directory: %s\n", path);
       exit (1);
     }
 
-  return path;
+  if (out_name != NULL)
+    *out_name = g_steal_pointer (&name);
+  return fd;
 }
 
 static int
-mask_mode (int mode)
+chase_request_path (RevokefsRequest *request,
+                    gsize data_size,
+                    char **out_name)
 {
-  /* mask setuid, setgid and world-writable permissions bits */
-  return mode & ~S_ISUID & ~S_ISGID & ~(S_IWGRP | S_IWOTH);
+  return chase_parent (request->data, data_size, out_name);
 }
 
 static void
-get_any_path_and_valid_path (RevokefsRequest *request,
-                             gsize data_size,
-                             char **any_path1,
-                             char **valid_path2)
+chase_request_2path (RevokefsRequest *request,
+                     gsize data_size,
+                     int *out_fd1, char **out_name1,
+                     int *out_fd2, char **out_name2)
 {
   if (request->arg1 >= data_size)
     {
@@ -257,15 +268,16 @@
       exit (1);
     }
 
-  *any_path1 = g_strndup ((const char *) request->data, request->arg1);
-  *valid_path2 = get_valid_path (request->data + request->arg1, data_size - request->arg1);
+  *out_fd1 = chase_parent (request->data, request->arg1, out_name1);
+  *out_fd2 = chase_parent (request->data + request->arg1,
+                           data_size - request->arg1, out_name2);
 }
 
 static void
-get_valid_2path (RevokefsRequest *request,
-                 gsize data_size,
-                 char **path1,
-                 char **path2)
+chase_request_any_and_path (RevokefsRequest *request,
+                            gsize data_size,
+                            char **out_any_path,
+                            int *out_fd, char **out_name)
 {
   if (request->arg1 >= data_size)
     {
@@ -273,8 +285,16 @@
       exit (1);
     }
 
-  *path1 = get_valid_path (request->data, request->arg1);
-  *path2 = get_valid_path (request->data + request->arg1, data_size - request->arg1);
+  *out_any_path = g_strndup ((const char *) request->data, request->arg1);
+  *out_fd = chase_parent (request->data + request->arg1,
+                          data_size - request->arg1, out_name);
+}
+
+static int
+mask_mode (int mode)
+{
+  /* mask setuid, setgid and world-writable permissions bits */
+  return mode & ~S_ISUID & ~S_ISGID & ~(S_IWGRP | S_IWOTH);
 }
 
 static ssize_t
@@ -282,10 +302,11 @@
               gsize data_size,
               RevokefsResponse *response)
 {
-  g_autofree char *path = get_valid_path (request->data, data_size);
+  g_autofree char *name = NULL;
+  glnx_autofd int parent_fd = chase_request_path (request, data_size, &name);
   int mode = request->arg1;
 
-  if (mkdirat (basefd, path, mask_mode (mode)) == -1)
+  if (mkdirat (parent_fd, name, mask_mode (mode)) == -1)
     response->result = -errno;
   else
     response->result = 0;
@@ -304,9 +325,10 @@
               gsize data_size,
               RevokefsResponse *response)
 {
-  g_autofree char *path = get_valid_path (request->data, data_size);
+  g_autofree char *name = NULL;
+  glnx_autofd int parent_fd = chase_request_path (request, data_size, &name);
 
-  if (unlinkat (basefd, path, AT_REMOVEDIR) == -1)
+  if (unlinkat (parent_fd, name, AT_REMOVEDIR) == -1)
     response->result = -errno;
   else
     response->result = 0;
@@ -325,9 +347,10 @@
               gsize data_size,
               RevokefsResponse *response)
 {
-  g_autofree char *path = get_valid_path (request->data, data_size);
+  g_autofree char *name = NULL;
+  glnx_autofd int parent_fd = chase_request_path (request, data_size, &name);
 
-  if (unlinkat (basefd, path, 0) == -1)
+  if (unlinkat (parent_fd, name, 0) == -1)
     response->result = -errno;
   else
     response->result = 0;
@@ -347,12 +370,13 @@
                 RevokefsResponse *response)
 {
   g_autofree char *from = NULL;
-  g_autofree char *to = NULL;
+  g_autofree char *to_name = NULL;
+  glnx_autofd int to_parent_fd = -1;
 
-  /* from doesn't have to be a valid path, it can be absolute or whatever */
-  get_any_path_and_valid_path (request, data_size,  &from, &to);
+  chase_request_any_and_path (request, data_size,
+                              &from, &to_parent_fd, &to_name);
 
-  if (symlinkat (from, basefd, to) == -1)
+  if (symlinkat (from, to_parent_fd, to_name) == -1)
     response->result = -errno;
   else
     response->result = 0;
@@ -371,12 +395,16 @@
              gsize data_size,
              RevokefsResponse *response)
 {
-  g_autofree char *from = NULL;
-  g_autofree char *to = NULL;
-
-  get_valid_2path (request, data_size,  &from, &to);
+  g_autofree char *from_name = NULL;
+  g_autofree char *to_name = NULL;
+  glnx_autofd int from_parent_fd = -1;
+  glnx_autofd int to_parent_fd = -1;
+
+  chase_request_2path (request, data_size,
+                       &from_parent_fd, &from_name,
+                       &to_parent_fd, &to_name);
 
-  if (linkat (basefd, from, basefd, to, 0) == -1)
+  if (linkat (from_parent_fd, from_name, to_parent_fd, to_name, 0) == -1)
     response->result = -errno;
   else
     response->result = 0;
@@ -395,14 +423,18 @@
              gsize data_size,
              RevokefsResponse *response)
 {
-  g_autofree char *from = NULL;
-  g_autofree char *to = NULL;
+  g_autofree char *from_name = NULL;
+  g_autofree char *to_name = NULL;
+  glnx_autofd int from_parent_fd = -1;
+  glnx_autofd int to_parent_fd = -1;
   unsigned int flags;
 
-  get_valid_2path (request, data_size,  &from, &to);
+  chase_request_2path (request, data_size,
+                       &from_parent_fd, &from_name,
+                       &to_parent_fd, &to_name);
   flags = (unsigned int)request->arg2;
 
-  if (renameat2 (basefd, from, basefd, to, flags) == -1)
+  if (renameat2 (from_parent_fd, from_name, to_parent_fd, to_name, flags) == -1)
     response->result = -errno;
   else
     response->result = 0;
@@ -424,17 +456,30 @@
               gsize data_size,
               RevokefsResponse *response)
 {
-  g_autofree char *path = get_valid_path (request->data, data_size);
+  g_autofree char *name = NULL;
+  glnx_autofd int parent_fd = chase_request_path (request, data_size, &name);
+  glnx_autofd int fd = -1;
   int mode = request->arg1;
 
-  /* Note we can't use AT_SYMLINK_NOFOLLOW yet;
-   * https://marc.info/?l=linux-kernel&m=148830147803162&w=2
-   * https://marc.info/?l=linux-fsdevel&m=149193779929561&w=2
-   */
-  if (fchmodat (basefd, path, mask_mode (mode), 0) != 0)
-    response->result = -errno;
+  if (fchmodat (parent_fd, name, mask_mode (mode), AT_SYMLINK_NOFOLLOW) == 0)
+    {
+      response->result = 0;
+    }
+  else if (errno == ENOTSUP)
+    {
+      /* Linux < 6.6 didn't implement fchmodat() with AT_SYMLINK_NOFOLLOW */
+      fd = openat (parent_fd, name, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
+      if (fd == -1)
+        response->result = -errno;
+      else if (fchmod (fd, mask_mode (mode)) != 0)
+        response->result = -errno;
+      else
+        response->result = 0;
+    }
   else
-    response->result = 0;
+    {
+      response->result = -errno;
+    }
 
   return 0;
 }
@@ -450,11 +495,12 @@
               gsize data_size,
               RevokefsResponse *response)
 {
-  g_autofree char *path = get_valid_path (request->data, data_size);
+  g_autofree char *name = NULL;
+  glnx_autofd int parent_fd = chase_request_path (request, data_size, &name);
   uid_t uid = request->arg1;
   gid_t gid = request->arg2;
 
-  if (fchownat (basefd, path, uid, gid, AT_SYMLINK_NOFOLLOW) != 0)
+  if (fchownat (parent_fd, name, uid, gid, AT_SYMLINK_NOFOLLOW) != 0)
     response->result = -errno;
   else
     response->result = 0;
@@ -473,10 +519,12 @@
                  gsize data_size,
                  RevokefsResponse *response)
 {
-  g_autofree char *path = get_valid_path (request->data, data_size);
+  g_autofree char *name = NULL;
+  glnx_autofd int parent_fd = chase_request_path (request, data_size, &name);
   off_t size = request->arg1;
+  glnx_autofd int fd = -1;
 
-  glnx_autofd int fd = openat (basefd, path, O_NOFOLLOW|O_WRONLY);
+  fd = openat (parent_fd, name, O_NOFOLLOW | O_WRONLY);
   if (fd == -1)
     response->result = -errno;
   else
@@ -501,7 +549,8 @@
                 gsize data_size,
                 RevokefsResponse *response)
 {
-  g_autofree char *path = NULL;
+  g_autofree char *name = NULL;
+  glnx_autofd int parent_fd = -1;
   struct timespec *tv;
 
   if (request->arg1 + sizeof (struct timespec) * 2 != data_size)
@@ -510,10 +559,10 @@
       exit (1);
     }
 
-  path = get_valid_path (request->data, request->arg1);
+  parent_fd = chase_request_path (request, request->arg1, &name);
   tv = (struct timespec *)(request->data + request->arg1);
 
-  if (utimensat (basefd, path, tv, AT_SYMLINK_NOFOLLOW) == -1)
+  if (utimensat (parent_fd, name, tv, AT_SYMLINK_NOFOLLOW) == -1)
     response->result = -errno;
   else
     response->result = 0;
@@ -533,16 +582,14 @@
              gsize data_size,
              RevokefsResponse *response)
 {
-  g_autofree char *path = get_valid_path (request->data, data_size);
+  g_autofree char *name = NULL;
+  glnx_autofd int parent_fd = chase_request_path (request, data_size, &name);
   int mode = request->arg1;
   int flags = request->arg2;
   int fd;
 
-
-  /* We need to specially handle O_TRUNC. Also, Fuse should have already
-   * resolved symlinks, but use O_NOFOLLOW to be safe to avoid following
-   * symlinks to some other filesystem. */
-  fd = openat (basefd, path, (flags & ~O_TRUNC) | O_NOFOLLOW, mask_mode (mode));
+  /* We handle O_TRUNC separately, a bit later */
+  fd = openat (parent_fd, name, (flags & ~O_TRUNC) | O_NOFOLLOW, mask_mode (mode));
   if (fd == -1)
     response->result = -errno;
   else
@@ -747,14 +794,15 @@
                gsize data_size,
                RevokefsResponse *response)
 {
-  g_autofree char *path = get_valid_path (request->data, data_size);
+  g_autofree char *name = NULL;
+  glnx_autofd int parent_fd = chase_request_path (request, data_size, &name);
   int mode = request->arg1;
 
   /* Apparently at least GNU coreutils rm calls `faccessat(W_OK)`
    * before trying to do an unlink.  So...we'll just lie about
    * writable access here.
    */
-  if (faccessat (basefd, path, mode, AT_SYMLINK_NOFOLLOW) == -1)
+  if (faccessat (parent_fd, name, mode, AT_SYMLINK_NOFOLLOW) == -1)
     response->result = -errno;
   else
     response->result = 0;
diff -Nru flatpak-1.16.6/subprojects/dbus-proxy.wrap flatpak-1.16.6/subprojects/dbus-proxy.wrap
--- flatpak-1.16.6/subprojects/dbus-proxy.wrap	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/subprojects/dbus-proxy.wrap	2026-08-11 14:32:06.000000000 +0100
@@ -1,5 +1,5 @@
 [wrap-git]
 url = https://github.com/flatpak/xdg-dbus-proxy
-# 0.1.6
-revision = 1c1989e56f94b9eb3b7567f8a6e8a0aa16cba496
+# 0.1.7
+revision = 6a170fa77e3cbecb48f9dd2478fe5c0a119eb467
 depth = 1
diff -Nru flatpak-1.16.6/subprojects/.gitignore flatpak-1.16.6/subprojects/.gitignore
--- flatpak-1.16.6/subprojects/.gitignore	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/subprojects/.gitignore	2026-08-11 14:32:06.000000000 +0100
@@ -1,2 +1,3 @@
+/.wraplock
 bubblewrap/
 dbus-proxy/
diff -Nru flatpak-1.16.6/subprojects/libglnx/glnx-chase.c flatpak-1.16.6/subprojects/libglnx/glnx-chase.c
--- flatpak-1.16.6/subprojects/libglnx/glnx-chase.c	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/subprojects/libglnx/glnx-chase.c	2026-08-11 14:32:06.000000000 +0100
@@ -17,6 +17,7 @@
 #include <unistd.h>
 
 #include <glnx-backports.h>
+#include <glnx-dirfd.h>
 #include <glnx-errors.h>
 #include <glnx-fdio.h>
 #include <glnx-local-alloc.h>
@@ -46,6 +47,13 @@
 #define GLNX_CHASE_ALL_FLAGS \
   (GLNX_CHASE_ALL_DEBUG_FLAGS | GLNX_CHASE_ALL_REGULAR_FLAGS)
 
+typedef int (* GlnxChaseCallback)(int          next_fd,
+                                  int          current_fd,
+                                  const char  *segment,
+                                  int          open_tree_flags,
+                                  gpointer     user_data,
+                                  GError     **error);
+
 typedef GQueue GlnxStatxQueue;
 
 static void
@@ -328,10 +336,12 @@
  * we're in full control over the resolving.
  */
 static int
-chase_manual (int              dirfd,
-              const char      *path,
-              GlnxChaseFlags   flags,
-              GError         **error)
+chase_manual (int                 dirfd,
+              const char         *path,
+              GlnxChaseFlags      flags,
+              GlnxChaseCallback   callback,
+              gpointer            user_data,
+              GError            **error)
 {
   gboolean is_absolute;
   g_autofree char *buffer = NULL;
@@ -349,6 +359,7 @@
    * - 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)
+   * - there is no callback
    *
    * TODO: if we have a guarantee that the open_tree syscall works, we can
    * shortcut even without GLNX_CHASE_NO_AUTOMOUNT
@@ -356,7 +367,8 @@
   if ((flags & (GLNX_CHASE_NO_AUTOMOUNT |
                 GLNX_CHASE_RESOLVE_BENEATH |
                 GLNX_CHASE_RESOLVE_IN_ROOT |
-                GLNX_CHASE_RESOLVE_NO_SYMLINKS)) == GLNX_CHASE_NO_AUTOMOUNT)
+                GLNX_CHASE_RESOLVE_NO_SYMLINKS)) == GLNX_CHASE_NO_AUTOMOUNT &&
+      callback == NULL)
     {
       GlnxChaseFlags open_tree_flags =
         (flags & (GLNX_CHASE_NOFOLLOW | GLNX_CHASE_ALL_DEBUG_FLAGS));
@@ -466,10 +478,25 @@
         GlnxChaseFlags open_tree_flags =
           GLNX_CHASE_NOFOLLOW |
           (flags & (GLNX_CHASE_NO_AUTOMOUNT | GLNX_CHASE_ALL_DEBUG_FLAGS));
+        g_autoptr(GError) local_error = NULL;
+
+        next_fd = chase_open_tree (fd, segment, open_tree_flags, &local_error);
+
+        /* Note that the callback can be called with next_fd < 0.
+         * If so, the error is already set, but may be cleared by
+         * the callback if it can recover from an error that already
+         * occurred. */
+        if (callback)
+          {
+            next_fd = callback (next_fd, fd, segment, open_tree_flags,
+                                user_data, &local_error);
+          }
 
-        next_fd = chase_open_tree (fd, segment, open_tree_flags, error);
         if (next_fd < 0)
-          return -1;
+          {
+            g_propagate_error (error, g_steal_pointer (&local_error));
+            return -1;
+          }
       }
 
       if (!glnx_chase_statx (next_fd, no_automount, &st, error))
@@ -602,30 +629,13 @@
   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 int
+glnx_chaseat_full (int                 dirfd,
+                   const char         *path,
+                   GlnxChaseFlags      flags,
+                   GlnxChaseCallback   callback,
+                   gpointer            user_data,
+                   GError            **error)
 {
   static gboolean can_openat2 = TRUE;
   glnx_autofd int fd = -1;
@@ -643,15 +653,14 @@
     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)
+  if (can_openat2 &&
+      (flags & GLNX_CHASE_NO_AUTOMOUNT) == 0 &&
+      (flags & GLNX_CHASE_DEBUG_NO_OPENAT2) == 0 &&
+      callback == NULL)
     {
       uint64_t openat2_flags = 0;
       uint64_t openat2_resolve = 0;
@@ -690,7 +699,7 @@
 
   if (fd < 0)
     {
-      fd = chase_manual (dirfd, path, flags, error);
+      fd = chase_manual (dirfd, path, flags, callback, user_data, error);
       if (fd < 0)
         return -1;
     }
@@ -745,6 +754,34 @@
 }
 
 /**
+ * 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)
+{
+  return glnx_chaseat_full (dirfd, path, flags, NULL, NULL, error);
+}
+
+/**
  * glnx_chase_and_statxat:
  * @dirfd: a directory file descriptor
  * @path: a path
@@ -787,3 +824,89 @@
 
   return g_steal_fd (&fd);
 }
+
+static int
+chase_and_mkdir (int                 next_fd,
+                 int                 current_fd,
+                 const char         *segment,
+                 G_GNUC_UNUSED int   open_tree_flags,
+                 gpointer            user_data,
+                 GError            **error)
+{
+  mode_t mode = GPOINTER_TO_INT (user_data);
+  glnx_autofd int new_fd = -1;
+
+  /* if chase managed to get the next segment, we already got our answer */
+  if (next_fd >= 0)
+    return next_fd;
+
+  /* if the problem isn't that the file doesn't exist, we propagate the error */
+  g_assert (*error != NULL);
+  if (!g_error_matches (*error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+    return -1;
+  g_clear_error (error);
+
+  /* create the directory with the specified mode */
+  if (!glnx_ensure_dir (current_fd, segment, mode, error))
+    return -1;
+
+  /* Get a fd to the created dir.
+   * This is racy, meaning another process can modify the filesystem and we
+   * might open a directory that we did not create and might have a different
+   * mode. There isn't a kernel API which returns a fd or an inode which makes
+   * this race unavoidable. This is fine though because the semantics of
+   * `glnx_chase_and_mkdirat` accept arbitrary directories (even with different
+   * modes) when chasing the path.
+   */
+  new_fd = openat (current_fd, segment,
+                   O_CLOEXEC | O_PATH | O_DIRECTORY | O_NOFOLLOW);
+  if (new_fd < 0)
+    return glnx_fd_throw_errno_prefix (error, "opening created dir failed");
+
+  return g_steal_fd (&new_fd);
+}
+
+/**
+ * glnx_chase_and_mkdirat:
+ * @dirfd: a directory file descriptor
+ * @path: a path
+ * @flags: restricted combination of GlnxChaseFlags flags
+ * @mode: the mode for new directories
+ * @error: a #GError
+ *
+ * Same as glnx_chase with `GLNX_CHASE_MUST_BE_DIRECTORY`, but when a path
+ * segment does not exist, a directory is created for the segment with the mode
+ * @mode.
+ * This essentially implement a fd-relative equivalent of g_mkdir_with_parents()
+ * or `mkdir -p`.
+ * Note, that directories in the path which already exist can have arbitrary
+ * modes.
+ *
+ * See glnx_chaseat for the meaning of @dirfd and @path.
+ *
+ * The @flags argument is the same as in glnx_chaseat, but setting
+ * `GLNX_CHASE_MUST_BE_REGULAR`, `GLNX_CHASE_MUST_BE_SOCKET`, or
+ * `GLNX_CHASE_MUST_BE_DIRECTORY` is an error.
+ *
+ * Returns: the chased file, or -1 with @error set on error
+ */
+int
+glnx_chase_and_mkdirat (int              dirfd,
+                        const char      *path,
+                        GlnxChaseFlags   flags,
+                        mode_t           mode,
+                        GError         **error)
+{
+  g_return_val_if_fail ((flags & ~(GLNX_CHASE_ALL_FLAGS)) == 0, -1);
+  g_return_val_if_fail ((flags & (GLNX_CHASE_MUST_BE_REGULAR |
+                                  GLNX_CHASE_MUST_BE_SOCKET |
+                                  GLNX_CHASE_MUST_BE_DIRECTORY)) == 0, -1);
+  g_return_val_if_fail ((mode & ~((mode_t) 07777)) == 0, -1);
+
+  /* This function always implies GLNX_CHASE_MUST_BE_DIRECTORY */
+  flags |= GLNX_CHASE_MUST_BE_DIRECTORY;
+
+  return glnx_chaseat_full (dirfd, path, flags,
+                            chase_and_mkdir, GINT_TO_POINTER (mode),
+                            error);
+}
diff -Nru flatpak-1.16.6/subprojects/libglnx/glnx-chase.h flatpak-1.16.6/subprojects/libglnx/glnx-chase.h
--- flatpak-1.16.6/subprojects/libglnx/glnx-chase.h	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/subprojects/libglnx/glnx-chase.h	2026-08-11 14:32:06.000000000 +0100
@@ -48,4 +48,10 @@
                             struct glnx_statx  *statbuf,
                             GError            **error);
 
+int glnx_chase_and_mkdirat (int              dirfd,
+                            const char      *path,
+                            GlnxChaseFlags   flags,
+                            mode_t           mode,
+                            GError         **error);
+
 G_END_DECLS
diff -Nru flatpak-1.16.6/subprojects/libglnx/tests/test-libglnx-chase.c flatpak-1.16.6/subprojects/libglnx/tests/test-libglnx-chase.c
--- flatpak-1.16.6/subprojects/libglnx/tests/test-libglnx-chase.c	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/subprojects/libglnx/tests/test-libglnx-chase.c	2026-08-11 14:32:06.000000000 +0100
@@ -50,6 +50,18 @@
   return st.st_ino;
 }
 
+static mode_t
+get_mode (int fd)
+{
+  int r;
+  struct stat st;
+
+  r = fstatat (fd, "", &st, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW);
+  g_assert_cmpint (r, >=, 0);
+
+  return st.st_mode & 0777;
+}
+
 static char *
 get_abspath (int         dfd,
              const char *path)
@@ -586,6 +598,62 @@
   g_clear_fd (&chase_fd, NULL);
 }
 
+static void
+test_chase_and_mkdir (void)
+{
+  g_autoptr(GError) error = NULL;
+  glnx_autofd int dfd = -1;
+  glnx_autofd int file_fd = -1;
+  glnx_autofd int chase_fd = -1;
+  ino_t expected_ino;
+
+  g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "d1", 0755,
+                                              &dfd,
+                                              NULL, &error));
+  g_assert_no_error (error);
+
+  chase_fd = glnx_chase_and_mkdirat (AT_FDCWD, "d1/d2/d3",
+                                     GLNX_CHASE_DEFAULT,
+                                     0755,
+                                     &error);
+  g_assert_cmpint (chase_fd, >=, 0);
+  g_assert_no_error (error);
+
+  file_fd = openat (AT_FDCWD, "d1/d2/d3", O_PATH | O_CLOEXEC | O_NOFOLLOW);
+  g_assert_cmpint (file_fd, >=, 0);
+  expected_ino = get_ino (file_fd);
+
+  g_assert_cmpint (get_ino (chase_fd), ==, expected_ino);
+
+  g_assert_cmpint (get_mode (dfd), ==, 0755);
+  g_assert_cmpint (get_mode (chase_fd), ==, 0755);
+
+  g_clear_fd (&chase_fd, NULL);
+  g_clear_fd (&file_fd, NULL);
+
+  chase_fd = glnx_chase_and_mkdirat (AT_FDCWD, "d1/d4/d5",
+                                     GLNX_CHASE_DEFAULT,
+                                     0700,
+                                     &error);
+  g_assert_cmpint (chase_fd, >=, 0);
+  g_assert_no_error (error);
+
+  file_fd = openat (AT_FDCWD, "d1", O_PATH | O_CLOEXEC | O_NOFOLLOW);
+  g_assert_cmpint (file_fd, >=, 0);
+  g_assert_cmpint (get_mode (file_fd), ==, 0755);
+  g_clear_fd (&file_fd, NULL);
+
+  file_fd = openat (AT_FDCWD, "d1/d4", O_PATH | O_CLOEXEC | O_NOFOLLOW);
+  g_assert_cmpint (file_fd, >=, 0);
+  g_assert_cmpint (get_mode (file_fd), ==, 0700);
+  g_clear_fd (&file_fd, NULL);
+
+  file_fd = openat (AT_FDCWD, "d1/d4/d5", O_PATH | O_CLOEXEC | O_NOFOLLOW);
+  g_assert_cmpint (file_fd, >=, 0);
+  g_assert_cmpint (get_mode (file_fd), ==, 0700);
+  g_clear_fd (&file_fd, NULL);
+}
+
 int main (int argc, char **argv)
 {
   _GLNX_TEST_SCOPED_TEMP_DIR;
@@ -602,6 +670,7 @@
   g_test_add_func ("/chase-and-statxat-basic", test_chase_and_statxat_basic);
   g_test_add_func ("/chase-and-statxat-symlink", test_chase_and_statxat_symlink);
   g_test_add_func ("/chase-and-statxat-permissions", test_chase_and_statxat_permissions);
+  g_test_add_func ("/chase-and-mkdir", test_chase_and_mkdir);
 
   ret = g_test_run();
 
diff -Nru flatpak-1.16.6/system-helper/flatpak-system-helper.c flatpak-1.16.6/system-helper/flatpak-system-helper.c
--- flatpak-1.16.6/system-helper/flatpak-system-helper.c	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/system-helper/flatpak-system-helper.c	2026-08-11 14:32:06.000000000 +0100
@@ -432,6 +432,19 @@
       return G_DBUS_METHOD_INVOCATION_HANDLED;
     }
 
+  ref = flatpak_decomposed_new_from_ref (arg_ref, &error);
+  if (ref == NULL)
+    {
+      g_dbus_method_invocation_return_gerror (invocation, error);
+      return G_DBUS_METHOD_INVOCATION_HANDLED;
+    }
+
+  if (!flatpak_is_valid_remote_name (arg_origin, -1, &error))
+    {
+      g_dbus_method_invocation_return_gerror (invocation, error);
+      return G_DBUS_METHOD_INVOCATION_HANDLED;
+    }
+
   if (strlen (arg_repo_path) > 0)
     {
       g_autoptr(GError) local_error = NULL;
@@ -475,13 +488,6 @@
         }
     }
 
-  ref = flatpak_decomposed_new_from_ref (arg_ref, &error);
-  if (ref == NULL)
-    {
-      g_dbus_method_invocation_return_gerror (invocation, error);
-      return G_DBUS_METHOD_INVOCATION_HANDLED;
-    }
-
   no_deploy = (arg_flags & FLATPAK_HELPER_DEPLOY_FLAGS_NO_DEPLOY) != 0;
   local_pull = (arg_flags & FLATPAK_HELPER_DEPLOY_FLAGS_LOCAL_PULL) != 0;
   reinstall = (arg_flags & FLATPAK_HELPER_DEPLOY_FLAGS_REINSTALL) != 0;
@@ -783,6 +789,18 @@
       return G_DBUS_METHOD_INVOCATION_HANDLED;
     }
 
+  if (!flatpak_is_valid_remote_name (arg_origin, -1, &error))
+    {
+      g_dbus_method_invocation_return_gerror (invocation, error);
+      return G_DBUS_METHOD_INVOCATION_HANDLED;
+    }
+
+  if (!flatpak_is_valid_arch (arg_arch, -1, &error))
+    {
+      g_dbus_method_invocation_return_gerror (invocation, error);
+      return G_DBUS_METHOD_INVOCATION_HANDLED;
+    }
+
   if (strlen (arg_repo_path) > 0)
     {
       g_autoptr(GFile) repo_file = g_file_new_for_path (arg_repo_path);
@@ -1012,6 +1030,12 @@
       return G_DBUS_METHOD_INVOCATION_HANDLED;
     }
 
+  if (!flatpak_is_valid_remote_name (arg_remote, -1, &error))
+    {
+      g_dbus_method_invocation_return_gerror (invocation, error);
+      return G_DBUS_METHOD_INVOCATION_HANDLED;
+    }
+
   if (!g_file_query_exists (bundle_file, NULL))
     {
       g_dbus_method_invocation_return_error (invocation, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
@@ -1057,10 +1081,9 @@
       return G_DBUS_METHOD_INVOCATION_HANDLED;
     }
 
-  if (*arg_remote == 0 || strchr (arg_remote, '/') != NULL)
+  if (!flatpak_is_valid_remote_name (arg_remote, -1, &error))
     {
-      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
-                                             "Invalid remote name: %s", arg_remote);
+      g_dbus_method_invocation_return_gerror (invocation, error);
       return G_DBUS_METHOD_INVOCATION_HANDLED;
     }
 
@@ -1198,10 +1221,9 @@
       return G_DBUS_METHOD_INVOCATION_HANDLED;
     }
 
-  if (*arg_remote == 0 || strchr (arg_remote, '/') != NULL)
+  if (!flatpak_is_valid_remote_name (arg_remote, -1, &error))
     {
-      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
-                                             "Invalid remote name: %s", arg_remote);
+      g_dbus_method_invocation_return_gerror (invocation, error);
       return G_DBUS_METHOD_INVOCATION_HANDLED;
     }
 
@@ -1284,10 +1306,9 @@
       return G_DBUS_METHOD_INVOCATION_HANDLED;
     }
 
-  if (*arg_remote == 0 || strchr (arg_remote, '/') != NULL)
+  if (!flatpak_is_valid_remote_name (arg_remote, -1, &error))
     {
-      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
-                                             "Invalid remote name: %s", arg_remote);
+      g_dbus_method_invocation_return_gerror (invocation, error);
       return G_DBUS_METHOD_INVOCATION_HANDLED;
     }
 
@@ -1297,7 +1318,7 @@
       return G_DBUS_METHOD_INVOCATION_HANDLED;
     }
 
-  if (!flatpak_dir_remove_ref (system, arg_remote, arg_ref, NULL, &error))
+  if (!flatpak_dir_remove_undeployed_ref (system, arg_remote, arg_ref, NULL, &error))
     {
       flatpak_invocation_return_error (invocation, error, "Error removing ref");
       return G_DBUS_METHOD_INVOCATION_HANDLED;
@@ -1851,6 +1872,12 @@
       return G_DBUS_METHOD_INVOCATION_HANDLED;
     }
 
+  if (!flatpak_is_valid_remote_name (arg_origin, -1, &error))
+    {
+      g_dbus_method_invocation_return_gerror (invocation, error);
+      return G_DBUS_METHOD_INVOCATION_HANDLED;
+    }
+
   only_cached = (arg_flags & FLATPAK_HELPER_GENERATE_OCI_SUMMARY_FLAGS_ONLY_CACHED) != 0;
 
   if (!flatpak_dir_ensure_repo (system, NULL, &error))
diff -Nru flatpak-1.16.6/tests/libtest.sh flatpak-1.16.6/tests/libtest.sh
--- flatpak-1.16.6/tests/libtest.sh	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/tests/libtest.sh	2026-08-11 14:32:06.000000000 +0100
@@ -282,6 +282,15 @@
     } 3> /dev/null
 }
 
+assert_fail () {
+    if "$@"; then
+        { { local BASH_XTRACEFD=3; } 2> /dev/null
+            echo "Command '$*' should not have succeeded at $(basename ${BASH_SOURCE[1]}):${BASH_LINENO[0]}" >&2
+            exit 1
+        } 3> /dev/null
+    fi
+}
+
 export FL_GPG_HOMEDIR=${TEST_DATA_DIR}/gpghome
 export FL_GPG_HOMEDIR2=${TEST_DATA_DIR}/gpghome2
 mkdir -p ${FL_GPG_HOMEDIR}
diff -Nru flatpak-1.16.6/tests/meson.build flatpak-1.16.6/tests/meson.build
--- flatpak-1.16.6/tests/meson.build	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/tests/meson.build	2026-08-11 14:32:06.000000000 +0100
@@ -127,6 +127,7 @@
       portal_gdbus[1],
     ],
   }],
+  ['test-ref-utils', {}],
 ]
 
 foreach testcase : c_tests
diff -Nru flatpak-1.16.6/tests/test-ref-utils.c flatpak-1.16.6/tests/test-ref-utils.c
--- flatpak-1.16.6/tests/test-ref-utils.c	1970-01-01 01:00:00.000000000 +0100
+++ flatpak-1.16.6/tests/test-ref-utils.c	2026-08-11 14:32:06.000000000 +0100
@@ -0,0 +1,360 @@
+/* vi:set et sw=2 sts=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e-s:
+ * Copyright © 2020-2026 Collabora Ltd.
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include <glib.h>
+
+#include "flatpak.h"
+#include "flatpak-ref-utils-private.h"
+
+#include "tests/testlib.h"
+
+static void
+test_valid_arch (void)
+{
+  static const char * const good[] =
+  {
+    "i386",
+    "mips64el",
+    "x86_64",
+  };
+  static const char * const bad[] =
+  {
+    "",
+    "--overrule-some-option",
+    ".",
+    "..",
+    "a/b",
+    "x86-64",
+    "../path_traversal",
+    "path/../traversal",
+    "path_traversal/..",
+    "\xc3\xa1",   /* U+00E1 LATIN SMALL LETTER A WITH ACUTE */
+    "a\xc3\xa1",
+  };
+  static const struct
+  {
+    const char *name;
+    int len;
+  }
+  initially_good[] =
+  {
+    { "abc\xc3\xa1", 3 },
+    { "_/", 1 },
+  };
+
+  for (size_t i = 0; i < G_N_ELEMENTS (good); i++)
+    {
+      g_autoptr(GError) local_error = NULL;
+      gboolean ok = flatpak_is_valid_arch (good[i], -1, &local_error);
+
+      g_test_message ("good architecture \"%s\" -> %s",
+                      good[i], ok ? "OK" : local_error->message);
+      g_assert_no_error (local_error);
+      g_assert_true (ok);
+    }
+
+  for (size_t i = 0; i < G_N_ELEMENTS (initially_good); i++)
+    {
+      g_autoptr(GError) local_error = NULL;
+      gboolean ok = flatpak_is_valid_arch (initially_good[i].name,
+                                           initially_good[i].len,
+                                           &local_error);
+
+      g_test_message ("good architecture \"%.*s\" -> %s",
+                      initially_good[i].len,
+                      initially_good[i].name,
+                      ok ? "OK" : local_error->message);
+      g_assert_no_error (local_error);
+      g_assert_true (ok);
+    }
+
+  for (size_t i = 0; i < G_N_ELEMENTS (bad); i++)
+    {
+      g_autoptr(GError) local_error = NULL;
+      gboolean ok = flatpak_is_valid_arch (bad[i], -1, &local_error);
+
+      g_test_message ("bad architecture \"%s\" -> %s",
+                      bad[i], ok ? "wrongly accepted" : local_error->message);
+      g_assert_nonnull (local_error);
+      g_assert_false (ok);
+    }
+
+    {
+      g_autoptr(GError) local_error = NULL;
+      gboolean ok = flatpak_is_valid_arch (good[0], 0, &local_error);
+
+      g_test_message ("empty architecture -> %s",
+                      ok ? "wrongly accepted" : local_error->message);
+      g_assert_nonnull (local_error);
+      g_assert_false (ok);
+    }
+}
+
+static void
+test_valid_branch (void)
+{
+  static const char * const good[] =
+  {
+    "stable",
+    "private-beta",
+    "public_beta",
+    "1.0",
+    "--whatever--",
+    "x",
+  };
+  static const char * const bad[] =
+  {
+    "",
+    ".",
+    "..",
+    "a/b",
+    "../path-traversal",
+    "path-traversal/..",
+    "path/../traversal",
+    "\xc3\xa1",   /* U+00E1 LATIN SMALL LETTER A WITH ACUTE */
+    "a\xc3\xa1",
+  };
+  static const struct
+  {
+    const char *name;
+    int len;
+  }
+  initially_good[] =
+  {
+    { "abc\xc3\xa1", 3 },
+    { "x/", 1 },
+  };
+
+  for (size_t i = 0; i < G_N_ELEMENTS (good); i++)
+    {
+      g_autoptr(GError) local_error = NULL;
+      gboolean ok = flatpak_is_valid_branch (good[i], -1, &local_error);
+
+      g_test_message ("good branch \"%s\" -> %s",
+                      good[i], ok ? "OK" : local_error->message);
+      g_assert_no_error (local_error);
+      g_assert_true (ok);
+    }
+
+  for (size_t i = 0; i < G_N_ELEMENTS (initially_good); i++)
+    {
+      g_autoptr(GError) local_error = NULL;
+      gboolean ok = flatpak_is_valid_branch (initially_good[i].name,
+                                             initially_good[i].len,
+                                             &local_error);
+
+      g_test_message ("good branch \"%.*s\" -> %s",
+                      initially_good[i].len,
+                      initially_good[i].name,
+                      ok ? "OK" : local_error->message);
+      g_assert_no_error (local_error);
+      g_assert_true (ok);
+    }
+
+  for (size_t i = 0; i < G_N_ELEMENTS (bad); i++)
+    {
+      g_autoptr(GError) local_error = NULL;
+      gboolean ok = flatpak_is_valid_branch (bad[i], -1, &local_error);
+
+      g_test_message ("bad branch \"%s\" -> %s",
+                      bad[i], ok ? "wrongly accepted" : local_error->message);
+      g_assert_nonnull (local_error);
+      g_assert_false (ok);
+    }
+
+    {
+      g_autoptr(GError) local_error = NULL;
+      gboolean ok = flatpak_is_valid_branch (good[0], 0, &local_error);
+
+      g_test_message ("empty branch -> %s",
+                      ok ? "wrongly accepted" : local_error->message);
+      g_assert_nonnull (local_error);
+      g_assert_false (ok);
+    }
+}
+
+#define NAME_CHAR_x16 "z12345678.abcdef"
+#define NAME_CHAR_x32 NAME_CHAR_x16 NAME_CHAR_x16
+#define NAME_CHAR_x64 NAME_CHAR_x32 NAME_CHAR_x32
+#define NAME_CHAR_x128 NAME_CHAR_x64 NAME_CHAR_x64
+#define NAME_CHAR_x256 NAME_CHAR_x128 NAME_CHAR_x128
+
+static void
+test_valid_name (void)
+{
+  static const char * const good[] =
+  {
+    "com.example.Foo",
+    "a.b.c",
+    "org._7_zip.Archiver",
+    "org._7_zip._7-zip",    /* "-" in last element discouraged but allowed */
+    "uk.co.pseudorandom.name.has.many.components",
+  };
+  static const char * const bad[] =
+  {
+    "",
+    "a.b",        /* not enough elements */
+    "org.7_zip.Archiver",
+    "org._7-zip.Archiver",
+    "\xc3\xa1",   /* U+00E1 LATIN SMALL LETTER A WITH ACUTE */
+    "a\xc3\xa1",
+    NAME_CHAR_x256,
+  };
+  static const struct
+  {
+    const char *name;
+    int len;
+  }
+  initially_good[] =
+  {
+    { "a.b.c\xc3\xa1", 5 },
+    { NAME_CHAR_x256, 255 },
+  };
+
+  for (size_t i = 0; i < G_N_ELEMENTS (good); i++)
+    {
+      g_autoptr(GError) local_error = NULL;
+      gboolean ok = flatpak_is_valid_name (good[i], -1, &local_error);
+
+      g_test_message ("good app name \"%s\" -> %s",
+                      good[i], ok ? "OK" : local_error->message);
+      g_assert_no_error (local_error);
+      g_assert_true (ok);
+    }
+
+  for (size_t i = 0; i < G_N_ELEMENTS (initially_good); i++)
+    {
+      g_autoptr(GError) local_error = NULL;
+      gboolean ok = flatpak_is_valid_name (initially_good[i].name,
+                                           initially_good[i].len,
+                                           &local_error);
+
+      g_test_message ("good app name \"%.*s\" -> %s",
+                      initially_good[i].len,
+                      initially_good[i].name,
+                      ok ? "OK" : local_error->message);
+      g_assert_no_error (local_error);
+      g_assert_true (ok);
+    }
+
+  for (size_t i = 0; i < G_N_ELEMENTS (bad); i++)
+    {
+      g_autoptr(GError) local_error = NULL;
+      gboolean ok = flatpak_is_valid_name (bad[i], -1, &local_error);
+
+      g_test_message ("bad app name \"%s\" -> %s",
+                      bad[i], ok ? "wrongly accepted" : local_error->message);
+      g_assert_nonnull (local_error);
+      g_assert_false (ok);
+    }
+
+    {
+      g_autoptr(GError) local_error = NULL;
+      gboolean ok = flatpak_is_valid_name (good[0], 0, &local_error);
+
+      g_test_message ("empty app name -> %s",
+                      ok ? "wrongly accepted" : local_error->message);
+      g_assert_nonnull (local_error);
+      g_assert_false (ok);
+    }
+}
+
+static void
+test_valid_remote_name (void)
+{
+  static const char * const good[] =
+  {
+    "7zip",
+    "_abc123",
+    "a",
+    "a_b",
+    "remote-1.0",
+  };
+  static const char * const bad[] =
+  {
+    "",
+    "--overrule-some-option",
+    ".",
+    "..",
+    "a/b",
+    "../path-traversal",
+    "path/../traversal",
+    "path-traversal/..",
+    "\xc3\xa1",   /* U+00E1 LATIN SMALL LETTER A WITH ACUTE */
+    "a\xc3\xa1",
+  };
+  static const struct
+  {
+    const char *name;
+    int len;
+  }
+  initially_good[] =
+  {
+    { "abc\xc3\xa1", 3 },
+    { "x/", 1 },
+  };
+
+  for (size_t i = 0; i < G_N_ELEMENTS (good); i++)
+    {
+      g_autoptr(GError) local_error = NULL;
+      gboolean ok = flatpak_is_valid_remote_name (good[i], -1, &local_error);
+
+      g_test_message ("good remote name \"%s\" -> %s",
+                      good[i], ok ? "OK" : local_error->message);
+      g_assert_no_error (local_error);
+      g_assert_true (ok);
+    }
+
+  for (size_t i = 0; i < G_N_ELEMENTS (initially_good); i++)
+    {
+      g_autoptr(GError) local_error = NULL;
+      gboolean ok = flatpak_is_valid_remote_name (initially_good[i].name,
+                                                  initially_good[i].len,
+                                                  &local_error);
+
+      g_test_message ("good remote name \"%.*s\" -> %s",
+                      initially_good[i].len,
+                      initially_good[i].name,
+                      ok ? "OK" : local_error->message);
+      g_assert_no_error (local_error);
+      g_assert_true (ok);
+    }
+
+  for (size_t i = 0; i < G_N_ELEMENTS (bad); i++)
+    {
+      g_autoptr(GError) local_error = NULL;
+      gboolean ok = flatpak_is_valid_remote_name (bad[i], -1, &local_error);
+
+      g_test_message ("bad remote name \"%s\" -> %s",
+                      bad[i], ok ? "wrongly accepted" : local_error->message);
+      g_assert_nonnull (local_error);
+      g_assert_false (ok);
+    }
+
+    {
+      g_autoptr(GError) local_error = NULL;
+      gboolean ok = flatpak_is_valid_remote_name (good[0], 0, &local_error);
+
+      g_test_message ("empty remote name -> %s",
+                      ok ? "wrongly accepted" : local_error->message);
+      g_assert_nonnull (local_error);
+      g_assert_false (ok);
+    }
+}
+
+int
+main (int argc, char *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+
+  g_test_add_func ("/ref-utils/valid-arch", test_valid_arch);
+  g_test_add_func ("/ref-utils/valid-branch", test_valid_branch);
+  g_test_add_func ("/ref-utils/valid-name", test_valid_name);
+  g_test_add_func ("/ref-utils/valid-remote-name", test_valid_remote_name);
+
+  return g_test_run ();
+}
diff -Nru flatpak-1.16.6/tests/test-run-custom.sh flatpak-1.16.6/tests/test-run-custom.sh
--- flatpak-1.16.6/tests/test-run-custom.sh	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/tests/test-run-custom.sh	2026-08-11 14:32:06.000000000 +0100
@@ -51,7 +51,7 @@
 
 ok "setup"
 
-! run --app-path="" --command=/app/bin/hello.sh org.test.Hello > /dev/null
+assert_fail 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$'
@@ -59,7 +59,7 @@
 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
+assert_fail run --app-path="" --command=/run/parent/usr/bin/runtime_hello.sh org.test.Hello > /dev/null
 
 ok "empty app path"
 
@@ -72,11 +72,11 @@
 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
+assert_fail 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
+assert_fail run --app-path=path-which-does-not-exist org.test.Hello > /dev/null
 
 ok "bad custom app path"
 
@@ -85,7 +85,7 @@
 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
+assert_fail run --app-fd=3 --command=/app/bin/hello.sh org.test.Hello > /dev/null
 
 ok "custom app fd"
 
@@ -95,14 +95,14 @@
 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
+assert_fail 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
+assert_fail run --usr-path=path-which-does-not-exist org.test.Hello > /dev/null
 
 ok "bad custom usr path"
 
@@ -116,7 +116,7 @@
 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
+assert_fail run --usr-fd=3 --command=/app/bin/hello.sh org.test.Hello > /dev/null
 
 ok "custom usr fd"
 
@@ -138,8 +138,8 @@
 
 ok "custom usr and app path"
 
-! run --usr-path=custom-runtime/files --app-path="" \
-    --command=/app/bin/hello.sh org.test.Hello > /dev/null
+assert_fail 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
@@ -173,7 +173,7 @@
 exec 3>&-
 
 exec 3< "${path}"
-! run --ro-bind-fd=3 --command=bash org.test.Hello -c "echo baz > ${path}" > /dev/null
+assert_fail 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"
diff -Nru flatpak-1.16.6/tests/test-run.sh flatpak-1.16.6/tests/test-run.sh
--- flatpak-1.16.6/tests/test-run.sh	2026-04-10 18:28:42.000000000 +0100
+++ flatpak-1.16.6/tests/test-run.sh	2026-08-11 14:32:06.000000000 +0100
@@ -24,7 +24,7 @@
 skip_without_bwrap
 skip_revokefs_without_fuse
 
-echo "1..27"
+echo "1..29"
 
 # Use stable rather than master as the branch so we can test that the run
 # command automatically finds the branch correctly
@@ -595,6 +595,115 @@
 
 ok "--persist=. persists all files"
 
+# com.valvesoftware.Steam is an example of an app that wants to use the
+# canonical paths ~/.cache, ~/.config and ~/.local/share, to work around
+# games that don't support the XDG basedir spec properly
+app_dir="$HOME/.var/app/org.test.Hello"
+rm -fr "$app_dir"
+mkdir -p "$app_dir"
+for subdir in .cache .config .local .local/share; do
+    mkdir -m700 "$app_dir/$subdir"
+done
+# This is the symlink that com.valvesoftware.Steam uses to achieve that
+ln -fns .cache "$app_dir/cache"
+ln -fns .config "$app_dir/config"
+ln -fns .local/share "$app_dir/data"
+run --command=sh --persist=. org.test.Hello -c '
+echo "HOME=$HOME"
+echo "XDG_CACHE_HOME=$XDG_CACHE_HOME"
+echo "XDG_CONFIG_HOME=$XDG_CONFIG_HOME"
+echo "XDG_DATA_HOME=$XDG_DATA_HOME"
+echo "via-HOME" > ~/via-HOME
+echo "via-var-app" > ~/.var/app/org.test.Hello/via-var-app
+echo "via-XDG_CACHE_HOME" > "$XDG_CACHE_HOME/via-XDG_CACHE_HOME"
+echo "via-XDG_CONFIG_HOME" > "$XDG_CONFIG_HOME/via-XDG_CONFIG_HOME"
+echo "via-XDG_DATA_HOME" > "$XDG_DATA_HOME/via-XDG_DATA_HOME"
+echo "via-.cache" > ~/.cache/via-.cache
+echo "via-.config" > ~/.config/via-.config
+echo "via-.local-share" > ~/.local/share/via-.local-share
+echo "via-cache" > ~/cache/via-cache
+echo "via-config" > ~/config/via-config
+echo "via-data" > ~/data/via-data
+echo "via-var-cache" > /var/cache/via-var-cache
+echo "via-var-config" > /var/config/via-var-config
+echo "via-var-data" > /var/data/via-var-data
+' >&2
+# $HOME and $app_dir in the sandbox are both the same as $app_dir outside
+assert_file_has_content "$app_dir/via-HOME" '^via-HOME$'
+assert_file_has_content "$app_dir/via-var-app" '^via-var-app$'
+# $app_dir/.cache, $app_dir/cache, /var/cache are the same place
+assert_file_has_content "$app_dir/cache/via-XDG_CACHE_HOME" '^via-XDG_CACHE_HOME$'
+assert_file_has_content "$app_dir/cache/via-.cache" '^via-\.cache$'
+assert_file_has_content "$app_dir/cache/via-cache" '^via-cache$'
+assert_file_has_content "$app_dir/cache/via-var-cache" '^via-var-cache$'
+# Same for config
+assert_file_has_content "$app_dir/config/via-XDG_CONFIG_HOME" '^via-XDG_CONFIG_HOME$'
+assert_file_has_content "$app_dir/config/via-.config" '^via-\.config$'
+assert_file_has_content "$app_dir/config/via-config" '^via-config$'
+assert_file_has_content "$app_dir/config/via-var-config" '^via-var-config$'
+# Same for data, except the canonical name is .local/share
+assert_file_has_content "$app_dir/data/via-XDG_DATA_HOME" '^via-XDG_DATA_HOME$'
+assert_file_has_content "$app_dir/data/via-.local-share" '^via-\.local-share$'
+assert_file_has_content "$app_dir/data/via-data" '^via-data$'
+assert_file_has_content "$app_dir/data/via-var-data" '^via-var-data$'
+# The realpath of $HOME/.cache canonicalizes to (realpath of $HOME)/.cache.
+# Experimentally, this is the arrangement that is least likely to break
+# non-spec-compliant game engines
+run --command=sh --persist=. org.test.Hello -c 'readlink -f ~/.cache' > "$app_dir/cache/realpath"
+echo "$(readlink -f "$HOME")/.cache" > "$app_dir/cache/expected-realpath"
+diff -u "$app_dir/cache/expected-realpath" "$app_dir/cache/realpath" >&2
+# Similarly $XDG_CONFIG_HOME canonicalizes to $HOME/.config
+run --command=sh --persist=. org.test.Hello -c 'readlink -f ~/.config' > "$app_dir/config/realpath"
+echo "$(readlink -f "$HOME")/.config" > "$app_dir/config/expected-realpath"
+diff -u "$app_dir/config/expected-realpath" "$app_dir/config/realpath" >&2
+# Similarly $XDG_DATA_HOME canonicalizes to $HOME/.local/share
+run --command=sh --persist=. org.test.Hello -c 'readlink -f ~/.local/share' > "$app_dir/data/realpath"
+echo "$(readlink -f "$HOME")/.local/share" > "$app_dir/data/expected-realpath"
+diff -u "$app_dir/data/expected-realpath" "$app_dir/data/realpath" >&2
+rm -fr "$app_dir"
+
+ok "--persist=. behaving like com.valvesoftware.Steam"
+
+app_dir="$HOME/.var/app/org.test.Hello"
+inaccessible="${TEST_DATA_DIR}/inaccessible"
+
+for path in cache data config cache/tmp; do
+    rm -fr "$app_dir" "$inaccessible"
+    mkdir -p "$app_dir" "$inaccessible"
+    case "$path" in
+        (cache/tmp)
+            mkdir -m700 "$app_dir/cache"
+            escape=../../../../../inaccessible
+            test "$app_dir/cache/$escape" -ef "$inaccessible"
+            ;;
+        (*)
+            escape=../../../../inaccessible
+            test "$app_dir/$escape" -ef "$inaccessible"
+            ;;
+    esac
+    for target in "$inaccessible" "$escape"; do
+        # There are various ways that an app could manipulate the contents
+        # of its per-app directory so that the next run of the same app would
+        # have a sandbox escape, but for the purposes of this test we cheat
+        # by setting up the same situation from outside the sandbox.
+        ln -fns "$target" "$app_dir/$path"
+        # The symlink target has been chosen to make these paths,
+        # outside the container, equivalent
+        test "$app_dir/$path" -ef "$inaccessible"
+        # The "|| :" inside the command ensures that if the app runs, then
+        # it succeeds, and then the assert_fail assertion fails.
+        # This is done because in the exploitable situations, we want Flatpak
+        # to refuse to run the app at all
+        assert_fail run --command=sh org.test.Hello -c "echo pwned > ~/$path/pwned || :"
+        assert_fail test -e "$inaccessible/pwned"
+        assert_fail run --command=sh org.test.Hello -c "echo pwned > /var/${path#*/}/pwned || :"
+        assert_fail test -e "$inaccessible/pwned"
+    done
+done
+
+ok "various attempts to escape app directory via symlinks (GHSA-8688-9x26-hhxj)"
+
+rm -fr "${TEST_DATA_DIR}/inaccessible"
 mkdir "${TEST_DATA_DIR}/inaccessible"
 echo FOO > ${TEST_DATA_DIR}/inaccessible/secret-file
 rm -fr "$HOME/.var/app/org.test.Hello"
