From 6dde1ae8d04274881d2023f3f0b9c6e10554423a Mon Sep 17 00:00:00 2001 From: AlmaLinux RelEng Bot Date: Thu, 28 May 2026 13:05:39 -0400 Subject: [PATCH] import UBI flatpak-1.16.0-9.el10_2.1 --- ...-1-update-subtree-libglnx-2026-04-07.patch | 2470 +++++++++++++++++ ...ong-variable-in-runtime-fd-selection.patch | 38 + ...n-run-parent-app-when-using-app-path.patch | 34 + ...-after-creating-the-instance-id-pipe.patch | 31 + ...ing-in-flatpak-run-add-app-info-args.patch | 123 + ...assing-an-fd-numer-which-is-not-a-fd.patch | 28 + ...078-15-run-do-not-close-bind-ro-bind.patch | 32 + ...ame-fd-validation-for-all-fd-options.patch | 95 + ...-bind-fd-binds-after-all-other-binds.patch | 97 + ...o-read-from-expose-fds-expose-fds-ro.patch | 38 + ...26-34078-19-run-fix-backport-mistake.patch | 28 + ...t-flatpak-bwrap-add-args-data-fd-dup.patch | 61 + ...test-usr-path-usr-fd-app-path-app-fd.patch | 190 ++ ...n-custom-test-bind-fd-and-ro-bind-fd.patch | 56 + ...78-22-run-cope-with-an-empty-runtime.patch | 110 + ...n-t-assume-there-is-always-a-runtime.patch | 44 + ...a-sh-to-test-extra-data-installation.patch | 126 + ...dd-test-for-noruntime-extra-data-app.patch | 121 + ...078-26-utils-add-flatpak-set-cloexec.patch | 52 + ...t-mark-fd-arguments-as-close-on-exec.patch | 102 + ...move-flatpak-get-path-for-fd-to-here.patch | 179 ++ ...sandbox-expose-ro-fd-is-out-of-range.patch | 66 + ...6-34078-3-utils-add-flatpak-parse-fd.patch | 94 + ...ndbox-expose-fds-instead-of-erroring.patch | 65 + ...state-flatpak-get-path-for-fd-checks.patch | 69 + ...to-an-existing-temporary-ostree-repo.patch | 37 + ...arguments-do-not-leak-to-the-command.patch | 61 + ...app-context-never-close-fds-0-1-or-2.patch | 103 + ...actor-out-flatpak-accept-fd-argument.patch | 235 ++ ...wrap-use-glnx-close-fd-as-clear-func.patch | 48 + ...e-runtime-and-app-deploy-directories.patch | 915 ++++++ ...-6-run-add-usr-fd-and-app-fd-options.patch | 78 + ...n-add-ro-bind-fds-to-flatpak-run-app.patch | 116 + ...6-34078-8-run-add-ro-bind-fd-options.patch | 95 + ...fd-and-usr-fd-options-to-avoid-races.patch | 602 ++++ ...-cached-files-in-the-cache-directory.patch | 84 + ...t-follow-symlinks-in-local-open-file.patch | 50 + ...emove-an-ongoing-pull-if-users-match.patch | 150 + ...crypto-policy-if-it-is-enabled-on-th.patch | 75 +- flatpak.spec | 50 +- 40 files changed, 7013 insertions(+), 35 deletions(-) create mode 100644 CVE-2026-34078-1-update-subtree-libglnx-2026-04-07.patch create mode 100644 CVE-2026-34078-10-run-fix-checking-wrong-variable-in-runtime-fd-selection.patch create mode 100644 CVE-2026-34078-11-run-mount-original-app-on-run-parent-app-when-using-app-path.patch create mode 100644 CVE-2026-34078-12-portal-update-max-fd-after-creating-the-instance-id-pipe.patch create mode 100644 CVE-2026-34078-13-run-fix-fd-tracking-in-flatpak-run-add-app-info-args.patch create mode 100644 CVE-2026-34078-14-utils-improve-error-message-when-passing-an-fd-numer-which-is-not-a-fd.patch create mode 100644 CVE-2026-34078-15-run-do-not-close-bind-ro-bind.patch create mode 100644 CVE-2026-34078-16-run-use-the-same-fd-validation-for-all-fd-options.patch create mode 100644 CVE-2026-34078-17-run-add-bind-fd-and-ro-bind-fd-binds-after-all-other-binds.patch create mode 100644 CVE-2026-34078-18-portal-use-g-array-index-to-read-from-expose-fds-expose-fds-ro.patch create mode 100644 CVE-2026-34078-19-run-fix-backport-mistake.patch create mode 100644 CVE-2026-34078-2-flatpak-bwrap-add-dup-ing-variant-flatpak-bwrap-add-args-data-fd-dup.patch create mode 100644 CVE-2026-34078-20-tests-test-run-custom-test-usr-path-usr-fd-app-path-app-fd.patch create mode 100644 CVE-2026-34078-21-tests-test-run-custom-test-bind-fd-and-ro-bind-fd.patch create mode 100644 CVE-2026-34078-22-run-cope-with-an-empty-runtime.patch create mode 100644 CVE-2026-34078-23-dir-in-apply-extra-data-don-t-assume-there-is-always-a-runtime.patch create mode 100644 CVE-2026-34078-24-tests-add-test-extra-data-sh-to-test-extra-data-installation.patch create mode 100644 CVE-2026-34078-25-tests-add-test-for-noruntime-extra-data-app.patch create mode 100644 CVE-2026-34078-26-utils-add-flatpak-set-cloexec.patch create mode 100644 CVE-2026-34078-27-run-context-mark-fd-arguments-as-close-on-exec.patch create mode 100644 CVE-2026-34078-28-utils-move-flatpak-get-path-for-fd-to-here.patch create mode 100644 CVE-2026-34078-29-portal-avoid-crash-if-sandbox-expose-ro-fd-is-out-of-range.patch create mode 100644 CVE-2026-34078-3-utils-add-flatpak-parse-fd.patch create mode 100644 CVE-2026-34078-30-portal-log-and-ignore-unusable-sandbox-expose-fds-instead-of-erroring.patch create mode 100644 CVE-2026-34078-31-portal-reinstate-flatpak-get-path-for-fd-checks.patch create mode 100644 CVE-2026-34078-32-libtest-allow-adding-a-new-ref-to-an-existing-temporary-ostree-repo.patch create mode 100644 CVE-2026-34078-33-tests-check-that-flatpak-run-fd-arguments-do-not-leak-to-the-command.patch create mode 100644 CVE-2026-34078-34-app-context-never-close-fds-0-1-or-2.patch create mode 100644 CVE-2026-34078-35-app-context-factor-out-flatpak-accept-fd-argument.patch create mode 100644 CVE-2026-34078-4-flatpak-bwrap-use-glnx-close-fd-as-clear-func.patch create mode 100644 CVE-2026-34078-5-run-use-o-path-fds-for-the-runtime-and-app-deploy-directories.patch create mode 100644 CVE-2026-34078-6-run-add-usr-fd-and-app-fd-options.patch create mode 100644 CVE-2026-34078-7-run-add-ro-bind-fds-to-flatpak-run-app.patch create mode 100644 CVE-2026-34078-8-run-add-ro-bind-fd-options.patch create mode 100644 CVE-2026-34078-9-portal-use-bind-fd-app-fd-and-usr-fd-options-to-avoid-races.patch create mode 100644 CVE-2026-34079-1-utils-only-remove-cached-files-in-the-cache-directory.patch create mode 100644 CVE-2026-34079-2-utils-do-not-follow-symlinks-in-local-open-file.patch create mode 100644 CVE-2026-34079-3-system-helper-only-remove-an-ongoing-pull-if-users-match.patch diff --git a/CVE-2026-34078-1-update-subtree-libglnx-2026-04-07.patch b/CVE-2026-34078-1-update-subtree-libglnx-2026-04-07.patch new file mode 100644 index 0000000..cc08731 --- /dev/null +++ b/CVE-2026-34078-1-update-subtree-libglnx-2026-04-07.patch @@ -0,0 +1,2470 @@ +From 03caf87c1bc14f8d347c8d9911afa7eb1cccab4c Mon Sep 17 00:00:00 2001 +From: Simon McVittie +Date: Tue, 7 Apr 2026 20:39:41 +0100 +Subject: [PATCH 01/14] Update subtree: libglnx 2026-04-07 + +* fdio: Avoid relying on VLAs or gcc-specific constant-folding +* errors: Fix URL to an old libgsystem commit +* lockfile: Assert non-null path in make_lock_file for analyzers +* backports: Add g_clear_fd +* glnx-errors.h: add glnx_fd_throw[_*] variants +* fdio: Add glnx_fd_reopen +* local-alloc: Remove duplicate definition of glnx_unref_object +* fdio: Add glnx_statx +* chase: Add glnx_chaseat which functions similar to openat2 +* chase: Add glnx_chase_and_statxat + +Signed-off-by: Simon McVittie +--- + subprojects/libglnx/Makefile-libglnx.am | 2 + + subprojects/libglnx/glnx-backports.c | 72 ++ + subprojects/libglnx/glnx-backports.h | 29 + + subprojects/libglnx/glnx-chase.c | 789 ++++++++++++++++++ + subprojects/libglnx/glnx-chase.h | 51 ++ + subprojects/libglnx/glnx-errors.h | 14 +- + subprojects/libglnx/glnx-fdio.c | 82 +- + subprojects/libglnx/glnx-fdio.h | 36 + + subprojects/libglnx/glnx-local-alloc.h | 1 - + subprojects/libglnx/glnx-lockfile.c | 2 + + subprojects/libglnx/glnx-missing-syscall.h | 353 ++++++++ + subprojects/libglnx/libglnx.h | 1 + + subprojects/libglnx/meson.build | 2 + + subprojects/libglnx/tests/meson.build | 1 + + .../libglnx/tests/test-libglnx-chase.c | 609 ++++++++++++++ + subprojects/libglnx/tests/test-libglnx-fdio.c | 153 ++++ + 16 files changed, 2191 insertions(+), 6 deletions(-) + create mode 100644 subprojects/libglnx/glnx-chase.c + create mode 100644 subprojects/libglnx/glnx-chase.h + create mode 100644 subprojects/libglnx/tests/test-libglnx-chase.c + +diff --git a/subprojects/libglnx/Makefile-libglnx.am b/subprojects/libglnx/Makefile-libglnx.am +index 5934a2b6..2c6ad447 100644 +--- a/subprojects/libglnx/Makefile-libglnx.am ++++ b/subprojects/libglnx/Makefile-libglnx.am +@@ -37,6 +37,8 @@ libglnx_la_SOURCES = \ + $(libglnx_srcpath)/glnx-backport-testutils.c \ + $(libglnx_srcpath)/glnx-backports.h \ + $(libglnx_srcpath)/glnx-backports.c \ ++ $(libglnx_srcpath)/glnx-chase.h \ ++ $(libglnx_srcpath)/glnx-chase.c \ + $(libglnx_srcpath)/glnx-local-alloc.h \ + $(libglnx_srcpath)/glnx-local-alloc.c \ + $(libglnx_srcpath)/glnx-errors.h \ +diff --git a/subprojects/libglnx/glnx-backports.c b/subprojects/libglnx/glnx-backports.c +index d5b68ad0..69f9577c 100644 +--- a/subprojects/libglnx/glnx-backports.c ++++ b/subprojects/libglnx/glnx-backports.c +@@ -106,6 +106,78 @@ _glnx_strv_equal (const gchar * const *strv1, + } + #endif + ++#if !GLIB_CHECK_VERSION(2, 76, 0) ++gboolean ++_glnx_close (gint fd, ++ GError **error) ++{ ++ int res; ++ ++ /* Important: if @error is NULL, we must not do anything that is ++ * not async-signal-safe. ++ */ ++ res = close (fd); ++ ++ if (res == -1) ++ { ++ int errsv = errno; ++ ++ if (errsv == EINTR) ++ { ++ /* Just ignore EINTR for now; a retry loop is the wrong thing to do ++ * on Linux at least. Anyone who wants to add a conditional check ++ * for e.g. HP-UX is welcome to do so later... ++ * ++ * close_func_with_invalid_fds() in gspawn.c has similar logic. ++ * ++ * https://lwn.net/Articles/576478/ ++ * http://lkml.indiana.edu/hypermail/linux/kernel/0509.1/0877.html ++ * https://bugzilla.gnome.org/show_bug.cgi?id=682819 ++ * http://utcc.utoronto.ca/~cks/space/blog/unix/CloseEINTR ++ * https://sites.google.com/site/michaelsafyan/software-engineering/checkforeintrwheninvokingclosethinkagain ++ * ++ * `close$NOCANCEL()` in gstdioprivate.h, on macOS, ensures that the fd is ++ * closed even if it did return EINTR. ++ */ ++ return TRUE; ++ } ++ ++ if (error) ++ { ++ g_set_error_literal (error, G_FILE_ERROR, ++ g_file_error_from_errno (errsv), ++ g_strerror (errsv)); ++ } ++ ++ if (errsv == EBADF) ++ { ++ /* There is a bug. Fail an assertion. Note that this function is supposed to be ++ * async-signal-safe, but in case an assertion fails, all bets are already off. */ ++ if (fd >= 0) ++ { ++ /* Closing an non-negative, invalid file descriptor is a bug. The bug is ++ * not necessarily in the caller of _glnx_close(), but somebody else ++ * might have wrongly closed fd. In any case, there is a serious bug ++ * somewhere. */ ++ g_critical ("_glnx_close(fd:%d) failed with EBADF. The tracking of file descriptors got messed up", fd); ++ } ++ else ++ { ++ /* Closing a negative "file descriptor" is less problematic. It's still a nonsensical action ++ * from the caller. Assert against that too. */ ++ g_critical ("_glnx_close(fd:%d) failed with EBADF. This is not a valid file descriptor", fd); ++ } ++ } ++ ++ errno = errsv; ++ ++ return FALSE; ++ } ++ ++ return TRUE; ++} ++#endif ++ + #if !GLIB_CHECK_VERSION(2, 80, 0) + /* This function is called between fork() and exec() and hence must be + * async-signal-safe (see signal-safety(7)). */ +diff --git a/subprojects/libglnx/glnx-backports.h b/subprojects/libglnx/glnx-backports.h +index 09e4609a..9ec11e4c 100644 +--- a/subprojects/libglnx/glnx-backports.h ++++ b/subprojects/libglnx/glnx-backports.h +@@ -29,6 +29,7 @@ + #include + + #include ++#include + #include + + G_BEGIN_DECLS +@@ -53,6 +54,34 @@ G_BEGIN_DECLS + } G_STMT_END + #endif + ++#if !GLIB_CHECK_VERSION(2, 76, 0) ++gboolean _glnx_close (gint fd, ++ GError **error); ++#else ++#define _glnx_close g_close ++#endif ++ ++#if !GLIB_CHECK_VERSION(2, 76, 0) ++static inline gboolean ++g_clear_fd (int *fd_ptr, ++ GError **error) ++{ ++ int fd = *fd_ptr; ++ ++ *fd_ptr = -1; ++ ++ if (fd < 0) ++ return TRUE; ++ ++ /* Suppress "Not available before" warning */ ++ G_GNUC_BEGIN_IGNORE_DEPRECATIONS ++ /* This importantly calls _glnx_close to always get async-signal-safe if ++ * error == NULL */ ++ return _glnx_close (fd, error); ++ G_GNUC_END_IGNORE_DEPRECATIONS ++} ++#endif ++ + #if !GLIB_CHECK_VERSION(2, 40, 0) + #define g_info(...) g_log (G_LOG_DOMAIN, G_LOG_LEVEL_INFO, __VA_ARGS__) + #endif +diff --git a/subprojects/libglnx/glnx-chase.c b/subprojects/libglnx/glnx-chase.c +new file mode 100644 +index 00000000..9ad2fe3b +--- /dev/null ++++ b/subprojects/libglnx/glnx-chase.c +@@ -0,0 +1,789 @@ ++/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- ++ * ++ * Copyright (C) 2026 Red Hat, Inc. ++ * SPDX-License-Identifier: LGPL-2.1-or-later ++ * ++ * glnx_chaseat was inspired by systemd's chase ++ */ ++ ++#include "libglnx-config.h" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++ ++#include ++ ++#define AUTOFS_SUPER_MAGIC 0x0187 /* man fstatfs */ ++ ++#define GLNX_CHASE_DEBUG_NO_OPENAT2 (1U << 31) ++#define GLNX_CHASE_DEBUG_NO_OPEN_TREE (1U << 30) ++ ++#define GLNX_CHASE_ALL_DEBUG_FLAGS \ ++ (GLNX_CHASE_DEBUG_NO_OPENAT2 | \ ++ GLNX_CHASE_DEBUG_NO_OPEN_TREE) ++ ++#define GLNX_CHASE_ALL_REGULAR_FLAGS \ ++ (GLNX_CHASE_NO_AUTOMOUNT | \ ++ GLNX_CHASE_NOFOLLOW | \ ++ GLNX_CHASE_RESOLVE_BENEATH | \ ++ GLNX_CHASE_RESOLVE_IN_ROOT | \ ++ GLNX_CHASE_RESOLVE_NO_SYMLINKS | \ ++ GLNX_CHASE_MUST_BE_REGULAR | \ ++ GLNX_CHASE_MUST_BE_DIRECTORY | \ ++ GLNX_CHASE_MUST_BE_SOCKET) ++ ++#define GLNX_CHASE_ALL_FLAGS \ ++ (GLNX_CHASE_ALL_DEBUG_FLAGS | GLNX_CHASE_ALL_REGULAR_FLAGS) ++ ++typedef GQueue GlnxStatxQueue; ++ ++static void ++glnx_statx_queue_push (GlnxStatxQueue *queue, ++ const struct glnx_statx *st) ++{ ++ struct glnx_statx *copy; ++ ++ copy = g_memdup2 (st, sizeof (*st)); ++ g_queue_push_tail (queue, copy); ++} ++ ++static void ++glnx_statx_queue_free_element (gpointer element, ++ G_GNUC_UNUSED gpointer userdata) ++{ ++ g_free (element); ++} ++ ++static void ++glnx_statx_queue_free (GlnxStatxQueue *squeue) ++{ ++ GQueue *queue = (GQueue *) squeue; ++ ++ /* Same as g_queue_clear_full (queue, g_free), but works for <2.60 */ ++ g_queue_foreach (queue, glnx_statx_queue_free_element, NULL); ++ g_queue_clear (queue); ++} ++ ++G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(GlnxStatxQueue, glnx_statx_queue_free) ++ ++static gboolean ++glnx_statx_inode_same (const struct glnx_statx *a, ++ const struct glnx_statx *b) ++{ ++ g_assert ((a->stx_mask & (GLNX_STATX_TYPE | GLNX_STATX_INO)) == ++ (GLNX_STATX_TYPE | GLNX_STATX_INO)); ++ g_assert ((b->stx_mask & (GLNX_STATX_TYPE | GLNX_STATX_INO)) == ++ (GLNX_STATX_TYPE | GLNX_STATX_INO)); ++ ++ return ((a->stx_mode ^ b->stx_mode) & S_IFMT) == 0 && ++ a->stx_dev_major == b->stx_dev_major && ++ a->stx_dev_minor == b->stx_dev_minor && ++ a->stx_ino == b->stx_ino; ++} ++ ++static gboolean ++glnx_statx_mount_same (const struct glnx_statx *a, ++ const struct glnx_statx *b) ++{ ++ g_assert ((a->stx_mask & (GLNX_STATX_MNT_ID | GLNX_STATX_MNT_ID_UNIQUE)) != 0); ++ g_assert ((b->stx_mask & (GLNX_STATX_MNT_ID | GLNX_STATX_MNT_ID_UNIQUE)) != 0); ++ ++ return a->stx_mnt_id == b->stx_mnt_id; ++} ++ ++static gboolean ++glnx_chase_statx (int dfd, ++ int additional_flags, ++ struct glnx_statx *buf, ++ GError **error) ++{ ++ if (!glnx_statx (dfd, "", ++ AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW | additional_flags, ++ GLNX_STATX_TYPE | GLNX_STATX_INO | ++ GLNX_STATX_MNT_ID | GLNX_STATX_MNT_ID_UNIQUE, ++ buf, ++ error)) ++ return FALSE; ++ ++ if ((buf->stx_mask & (GLNX_STATX_TYPE | GLNX_STATX_INO)) != ++ (GLNX_STATX_TYPE | GLNX_STATX_INO) || ++ (buf->stx_mask & (GLNX_STATX_MNT_ID | GLNX_STATX_MNT_ID_UNIQUE)) == 0) ++ { ++ errno = ENODATA; ++ return glnx_throw_errno_prefix (error, ++ "statx didn't return all required fields"); ++ } ++ ++ return TRUE; ++} ++ ++/* TODO: procfs magiclinks handling */ ++ ++/* open_tree subset which transparently falls back to openat. ++ * ++ * Returned fd is always OPATH and CLOEXEC. ++ * ++ * With NO_AUTOMOUNT this function never triggers automounts. Otherwise, it only ++ * guarantees to trigger an automount which is on last segment of the path! ++ * ++ * flags can be a combinations of: ++ * - GLNX_CHASE_NO_AUTOMOUNT ++ * - GLNX_CHASE_NOFOLLOW ++ */ ++static int ++chase_open_tree (int dirfd, ++ const char *path, ++ GlnxChaseFlags flags, ++ GError **error) ++{ ++ glnx_autofd int fd = -1; ++ static gboolean can_open_tree = TRUE; ++ unsigned int openat_flags = 0; ++ ++ g_assert ((flags & ~(GLNX_CHASE_NO_AUTOMOUNT | ++ GLNX_CHASE_NOFOLLOW | ++ GLNX_CHASE_ALL_DEBUG_FLAGS)) == 0); ++ ++ /* First we try to actually use open_tree, and then fall back to the impl ++ * using openat. ++ * Technically racy (static, not synced), but both paths work fine so it ++ * doesn't matter. */ ++ if (can_open_tree && (flags & GLNX_CHASE_DEBUG_NO_OPEN_TREE) == 0) ++ { ++ unsigned int open_tree_flags = 0; ++ ++ open_tree_flags = OPEN_TREE_CLOEXEC; ++ if ((flags & GLNX_CHASE_NOFOLLOW) != 0) ++ open_tree_flags |= AT_SYMLINK_NOFOLLOW; ++ if ((flags & GLNX_CHASE_NO_AUTOMOUNT) != 0) ++ open_tree_flags |= AT_NO_AUTOMOUNT; ++ ++ fd = open_tree (dirfd, path, open_tree_flags); ++ ++ /* If open_tree is not supported, or blocked (EPERM), we fall back to ++ * openat */ ++ if (fd < 0 && G_IN_SET (errno, ++ EOPNOTSUPP, ++ ENOTTY, ++ ENOSYS, ++ EAFNOSUPPORT, ++ EPFNOSUPPORT, ++ EPROTONOSUPPORT, ++ ESOCKTNOSUPPORT, ++ ENOPROTOOPT, ++ EPERM)) ++ can_open_tree = FALSE; ++ else if (fd < 0) ++ return glnx_fd_throw_errno_prefix (error, "open_tree"); ++ else ++ return g_steal_fd (&fd); ++ } ++ ++ openat_flags = O_CLOEXEC | O_PATH; ++ if ((flags & GLNX_CHASE_NOFOLLOW) != 0) ++ openat_flags |= O_NOFOLLOW; ++ ++ fd = openat (dirfd, path, openat_flags); ++ if (fd < 0) ++ return glnx_fd_throw_errno_prefix (error, "openat in open_tree fallback"); ++ ++ /* openat does not trigger automounts, so we have to manually do so ++ * unless NO_AUTOMOUNT was specified */ ++ if ((flags & GLNX_CHASE_NO_AUTOMOUNT) == 0) ++ { ++ struct statfs stfs; ++ ++ if (fstatfs (fd, &stfs) < 0) ++ return glnx_fd_throw_errno_prefix (error, "fstatfs in open_tree fallback"); ++ ++ /* fstatfs(2) can then be used to determine if it is, in fact, an ++ * untriggered automount point (.f_type == AUTOFS_SUPER_MAGIC). */ ++ if (stfs.f_type == AUTOFS_SUPER_MAGIC) ++ { ++ glnx_autofd int new_fd = -1; ++ ++ new_fd = openat (fd, ".", openat_flags | O_DIRECTORY); ++ /* For some reason, openat with O_PATH | O_DIRECTORY does trigger ++ * automounts, without us having to actually open the file, so let's ++ * use this here. It only works for directories though. */ ++ if (new_fd >= 0) ++ return g_steal_fd (&new_fd); ++ ++ if (errno != ENOTDIR) ++ return glnx_fd_throw_errno_prefix (error, "openat(O_DIRECTORY) in autofs mount open_tree fallback"); ++ ++ /* The automount is a directory, so let's try to open the file, ++ * which can fail because we are missing permissions, but that's ++ * okay, we only need to trigger automount. */ ++ new_fd = openat (fd, ".", (openat_flags & ~O_PATH) | ++ O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY); ++ glnx_close_fd (&new_fd); ++ ++ /* And try again with O_PATH */ ++ new_fd = openat (dirfd, path, openat_flags); ++ if (new_fd < 0) ++ return glnx_fd_throw_errno_prefix (error, "reopening in autofs mount open_tree fallback"); ++ ++ if (fstatfs (new_fd, &stfs) < 0) ++ return glnx_fd_throw_errno_prefix (error, "fstatfs in autofs mount open_tree fallback"); ++ ++ /* bail if we didn't manage to trigger the automount */ ++ if (stfs.f_type == AUTOFS_SUPER_MAGIC) ++ { ++ errno = EOPNOTSUPP; ++ return glnx_fd_throw_errno_prefix (error, "unable to trigger automount"); ++ } ++ ++ return g_steal_fd (&new_fd); ++ } ++ } ++ ++ return g_steal_fd (&fd); ++} ++ ++static int ++open_cwd (GlnxChaseFlags flags, ++ GError **error) ++{ ++ GLNX_AUTO_PREFIX_ERROR ("cannot open working directory", error); ++ ++ /* NO_AUTOMOUNT should be fine here because automount must have been ++ * triggered already for the CWD */ ++ return chase_open_tree (AT_FDCWD, ".", ++ (flags & GLNX_CHASE_ALL_DEBUG_FLAGS) | ++ GLNX_CHASE_NO_AUTOMOUNT | ++ GLNX_CHASE_NOFOLLOW, ++ error); ++} ++ ++static int ++open_root (GlnxChaseFlags flags, ++ GError **error) ++{ ++ GLNX_AUTO_PREFIX_ERROR ("cannot open root directory", error); ++ ++ /* NO_AUTOMOUNT should be fine here because automount must have been ++ * triggered already for the root */ ++ return chase_open_tree (AT_FDCWD, "/", ++ (flags & GLNX_CHASE_ALL_DEBUG_FLAGS) | ++ GLNX_CHASE_NO_AUTOMOUNT | ++ GLNX_CHASE_NOFOLLOW, ++ error); ++} ++ ++/* This returns the next segment of a path and tells us if it is the last ++ * segment. ++ * ++ * Importantly, a segment is anything after a "/", even if it is empty or ".". ++ * ++ * For example: ++ * "" -> "" ++ * "/" -> "" ++ * "////" -> "" ++ * "foo/bar" -> "foo", "bar" ++ * "foo//bar" -> "foo", "bar" ++ * "///foo//bar" -> "foo", "bar" ++ * "///foo//bar/" -> "foo", "bar", "" ++ * "///foo//bar/." -> "foo", "bar", "." ++ */ ++static char * ++extract_next_segment (const char **remaining, ++ gboolean *is_last) ++{ ++ const char *r = *remaining; ++ const char *s; ++ size_t len = 0; ++ ++ while (r[0] != '\0' && G_IS_DIR_SEPARATOR (r[0])) ++ r++; ++ ++ s = r; ++ ++ while (r[0] != '\0' && !G_IS_DIR_SEPARATOR (r[0])) ++ { ++ r++; ++ len++; ++ } ++ ++ *is_last = (r[0] == '\0'); ++ *remaining = r; ++ return g_strndup (s, len); ++} ++ ++/* This iterates over the segments of path and opens the corresponding ++ * directories or files. This gives us the opportunity to implement openat2 ++ * like RESOLVE_ semantics, without actually needing openat2. ++ * It also allows us to implement features which openat2 does not have because ++ * we're in full control over the resolving. ++ */ ++static int ++chase_manual (int dirfd, ++ const char *path, ++ GlnxChaseFlags flags, ++ GError **error) ++{ ++ gboolean is_absolute; ++ g_autofree char *buffer = NULL; ++ const char *remaining; ++ glnx_autofd int owned_root_fd = -1; ++ int root_fd; ++ glnx_autofd int owned_fd = -1; ++ int fd; ++ int remaining_follows = GLNX_CHASE_MAX; ++ struct glnx_statx st; ++ g_auto(GlnxStatxQueue) path_st = G_QUEUE_INIT; ++ int no_automount; ++ ++ /* Take a shortcut if ++ * - none of the resolve flags are set (they would require work here) ++ * - NO_AUTOMOUNT is set (chase_open_tree only triggers the automount for ++ * last component in some cases) ++ * ++ * TODO: if we have a guarantee that the open_tree syscall works, we can ++ * shortcut even without GLNX_CHASE_NO_AUTOMOUNT ++ */ ++ if ((flags & (GLNX_CHASE_NO_AUTOMOUNT | ++ GLNX_CHASE_RESOLVE_BENEATH | ++ GLNX_CHASE_RESOLVE_IN_ROOT | ++ GLNX_CHASE_RESOLVE_NO_SYMLINKS)) == GLNX_CHASE_NO_AUTOMOUNT) ++ { ++ GlnxChaseFlags open_tree_flags = ++ (flags & (GLNX_CHASE_NOFOLLOW | GLNX_CHASE_ALL_DEBUG_FLAGS)); ++ ++ return chase_open_tree (dirfd, path, open_tree_flags, error); ++ } ++ ++ no_automount = (flags & GLNX_CHASE_NO_AUTOMOUNT) != 0 ? AT_NO_AUTOMOUNT : 0; ++ ++ is_absolute = g_path_is_absolute (path); ++ ++ if (is_absolute && (flags & GLNX_CHASE_RESOLVE_BENEATH) != 0) ++ { ++ /* Absolute paths always get rejected with RESOLVE_BENEATH with errno ++ * EXDEV */ ++ ++ errno = EXDEV; ++ return glnx_fd_throw_errno_prefix (error, "absolute path not allowed for RESOLVE_BENEATH"); ++ } ++ else if (!is_absolute || ++ (is_absolute && (flags & GLNX_CHASE_RESOLVE_IN_ROOT) != 0)) ++ { ++ /* The absolute path is relative to dirfd with GLNX_CHASE_RESOLVE_IN_ROOT, ++ * and a relative path is always relative. */ ++ ++ /* In both cases we use dirfd as our chase root */ ++ if (dirfd == AT_FDCWD) ++ { ++ owned_root_fd = root_fd = open_cwd (flags, error); ++ if (root_fd < 0) ++ return -1; ++ } ++ else ++ { ++ root_fd = dirfd; ++ } ++ } ++ else ++ { ++ /* For absolute paths, we ignore dirfd, we use the actual root / for our ++ * chase root */ ++ g_assert (is_absolute); ++ ++ owned_root_fd = root_fd = open_root (flags, error); ++ if (root_fd < 0) ++ return -1; ++ } ++ ++ /* At this point, we always have (a relative) path, relative to root_fd */ ++ is_absolute = FALSE; ++ g_assert (root_fd >= 0); ++ ++ /* Add root to path_st, so we can verify if we get back to it */ ++ if (!glnx_chase_statx (root_fd, no_automount, &st, error)) ++ return -1; ++ ++ glnx_statx_queue_push (&path_st, &st); ++ ++ /* Let's start walking the path! */ ++ buffer = g_strdup (path); ++ remaining = buffer; ++ fd = root_fd; ++ ++ for (;;) ++ { ++ g_autofree char *segment = NULL; ++ gboolean is_last; ++ glnx_autofd int next_fd = -1; ++ ++ segment = extract_next_segment (&remaining, &is_last); ++ ++ /* If we encounter an empty segment ("", "."), we stay where we are and ++ * ignore the segment, or just exit if it is the last segment. */ ++ if (g_strcmp0 (segment, "") == 0 || g_strcmp0 (segment, ".") == 0) ++ { ++ if (is_last) ++ break; ++ continue; ++ } ++ ++ /* Special handling for going down the tree with RESOLVE_ flags */ ++ if (g_strcmp0 (segment, "..") == 0) ++ { ++ /* path_st contains the stat of the root if we're at root, so the ++ * length is 1 in that case, and going lower than the root is not ++ * allowed here! */ ++ ++ if (path_st.length <= 1 && (flags & GLNX_CHASE_RESOLVE_BENEATH) != 0) ++ { ++ /* With RESOLVE_BENEATH, error out if we would end up above the ++ * root fd */ ++ errno = EXDEV; ++ return glnx_fd_throw_errno_prefix (error, "attempted to traverse above root path via \"..\""); ++ } ++ else if (path_st.length <= 1 && (flags & GLNX_CHASE_RESOLVE_IN_ROOT) != 0) ++ { ++ /* With RESOLVE_IN_ROOT, we pretend that we hit the real root, ++ * and stay there, just like the kernel does. */ ++ continue; ++ } ++ } ++ ++ { ++ /* Open the next segment. We always use GLNX_CHASE_NOFOLLOW here to be ++ * able to ensure the RESOLVE flags, and automount behavior. */ ++ ++ GlnxChaseFlags open_tree_flags = ++ GLNX_CHASE_NOFOLLOW | ++ (flags & (GLNX_CHASE_NO_AUTOMOUNT | GLNX_CHASE_ALL_DEBUG_FLAGS)); ++ ++ next_fd = chase_open_tree (fd, segment, open_tree_flags, error); ++ if (next_fd < 0) ++ return -1; ++ } ++ ++ if (!glnx_chase_statx (next_fd, no_automount, &st, error)) ++ return -1; ++ ++ /* We resolve links if: they are not in the last component, or if they ++ * are the last component and NOFOLLOW is not set. */ ++ if (S_ISLNK (st.stx_mode) && ++ (!is_last || (flags & GLNX_CHASE_NOFOLLOW) == 0)) ++ { ++ g_autofree char *link = NULL; ++ g_autofree char *new_buffer = NULL; ++ ++ /* ...however, we do not resolve symlinks with NO_SYMLINKS, and use ++ * remaining_follows to ensure we don't loop forever. */ ++ if ((flags & GLNX_CHASE_RESOLVE_NO_SYMLINKS) != 0 || ++ --remaining_follows <= 0) ++ { ++ errno = ELOOP; ++ return glnx_fd_throw_errno_prefix (error, "followed too many symlinks"); ++ } ++ ++ /* AT_EMPTY_PATH is implied for readlinkat */ ++ link = glnx_readlinkat_malloc (next_fd, "", NULL, error); ++ if (!link) ++ return -1; ++ ++ if (g_path_is_absolute (link) && ++ (flags & GLNX_CHASE_RESOLVE_BENEATH) != 0) ++ { ++ errno = EXDEV; ++ return glnx_fd_throw_errno_prefix (error, "absolute symlink not allowed for RESOLVE_BENEATH"); ++ } ++ ++ /* The link can be absolute, and we handle that below, by changing the ++ * dirfd. The path *remains* and absolute path internally, but that is ++ * okay because we always interpret any path (even absolute ones) as ++ * being relative to the dirfd */ ++ new_buffer = g_strdup_printf ("%s/%s", link, remaining); ++ g_clear_pointer (&buffer, g_free); ++ buffer = g_steal_pointer (&new_buffer); ++ remaining = buffer; ++ ++ if (g_path_is_absolute (link)) ++ { ++ if ((flags & GLNX_CHASE_RESOLVE_IN_ROOT) != 0) ++ { ++ /* If the path was absolute, and RESOLVE_IN_ROOT is set, we ++ * will resolve the remaining path relative to root_fd */ ++ ++ g_clear_fd (&owned_fd, NULL); ++ fd = root_fd; ++ } ++ else ++ { ++ /* If the path was absolute, we will resolve the remaining ++ * path relative to the real root */ ++ ++ g_clear_fd (&owned_fd, NULL); ++ fd = owned_fd = open_root (flags, error); ++ if (fd < 0) ++ return -1; ++ } ++ ++ /* path_st must only contain the new root at this point */ ++ if (!glnx_chase_statx (fd, no_automount, &st, error)) ++ return -1; ++ ++ glnx_statx_queue_free (&path_st); ++ g_queue_init (&path_st); ++ glnx_statx_queue_push (&path_st, &st); ++ } ++ ++ continue; ++ } ++ ++ /* Either adds an element to path_st or removes one if we got down the ++ * tree. This also checks that going down the tree ends up at the inode ++ * we saw before (if we saw it before). */ ++ if (g_strcmp0 (segment, "..") == 0) ++ { ++ g_autofree struct glnx_statx *old_tail = NULL; ++ struct glnx_statx *lower_st; ++ ++ old_tail = g_queue_pop_tail (&path_st); ++ ++ lower_st = g_queue_peek_tail (&path_st); ++ if (lower_st && ++ (!glnx_statx_mount_same (&st, lower_st) || ++ !glnx_statx_inode_same (&st, lower_st))) ++ { ++ errno = EXDEV; ++ return glnx_fd_throw_errno_prefix (error, "a parent directory changed while traversing"); ++ } ++ } ++ else ++ { ++ glnx_statx_queue_push (&path_st, &st); ++ } ++ ++ /* There is still another path component, but the next fd is not a ++ * a directory. We need the fd to be a directory though, to open the next ++ * segment from. So bail with the appropriate error. */ ++ if (!is_last && !S_ISDIR (st.stx_mode)) ++ { ++ errno = ENOTDIR; ++ return glnx_fd_throw_errno_prefix (error, "a non-final path segment is not a directory"); ++ } ++ ++ g_clear_fd (&owned_fd, NULL); ++ fd = owned_fd = g_steal_fd (&next_fd); ++ ++ if (is_last) ++ break; ++ } ++ ++ /* We need an owned fd to return. Only having fd and not owned_fd can happen ++ * if we never finished a single iteration, or if an absolute path with ++ * RESOLVE_IN_ROOT makes us point at root_fd. ++ * We just re-open fd to always get an owned fd. ++ * Note that this only works because in all cases where owned_fd does not ++ * exists, fd is a directory. */ ++ if (owned_fd < 0) ++ { ++ owned_fd = openat (fd, ".", O_PATH | O_CLOEXEC | O_NOFOLLOW); ++ if (owned_fd < 0) ++ return glnx_fd_throw_errno_prefix (error, "reopening failed"); ++ } ++ ++ return g_steal_fd (&owned_fd); ++} ++ ++/** ++ * glnx_chaseat: ++ * @dirfd: a directory file descriptor ++ * @path: a path ++ * @flags: combination of GlnxChaseFlags flags ++ * @error: a #GError ++ * ++ * Behaves similar to openat, but with a number of differences: ++ * ++ * - All file descriptors which get returned are O_PATH and O_CLOEXEC. If you ++ * want to actually open the file for reading or writing, use glnx_fd_reopen, ++ * openat, or other at-style functions. ++ * - By default, automounts get triggered and the O_PATH fd will point to inodes ++ * in the newly mounted filesystem if an automount is encountered. This can be ++ * turned off with GLNX_CHASE_NO_AUTOMOUNT. ++ * - The GLNX_CHASE_RESOLVE_ flags can be used to safely deal with symlinks. ++ * ++ * Returns: the chased file, or -1 with @error set on error ++ */ ++int ++glnx_chaseat (int dirfd, ++ const char *path, ++ GlnxChaseFlags flags, ++ GError **error) ++{ ++ static gboolean can_openat2 = TRUE; ++ glnx_autofd int fd = -1; ++ ++ g_return_val_if_fail (dirfd >= 0 || dirfd == AT_FDCWD, -1); ++ g_return_val_if_fail (path != NULL, -1); ++ g_return_val_if_fail ((flags & ~(GLNX_CHASE_ALL_FLAGS)) == 0, -1); ++ g_return_val_if_fail (error == NULL || *error == NULL, -1); ++ ++ { ++ int must_flags = flags & (GLNX_CHASE_MUST_BE_REGULAR | ++ GLNX_CHASE_MUST_BE_DIRECTORY | ++ GLNX_CHASE_MUST_BE_SOCKET); ++ /* check that no more than one bit is set (= power of two) */ ++ g_return_val_if_fail ((must_flags & (must_flags - 1)) == 0, -1); ++ } ++ ++ /* TODO: Add a callback which is called for every resolved path segment, to ++ * allow users to verify and expand the functionality safely. */ ++ ++ /* We need the manual impl for NO_AUTOMOUNT, and we can skip this, if we don't ++ * have openat2 at all. ++ * Technically racy (static, not synced), but both paths work fine so it ++ * doesn't matter. */ ++ if (can_openat2 && (flags & GLNX_CHASE_NO_AUTOMOUNT) == 0 && ++ (flags & GLNX_CHASE_DEBUG_NO_OPENAT2) == 0) ++ { ++ uint64_t openat2_flags = 0; ++ uint64_t openat2_resolve = 0; ++ struct open_how how; ++ ++ openat2_flags = O_PATH | O_CLOEXEC; ++ if ((flags & GLNX_CHASE_NOFOLLOW) != 0) ++ openat2_flags |= O_NOFOLLOW; ++ ++ openat2_resolve |= RESOLVE_NO_MAGICLINKS; ++ if ((flags & GLNX_CHASE_RESOLVE_BENEATH) != 0) ++ openat2_resolve |= RESOLVE_BENEATH; ++ if ((flags & GLNX_CHASE_RESOLVE_IN_ROOT) != 0) ++ openat2_resolve |= RESOLVE_IN_ROOT; ++ if ((flags & GLNX_CHASE_RESOLVE_NO_SYMLINKS) != 0) ++ openat2_resolve |= RESOLVE_NO_SYMLINKS; ++ ++ how = (struct open_how) { ++ .flags = openat2_flags, ++ .mode = 0, ++ .resolve = openat2_resolve, ++ }; ++ ++ fd = openat2 (dirfd, path, &how, sizeof (how)); ++ if (fd < 0) ++ { ++ /* If the syscall is not implemented (ENOSYS) or blocked by ++ * seccomp (EPERM), we need to fall back to the manual path chasing ++ * via open_tree. */ ++ if (G_IN_SET (errno, ENOSYS, EPERM)) ++ can_openat2 = FALSE; ++ else ++ return glnx_fd_throw_errno (error); ++ } ++ } ++ ++ if (fd < 0) ++ { ++ fd = chase_manual (dirfd, path, flags, error); ++ if (fd < 0) ++ return -1; ++ } ++ ++ if ((flags & (GLNX_CHASE_MUST_BE_REGULAR | ++ GLNX_CHASE_MUST_BE_DIRECTORY | ++ GLNX_CHASE_MUST_BE_SOCKET)) != 0) ++ { ++ struct glnx_statx st; ++ ++ if (!glnx_statx (fd, "", ++ AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW | ++ ((flags & GLNX_CHASE_NO_AUTOMOUNT) ? AT_NO_AUTOMOUNT : 0), ++ GLNX_STATX_TYPE, ++ &st, ++ error)) ++ return -1; ++ ++ if ((st.stx_mask & GLNX_STATX_TYPE) == 0) ++ { ++ errno = ENODATA; ++ return glnx_fd_throw_errno_prefix (error, "unable to get file type"); ++ } ++ ++ if ((flags & GLNX_CHASE_MUST_BE_REGULAR) != 0 && ++ !S_ISREG (st.stx_mode)) ++ { ++ if (S_ISDIR (st.stx_mode)) ++ errno = EISDIR; ++ else ++ errno = EBADFD; ++ ++ return glnx_fd_throw_errno_prefix (error, "not a regular file"); ++ } ++ ++ if ((flags & GLNX_CHASE_MUST_BE_DIRECTORY) != 0 && ++ !S_ISDIR (st.stx_mode)) ++ { ++ errno = ENOTDIR; ++ return glnx_fd_throw_errno_prefix (error, "not a directory"); ++ } ++ ++ if ((flags & GLNX_CHASE_MUST_BE_SOCKET) != 0 && ++ !S_ISSOCK (st.stx_mode)) ++ { ++ errno = ENOTSOCK; ++ return glnx_fd_throw_errno_prefix (error, "not a socket"); ++ } ++ } ++ ++ return g_steal_fd (&fd); ++} ++ ++/** ++ * glnx_chase_and_statxat: ++ * @dirfd: a directory file descriptor ++ * @path: a path ++ * @flags: combination of GlnxChaseFlags flags ++ * @mask: combination of GLNX_STATX_ flags ++ * @statbuf: a pointer to a struct glnx_statx which will be filled out ++ * @error: a #GError ++ * ++ * Stats the file at @path relative to @dirfd and fills out @statbuf with the ++ * result according to the interest mask @mask. ++ * ++ * See glnx_chaseat for the meaning of @dirfd, @path, and @flags. ++ * ++ * Returns: the chased file, or -1 with @error set on error ++ */ ++int ++glnx_chase_and_statxat (int dirfd, ++ const char *path, ++ GlnxChaseFlags flags, ++ unsigned int mask, ++ struct glnx_statx *statbuf, ++ GError **error) ++{ ++ glnx_autofd int fd = -1; ++ ++ /* other args are checked by glnx_chaseat */ ++ g_return_val_if_fail (statbuf != NULL, FALSE); ++ ++ fd = glnx_chaseat (dirfd, path, flags, error); ++ if (fd < 0) ++ return -1; ++ ++ if (!glnx_statx (fd, "", ++ AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW | ++ ((flags & GLNX_CHASE_NO_AUTOMOUNT) ? AT_NO_AUTOMOUNT : 0), ++ mask, ++ statbuf, ++ error)) ++ return -1; ++ ++ return g_steal_fd (&fd); ++} +diff --git a/subprojects/libglnx/glnx-chase.h b/subprojects/libglnx/glnx-chase.h +new file mode 100644 +index 00000000..05dac12d +--- /dev/null ++++ b/subprojects/libglnx/glnx-chase.h +@@ -0,0 +1,51 @@ ++/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- ++ * ++ * Copyright (C) 2026 Red Hat, Inc. ++ * SPDX-License-Identifier: LGPL-2.1-or-later ++ */ ++ ++#pragma once ++ ++#include ++ ++typedef enum _GlnxChaseFlags { ++ /* Default */ ++ GLNX_CHASE_DEFAULT = 0, ++ /* Disable triggering of automounts */ ++ GLNX_CHASE_NO_AUTOMOUNT = 1 << 1, ++ /* Do not follow the path's right-most component. When the path's right-most ++ * component refers to symlink, return O_PATH fd of the symlink. */ ++ GLNX_CHASE_NOFOLLOW = 1 << 2, ++ /* Do not permit the path resolution to succeed if any component of the ++ * resolution is not a descendant of the directory indicated by dirfd. */ ++ GLNX_CHASE_RESOLVE_BENEATH = 1 << 3, ++ /* Symlinks are resolved relative to the given dirfd instead of root. */ ++ GLNX_CHASE_RESOLVE_IN_ROOT = 1 << 4, ++ /* Fail if any symlink is encountered. */ ++ GLNX_CHASE_RESOLVE_NO_SYMLINKS = 1 << 5, ++ /* Fail if the path's right-most component is not a regular file */ ++ GLNX_CHASE_MUST_BE_REGULAR = 1 << 6, ++ /* Fail if the path's right-most component is not a directory */ ++ GLNX_CHASE_MUST_BE_DIRECTORY = 1 << 7, ++ /* Fail if the path's right-most component is not a socket */ ++ GLNX_CHASE_MUST_BE_SOCKET = 1 << 8, ++} GlnxChaseFlags; ++ ++/* How many iterations to execute before returning ELOOP */ ++#define GLNX_CHASE_MAX 32 ++ ++G_BEGIN_DECLS ++ ++int glnx_chaseat (int dirfd, ++ const char *path, ++ GlnxChaseFlags flags, ++ GError **error); ++ ++int glnx_chase_and_statxat (int dirfd, ++ const char *path, ++ GlnxChaseFlags flags, ++ unsigned int mask, ++ struct glnx_statx *statbuf, ++ GError **error); ++ ++G_END_DECLS +diff --git a/subprojects/libglnx/glnx-errors.h b/subprojects/libglnx/glnx-errors.h +index e1dc3a56..5434a6f7 100644 +--- a/subprojects/libglnx/glnx-errors.h ++++ b/subprojects/libglnx/glnx-errors.h +@@ -32,6 +32,10 @@ gboolean glnx_throw (GError **error, const char *fmt, ...) G_GNUC_PRINTF (2,3); + #define glnx_null_throw(error, args...) \ + ({glnx_throw (error, args); NULL;}) + ++/* Like glnx_throw(), but yields -1 (invalid fd). */ ++#define glnx_fd_throw(error, args...) \ ++ ({glnx_throw (error, args); -1;}) ++ + /* Implementation detail of glnx_throw_prefix() */ + void glnx_real_set_prefix_error_va (GError *error, + const char *format, +@@ -97,7 +101,7 @@ glnx_throw_errno (GError **error) + g_strerror (errsv)); + /* We also restore the value of errno, since that's + * what was done in a long-ago libgsystem commit +- * https://git.gnome.org/browse/libgsystem/commit/?id=ed106741f7a0596dc8b960b31fdae671d31d666d ++ * https://gitlab.gnome.org/Archive/libgsystem/-/commit/ed106741f7a0596dc8b960b31fdae671d31d666d + * but I certainly can't remember now why I did that. + */ + errno = errsv; +@@ -108,6 +112,10 @@ glnx_throw_errno (GError **error) + #define glnx_null_throw_errno(error) \ + ({glnx_throw_errno (error); NULL;}) + ++/* Like glnx_throw_errno(), but yields -1 (invalid fd). */ ++#define glnx_fd_throw_errno(error) \ ++ ({glnx_throw_errno (error); -1;}) ++ + /* Implementation detail of glnx_throw_errno_prefix() */ + void glnx_real_set_prefix_error_from_errno_va (GError **error, + gint errsv, +@@ -120,6 +128,10 @@ gboolean glnx_throw_errno_prefix (GError **error, const char *fmt, ...) G_GNUC_P + #define glnx_null_throw_errno_prefix(error, args...) \ + ({glnx_throw_errno_prefix (error, args); NULL;}) + ++/* Like glnx_throw_errno_prefix(), but yields -1 (invalid fd). */ ++#define glnx_fd_throw_errno_prefix(error, args...) \ ++ ({glnx_throw_errno_prefix (error, args); -1;}) ++ + /* BEGIN LEGACY APIS */ + + #define glnx_set_error_from_errno(error) \ +diff --git a/subprojects/libglnx/glnx-fdio.c b/subprojects/libglnx/glnx-fdio.c +index 98732051..59261668 100644 +--- a/subprojects/libglnx/glnx-fdio.c ++++ b/subprojects/libglnx/glnx-fdio.c +@@ -321,6 +321,8 @@ glnx_open_anonymous_tmpfile (int flags, + error); + } + ++static const char proc_self_fd_slash[] = "/proc/self/fd/"; ++ + /* Use this after calling glnx_open_tmpfile_linkable_at() to give + * the file its final name (link into place). + */ +@@ -367,8 +369,8 @@ glnx_link_tmpfile_at (GLnxTmpfile *tmpf, + else + { + /* This case we have O_TMPFILE, so our reference to it is via /proc/self/fd */ +- char proc_fd_path[strlen("/proc/self/fd/") + DECIMAL_STR_MAX(tmpf->fd) + 1]; +- snprintf (proc_fd_path, sizeof (proc_fd_path), "/proc/self/fd/%i", tmpf->fd); ++ char proc_fd_path[sizeof (proc_self_fd_slash) + DECIMAL_STR_MAX(tmpf->fd)]; ++ snprintf (proc_fd_path, sizeof (proc_fd_path), "%s%i", proc_self_fd_slash, tmpf->fd); + + if (replace) + { +@@ -455,8 +457,8 @@ glnx_tmpfile_reopen_rdonly (GLnxTmpfile *tmpf, + else + { + /* This case we have O_TMPFILE, so our reference to it is via /proc/self/fd */ +- char proc_fd_path[strlen("/proc/self/fd/") + DECIMAL_STR_MAX(tmpf->fd) + 1]; +- snprintf (proc_fd_path, sizeof (proc_fd_path), "/proc/self/fd/%i", tmpf->fd); ++ char proc_fd_path[sizeof (proc_self_fd_slash) + DECIMAL_STR_MAX(tmpf->fd)]; ++ snprintf (proc_fd_path, sizeof (proc_fd_path), "%s%i", proc_self_fd_slash, tmpf->fd); + + if (!glnx_openat_rdonly (AT_FDCWD, proc_fd_path, TRUE, &rdonly_fd, error)) + return FALSE; +@@ -1205,3 +1207,75 @@ glnx_file_replace_contents_with_perms_at (int dfd, + + return TRUE; + } ++ ++/** ++ * glnx_fd_reopen: ++ * @fd: a file descriptor ++ * @flags: combination of openat flags ++ * @error: a #GError ++ * ++ * Reopens the specified fd with new flags. This is useful for converting an ++ * O_PATH fd into a regular one, or to turn O_RDWR fds into O_RDONLY fds. ++ * ++ * This implicitly sets `O_CLOEXEC | O_NOCTTY` in @flags. ++ * ++ * `O_CREAT` isn't allowed in @flags. ++ * ++ * This doesn't work on sockets (since they cannot be open()ed, ever). ++ * ++ * This implicitly resets the file read index to 0. ++ * ++ * If AT_FDCWD is specified as file descriptor, the function returns an fd to ++ * the current working directory. ++ * ++ * If the specified file descriptor refers to a symlink via O_PATH, then this ++ * function cannot be used to follow that symlink. Because we cannot have ++ * non-O_PATH fds to symlinks reopening it without O_PATH will always result in ++ * ELOOP. Or in other words: if you have an O_PATH fd to a symlink you can ++ * reopen it only if you pass O_PATH again. ++ */ ++int ++glnx_fd_reopen (int fd, ++ int flags, ++ GError **error) ++{ ++ glnx_autofd int new_fd = -1; ++ ++ g_return_val_if_fail (fd >= 0 || fd == AT_FDCWD, -1); ++ g_return_val_if_fail ((flags & O_CREAT) == 0, -1); ++ ++ /* */ ++ flags |= O_CLOEXEC | O_NOCTTY; ++ ++ /* O_NOFOLLOW is not allowed in fd_reopen(), because after all this is ++ * primarily implemented via a symlink-based interface in /proc/self/fd. Let's ++ * refuse this here early. Note that the kernel would generate ELOOP here too, ++ * hence this manual check is mostly redundant – the only reason we add it ++ * here is so that the O_DIRECTORY special case (see below) behaves the same ++ * way as the non-O_DIRECTORY case. */ ++ if ((flags & O_NOFOLLOW) != 0) ++ { ++ errno = ELOOP; ++ return glnx_fd_throw_errno (error); ++ } ++ ++ if ((flags & O_DIRECTORY) != 0 || fd == AT_FDCWD) ++ { ++ /* If we shall reopen the fd as directory we can just go via "." and thus ++ * bypass the whole magic /proc/ directory, and make ourselves independent ++ * of that being mounted. */ ++ new_fd = openat (fd, ".", flags | O_DIRECTORY); ++ } ++ else ++ { ++ g_autofree char *proc_fd_path = NULL; ++ ++ proc_fd_path = g_strdup_printf ("/proc/self/fd/%d", fd); ++ new_fd = open (proc_fd_path, flags); ++ } ++ ++ if (new_fd < 0) ++ return glnx_fd_throw_errno (error); ++ ++ return g_steal_fd (&new_fd); ++} +diff --git a/subprojects/libglnx/glnx-fdio.h b/subprojects/libglnx/glnx-fdio.h +index af534795..acd546df 100644 +--- a/subprojects/libglnx/glnx-fdio.h ++++ b/subprojects/libglnx/glnx-fdio.h +@@ -22,6 +22,7 @@ + #pragma once + + #include ++#include + #include + #include + #include +@@ -313,6 +314,37 @@ glnx_fstatat (int dfd, + return TRUE; + } + ++/** ++ * glnx_statx: ++ * @dfd: Directory FD to stat beneath ++ * @path: Path to stat beneath @dfd ++ * @flags: Flags to pass to statx() ++ * @mask: Mask to pass to statx() ++ * @buf: (out caller-allocates): Return location for statx details ++ * @error: Return location for a #GError, or %NULL ++ * ++ * Wrapper around statx() which adds #GError support and ensures that it ++ * retries on %EINTR. ++ * ++ * The mask to pass must be a combination of GLNX_STATX_* flags which are ++ * defined by glnx, which map up with the struct glnx_statx. ++ * ++ * Returns: %TRUE on success, or %FALSE setting both @error and `errno` ++ * Since: UNRELEASED ++ */ ++static inline gboolean ++glnx_statx (int dfd, ++ const char *path, ++ unsigned flags, ++ unsigned int mask, ++ struct glnx_statx *buf, ++ GError **error) ++{ ++ if (TEMP_FAILURE_RETRY (glnx_statx_syscall (dfd, path, flags, mask, buf)) != 0) ++ return glnx_throw_errno_prefix (error, "statx(%s)", path); ++ return TRUE; ++} ++ + /** + * glnx_fstatat_allow_noent: + * @dfd: Directory FD to stat beneath +@@ -383,4 +415,8 @@ glnx_unlinkat (int dfd, + return TRUE; + } + ++int glnx_fd_reopen (int fd, ++ int flags, ++ GError **error); ++ + G_END_DECLS +diff --git a/subprojects/libglnx/glnx-local-alloc.h b/subprojects/libglnx/glnx-local-alloc.h +index 65ae747f..a6e0e9b4 100644 +--- a/subprojects/libglnx/glnx-local-alloc.h ++++ b/subprojects/libglnx/glnx-local-alloc.h +@@ -43,7 +43,6 @@ glnx_local_obj_unref (void *v) + if (o) + g_object_unref (o); + } +-#define glnx_unref_object __attribute__ ((cleanup(glnx_local_obj_unref))) + + /* Backwards-compat with older libglnx */ + #define glnx_steal_fd g_steal_fd +diff --git a/subprojects/libglnx/glnx-lockfile.c b/subprojects/libglnx/glnx-lockfile.c +index fcda84cf..30e638c1 100644 +--- a/subprojects/libglnx/glnx-lockfile.c ++++ b/subprojects/libglnx/glnx-lockfile.c +@@ -66,6 +66,8 @@ glnx_make_lock_file(int dfd, const char *p, int operation, GLnxLockFile *out_loc + g_autofree char *t = NULL; + int r; + ++ g_return_val_if_fail (p != NULL, FALSE); ++ + /* + * We use UNPOSIX locks if they are available. They have nice + * semantics, and are mostly compatible with NFS. However, +diff --git a/subprojects/libglnx/glnx-missing-syscall.h b/subprojects/libglnx/glnx-missing-syscall.h +index c32a3f73..f3bdb3ba 100644 +--- a/subprojects/libglnx/glnx-missing-syscall.h ++++ b/subprojects/libglnx/glnx-missing-syscall.h +@@ -33,6 +33,7 @@ + + #include "libglnx-config.h" + #include ++#include + + #if !HAVE_DECL_RENAMEAT2 + # ifndef __NR_renameat2 +@@ -236,3 +237,355 @@ inline_close_range (unsigned int low, + #define close_range(low, high, flags) inline_close_range(low, high, flags) + #define HAVE_CLOSE_RANGE + #endif ++ ++#ifndef __IGNORE_statx ++# if defined(__aarch64__) ++# define systemd_NR_statx 291 ++# elif defined(__alpha__) ++# define systemd_NR_statx 522 ++# elif defined(__arc__) || defined(__tilegx__) ++# define systemd_NR_statx 291 ++# elif defined(__arm__) ++# define systemd_NR_statx 397 ++# elif defined(__i386__) ++# define systemd_NR_statx 383 ++# elif defined(__ia64__) ++# define systemd_NR_statx 1350 ++# elif defined(__loongarch_lp64) ++# define systemd_NR_statx 291 ++# elif defined(__m68k__) ++# define systemd_NR_statx 379 ++# elif defined(_MIPS_SIM) ++# if _MIPS_SIM == _MIPS_SIM_ABI32 ++# define systemd_NR_statx 4366 ++# elif _MIPS_SIM == _MIPS_SIM_NABI32 ++# define systemd_NR_statx 6330 ++# elif _MIPS_SIM == _MIPS_SIM_ABI64 ++# define systemd_NR_statx 5326 ++# else ++# error "Unknown MIPS ABI" ++# endif ++# elif defined(__hppa__) ++# define systemd_NR_statx 349 ++# elif defined(__powerpc__) ++# define systemd_NR_statx 383 ++# elif defined(__riscv) ++# if __riscv_xlen == 32 ++# define systemd_NR_statx 291 ++# elif __riscv_xlen == 64 ++# define systemd_NR_statx 291 ++# else ++# error "Unknown RISC-V ABI" ++# endif ++# elif defined(__s390__) ++# define systemd_NR_statx 379 ++# elif defined(__sparc__) ++# define systemd_NR_statx 360 ++# elif defined(__x86_64__) ++# if defined(__ILP32__) ++# define systemd_NR_statx (332 | /* __X32_SYSCALL_BIT */ 0x40000000) ++# else ++# define systemd_NR_statx 332 ++# endif ++# elif !defined(missing_arch_template) ++# warning "statx() syscall number is unknown for your architecture" ++# endif ++ ++/* may be an (invalid) negative number due to libseccomp, see PR 13319 */ ++# if defined __NR_statx && __NR_statx >= 0 ++# if defined systemd_NR_statx ++G_STATIC_ASSERT (__NR_statx == systemd_NR_statx); ++# endif ++# else ++# if defined __NR_statx ++# undef __NR_statx ++# endif ++# if defined systemd_NR_statx && systemd_NR_statx >= 0 ++# define __NR_statx systemd_NR_statx ++# endif ++# endif ++#endif ++ ++#if !defined(HAVE_GLNX_STATX) && defined(__NR_statx) ++#define GLNX_STATX_TYPE 0x00000001U /* Want/got stx_mode & S_IFMT */ ++#define GLNX_STATX_MODE 0x00000002U /* Want/got stx_mode & ~S_IFMT */ ++#define GLNX_STATX_NLINK 0x00000004U /* Want/got stx_nlink */ ++#define GLNX_STATX_UID 0x00000008U /* Want/got stx_uid */ ++#define GLNX_STATX_GID 0x00000010U /* Want/got stx_gid */ ++#define GLNX_STATX_ATIME 0x00000020U /* Want/got stx_atime */ ++#define GLNX_STATX_MTIME 0x00000040U /* Want/got stx_mtime */ ++#define GLNX_STATX_CTIME 0x00000080U /* Want/got stx_ctime */ ++#define GLNX_STATX_INO 0x00000100U /* Want/got stx_ino */ ++#define GLNX_STATX_SIZE 0x00000200U /* Want/got stx_size */ ++#define GLNX_STATX_BLOCKS 0x00000400U /* Want/got stx_blocks */ ++#define GLNX_STATX_BASIC_STATS 0x000007ffU /* The stuff in the normal stat struct */ ++#define GLNX_STATX_BTIME 0x00000800U /* Want/got stx_btime */ ++#define GLNX_STATX_MNT_ID 0x00001000U /* Got stx_mnt_id */ ++#define GLNX_STATX_DIOALIGN 0x00002000U /* Want/got direct I/O alignment info */ ++#define GLNX_STATX_MNT_ID_UNIQUE 0x00004000U /* Want/got extended stx_mount_id */ ++#define GLNX_STATX_SUBVOL 0x00008000U /* Want/got stx_subvol */ ++#define GLNX_STATX_WRITE_ATOMIC 0x00010000U /* Want/got atomic_write_* fields */ ++#define GLNX_STATX_DIO_READ_ALIGN 0x00020000U /* Want/got dio read alignment info */ ++#define GLNX_STATX__RESERVED 0x80000000U /* Reserved for future struct statx expansion */ ++ ++struct glnx_statx_timestamp ++{ ++ int64_t tv_sec; ++ uint32_t tv_nsec; ++ int32_t __reserved; ++}; ++ ++struct glnx_statx ++{ ++ uint32_t stx_mask; ++ uint32_t stx_blksize; ++ uint64_t stx_attributes; ++ uint32_t stx_nlink; ++ uint32_t stx_uid; ++ uint32_t stx_gid; ++ uint16_t stx_mode; ++ uint16_t __spare0[1]; ++ uint64_t stx_ino; ++ uint64_t stx_size; ++ uint64_t stx_blocks; ++ uint64_t stx_attributes_mask; ++ struct glnx_statx_timestamp stx_atime; ++ struct glnx_statx_timestamp stx_btime; ++ struct glnx_statx_timestamp stx_ctime; ++ struct glnx_statx_timestamp stx_mtime; ++ uint32_t stx_rdev_major; ++ uint32_t stx_rdev_minor; ++ uint32_t stx_dev_major; ++ uint32_t stx_dev_minor; ++ uint64_t stx_mnt_id; ++ uint32_t stx_dio_mem_align; ++ uint32_t stx_dio_offset_align; ++ uint64_t stx_subvol; ++ uint32_t stx_atomic_write_unit_min; ++ uint32_t stx_atomic_write_unit_max; ++ uint32_t stx_atomic_write_segments_max; ++ uint32_t stx_dio_read_offset_align; ++ uint32_t stx_atomic_write_unit_max_opt; ++ uint32_t __spare2[1]; ++ uint64_t __spare3[8]; ++}; ++ ++static inline int ++glnx_statx_syscall (int dfd, ++ const char *filename, ++ unsigned flags, ++ unsigned int mask, ++ struct glnx_statx *buf) ++{ ++ memset (buf, 0xbf, sizeof (*buf)); ++ return syscall (__NR_statx, dfd, filename, flags, mask, buf); ++ return 0; ++} ++ ++#define HAVE_GLNX_STATX ++#endif ++ ++/* Copied from systemd git: ff83795469 ("boot: Improve log message") ++ * - open_tree ++ * - openat2 ++ */ ++ ++#ifndef __IGNORE_open_tree ++# if defined(__aarch64__) ++# define systemd_NR_open_tree 428 ++# elif defined(__alpha__) ++# define systemd_NR_open_tree 538 ++# elif defined(__arc__) || defined(__tilegx__) ++# define systemd_NR_open_tree 428 ++# elif defined(__arm__) ++# define systemd_NR_open_tree 428 ++# elif defined(__i386__) ++# define systemd_NR_open_tree 428 ++# elif defined(__ia64__) ++# define systemd_NR_open_tree 1452 ++# elif defined(__loongarch_lp64) ++# define systemd_NR_open_tree 428 ++# elif defined(__m68k__) ++# define systemd_NR_open_tree 428 ++# elif defined(_MIPS_SIM) ++# if _MIPS_SIM == _MIPS_SIM_ABI32 ++# define systemd_NR_open_tree 4428 ++# elif _MIPS_SIM == _MIPS_SIM_NABI32 ++# define systemd_NR_open_tree 6428 ++# elif _MIPS_SIM == _MIPS_SIM_ABI64 ++# define systemd_NR_open_tree 5428 ++# else ++# error "Unknown MIPS ABI" ++# endif ++# elif defined(__hppa__) ++# define systemd_NR_open_tree 428 ++# elif defined(__powerpc__) ++# define systemd_NR_open_tree 428 ++# elif defined(__riscv) ++# if __riscv_xlen == 32 ++# define systemd_NR_open_tree 428 ++# elif __riscv_xlen == 64 ++# define systemd_NR_open_tree 428 ++# else ++# error "Unknown RISC-V ABI" ++# endif ++# elif defined(__s390__) ++# define systemd_NR_open_tree 428 ++# elif defined(__sparc__) ++# define systemd_NR_open_tree 428 ++# elif defined(__x86_64__) ++# if defined(__ILP32__) ++# define systemd_NR_open_tree (428 | /* __X32_SYSCALL_BIT */ 0x40000000) ++# else ++# define systemd_NR_open_tree 428 ++# endif ++# elif !defined(missing_arch_template) ++# warning "open_tree() syscall number is unknown for your architecture" ++# endif ++ ++/* may be an (invalid) negative number due to libseccomp, see PR 13319 */ ++# if defined __NR_open_tree && __NR_open_tree >= 0 ++# if defined systemd_NR_open_tree ++G_STATIC_ASSERT (__NR_open_tree == systemd_NR_open_tree); ++# endif ++# else ++# if defined __NR_open_tree ++# undef __NR_open_tree ++# endif ++# if defined systemd_NR_open_tree && systemd_NR_open_tree >= 0 ++# define __NR_open_tree systemd_NR_open_tree ++# endif ++# endif ++#endif ++ ++#if !defined(HAVE_OPEN_TREE) && defined(__NR_open_tree) ++#ifndef OPEN_TREE_CLONE ++#define OPEN_TREE_CLONE 1 ++#endif ++ ++#ifndef OPEN_TREE_CLOEXEC ++#define OPEN_TREE_CLOEXEC O_CLOEXEC ++#endif ++ ++static inline int ++inline_open_tree (int dfd, ++ const char *filename, ++ unsigned flags) ++{ ++ return syscall(__NR_open_tree, dfd, filename, flags); ++} ++#define open_tree inline_open_tree ++#define HAVE_OPEN_TREE ++#endif ++ ++#ifndef __IGNORE_openat2 ++# if defined(__aarch64__) ++# define systemd_NR_openat2 437 ++# elif defined(__alpha__) ++# define systemd_NR_openat2 547 ++# elif defined(__arc__) || defined(__tilegx__) ++# define systemd_NR_openat2 437 ++# elif defined(__arm__) ++# define systemd_NR_openat2 437 ++# elif defined(__i386__) ++# define systemd_NR_openat2 437 ++# elif defined(__ia64__) ++# define systemd_NR_openat2 1461 ++# elif defined(__loongarch_lp64) ++# define systemd_NR_openat2 437 ++# elif defined(__m68k__) ++# define systemd_NR_openat2 437 ++# elif defined(_MIPS_SIM) ++# if _MIPS_SIM == _MIPS_SIM_ABI32 ++# define systemd_NR_openat2 4437 ++# elif _MIPS_SIM == _MIPS_SIM_NABI32 ++# define systemd_NR_openat2 6437 ++# elif _MIPS_SIM == _MIPS_SIM_ABI64 ++# define systemd_NR_openat2 5437 ++# else ++# error "Unknown MIPS ABI" ++# endif ++# elif defined(__hppa__) ++# define systemd_NR_openat2 437 ++# elif defined(__powerpc__) ++# define systemd_NR_openat2 437 ++# elif defined(__riscv) ++# if __riscv_xlen == 32 ++# define systemd_NR_openat2 437 ++# elif __riscv_xlen == 64 ++# define systemd_NR_openat2 437 ++# else ++# error "Unknown RISC-V ABI" ++# endif ++# elif defined(__s390__) ++# define systemd_NR_openat2 437 ++# elif defined(__sparc__) ++# define systemd_NR_openat2 437 ++# elif defined(__x86_64__) ++# if defined(__ILP32__) ++# define systemd_NR_openat2 (437 | /* __X32_SYSCALL_BIT */ 0x40000000) ++# else ++# define systemd_NR_openat2 437 ++# endif ++# elif !defined(missing_arch_template) ++# warning "openat2() syscall number is unknown for your architecture" ++# endif ++ ++/* may be an (invalid) negative number due to libseccomp, see PR 13319 */ ++# if defined __NR_openat2 && __NR_openat2 >= 0 ++# if defined systemd_NR_openat2 ++G_STATIC_ASSERT (__NR_openat2 == systemd_NR_openat2); ++# endif ++# else ++# if defined __NR_openat2 ++# undef __NR_openat2 ++# endif ++# if defined systemd_NR_openat2 && systemd_NR_openat2 >= 0 ++# define __NR_openat2 systemd_NR_openat2 ++# endif ++# endif ++#endif ++ ++#if !defined(HAVE_OPENAT2) && defined(__NR_openat2) ++#ifndef RESOLVE_NO_XDEV ++#define RESOLVE_NO_XDEV 0x01 ++#endif ++ ++#ifndef RESOLVE_NO_MAGICLINKS ++#define RESOLVE_NO_MAGICLINKS 0x02 ++#endif ++ ++#ifndef RESOLVE_NO_SYMLINKS ++#define RESOLVE_NO_SYMLINKS 0x04 ++#endif ++ ++#ifndef RESOLVE_BENEATH ++#define RESOLVE_BENEATH 0x08 ++#endif ++ ++#ifndef RESOLVE_IN_ROOT ++#define RESOLVE_IN_ROOT 0x10 ++#endif ++ ++#ifndef RESOLVE_CACHED ++#define RESOLVE_CACHED 0x20 ++#endif ++ ++struct inline_open_how { ++ uint64_t flags; ++ uint64_t mode; ++ uint64_t resolve; ++}; ++#define open_how inline_open_how ++ ++static inline int ++inline_openat2 (int dfd, ++ const char *filename, ++ void *buffer, ++ size_t size) ++{ ++ return syscall(__NR_openat2, dfd, filename, buffer, size); ++} ++#define openat2 inline_openat2 ++#define HAVE_OPENAT2 ++#endif +diff --git a/subprojects/libglnx/libglnx.h b/subprojects/libglnx/libglnx.h +index 63d73adc..2b5e07f9 100644 +--- a/subprojects/libglnx/libglnx.h ++++ b/subprojects/libglnx/libglnx.h +@@ -31,6 +31,7 @@ G_BEGIN_DECLS + #include + #include + #include ++#include + #include + #include + #include +diff --git a/subprojects/libglnx/meson.build b/subprojects/libglnx/meson.build +index 7303fa32..0fff39d2 100644 +--- a/subprojects/libglnx/meson.build ++++ b/subprojects/libglnx/meson.build +@@ -76,6 +76,8 @@ libglnx_sources = [ + 'glnx-backport-testutils.h', + 'glnx-backports.c', + 'glnx-backports.h', ++ 'glnx-chase.c', ++ 'glnx-chase.h', + 'glnx-console.c', + 'glnx-console.h', + 'glnx-dirfd.c', +diff --git a/subprojects/libglnx/tests/meson.build b/subprojects/libglnx/tests/meson.build +index 6c46b45c..5b773e8e 100644 +--- a/subprojects/libglnx/tests/meson.build ++++ b/subprojects/libglnx/tests/meson.build +@@ -34,6 +34,7 @@ if get_option('tests') + + test_names = [ + 'backports', ++ 'chase', + 'errors', + 'fdio', + 'macros', +diff --git a/subprojects/libglnx/tests/test-libglnx-chase.c b/subprojects/libglnx/tests/test-libglnx-chase.c +new file mode 100644 +index 00000000..b0ce1b41 +--- /dev/null ++++ b/subprojects/libglnx/tests/test-libglnx-chase.c +@@ -0,0 +1,609 @@ ++/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- ++ * ++ * Copyright (C) 2026 Red Hat, Inc. ++ * SPDX-License-Identifier: LGPL-2.0-or-later ++ */ ++ ++#include "libglnx-config.h" ++#include "libglnx.h" ++#include ++#include ++#include ++#include ++#include ++ ++#include "libglnx-testlib.h" ++ ++#define GLNX_CHASE_DEBUG_NO_OPENAT2 (1U << 31) ++#define GLNX_CHASE_DEBUG_NO_OPEN_TREE (1U << 30) ++ ++const char *test_paths[] = { ++ "file/baz", ++ "file/baz/", ++ "file/baz/.", ++ "file/baz/../baz", ++ "file////baz/..//baz", ++ "file////baz/..//../file/baz", ++}; ++ ++static ino_t ++get_ino (int fd) ++{ ++ int r; ++ struct stat st; ++ ++ r = fstatat (fd, "", &st, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); ++ g_assert_cmpint (r, >=, 0); ++ ++ return st.st_ino; ++} ++ ++static ino_t ++path_get_ino (const char *path) ++{ ++ int r; ++ struct stat st; ++ ++ r = fstatat (AT_FDCWD, path, &st, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); ++ g_assert_cmpint (r, >=, 0); ++ ++ return st.st_ino; ++} ++ ++static char * ++get_abspath (int dfd, ++ const char *path) ++{ ++ g_autofree char *proc_fd_path = NULL; ++ g_autofree char *abs = NULL; ++ g_autoptr(GError) error = NULL; ++ ++ proc_fd_path = g_strdup_printf ("/proc/self/fd/%d", dfd); ++ abs = glnx_readlinkat_malloc (AT_FDCWD, proc_fd_path, NULL, &error); ++ g_assert_no_error (error); ++ g_assert_nonnull (abs); ++ ++ return g_strdup_printf ("%s/%s", abs, path); ++} ++ ++static void ++check_chase (int dfd, ++ const char *path, ++ GlnxChaseFlags flags, ++ int expected_ino) ++{ ++ g_autoptr(GError) error = NULL; ++ glnx_autofd int chase_fd = -1; ++ ++ /* let's try to test the openat2 impl */ ++ chase_fd = glnx_chaseat (dfd, path, flags, &error); ++ g_assert_no_error (error); ++ g_assert_cmpint (chase_fd, >=, 0); ++ g_assert_cmpint (get_ino (chase_fd), ==, expected_ino); ++ g_clear_fd (&chase_fd, NULL); ++ ++ /* let's try to test the open_tree impl */ ++ chase_fd = glnx_chaseat (dfd, path, ++ flags | GLNX_CHASE_DEBUG_NO_OPENAT2, ++ &error); ++ g_assert_no_error (error); ++ g_assert_cmpint (chase_fd, >=, 0); ++ g_assert_cmpint (get_ino (chase_fd), ==, expected_ino); ++ g_clear_fd (&chase_fd, NULL); ++ ++ /* let's try to test the openat impl */ ++ chase_fd = glnx_chaseat (dfd, path, ++ flags | ++ GLNX_CHASE_DEBUG_NO_OPENAT2 | ++ GLNX_CHASE_DEBUG_NO_OPEN_TREE, ++ &error); ++ g_assert_no_error (error); ++ g_assert_cmpint (chase_fd, >=, 0); ++ g_assert_cmpint (get_ino (chase_fd), ==, expected_ino); ++ g_clear_fd (&chase_fd, NULL); ++} ++ ++static void ++check_chase_error (int dfd, ++ const char *path, ++ GlnxChaseFlags flags, ++ GQuark err_domain, ++ gint err_code) ++{ ++ g_autoptr(GError) error = NULL; ++ glnx_autofd int chase_fd = -1; ++ ++ /* let's try to test the openat2 impl */ ++ chase_fd = glnx_chaseat (dfd, path, flags, &error); ++ g_assert_cmpint (chase_fd, <, 0); ++ g_assert_error (error, err_domain, err_code); ++ g_clear_error (&error); ++ ++ /* let's try to test the open_tree impl */ ++ chase_fd = glnx_chaseat (dfd, path, ++ flags | GLNX_CHASE_DEBUG_NO_OPENAT2, ++ &error); ++ g_assert_cmpint (chase_fd, <, 0); ++ g_assert_error (error, err_domain, err_code); ++ g_clear_error (&error); ++ ++ /* let's try to test the openat impl */ ++ chase_fd = glnx_chaseat (dfd, path, ++ flags | ++ GLNX_CHASE_DEBUG_NO_OPENAT2 | ++ GLNX_CHASE_DEBUG_NO_OPEN_TREE, ++ &error); ++ g_assert_cmpint (chase_fd, <, 0); ++ g_assert_error (error, err_domain, err_code); ++ g_clear_error (&error); ++} ++ ++static void ++test_chase_relative (void) ++{ ++ g_autoptr(GError) error = NULL; ++ glnx_autofd int dfd = -1; ++ int expected_ino; ++ ++ g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755, ++ &dfd, ++ NULL, &error)); ++ g_assert_no_error (error); ++ g_assert_cmpint (dfd, >=, 0); ++ ++ expected_ino = get_ino (dfd); ++ ++ for (size_t i = 0; i < G_N_ELEMENTS (test_paths); i++) ++ check_chase (AT_FDCWD, test_paths[i], 0, expected_ino); ++ ++ check_chase_error (AT_FDCWD, "nope", 0, G_IO_ERROR, G_IO_ERROR_NOT_FOUND); ++} ++ ++static void ++test_chase_relative_fd (void) ++{ ++ g_autoptr(GError) error = NULL; ++ glnx_autofd int dfd = -1; ++ int expected_ino; ++ glnx_autofd int cwdfd = -1; ++ ++ g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755, ++ &dfd, ++ NULL, &error)); ++ g_assert_no_error (error); ++ g_assert_cmpint (dfd, >=, 0); ++ ++ expected_ino = get_ino (dfd); ++ ++ cwdfd = openat (AT_FDCWD, ".", O_PATH | O_CLOEXEC | O_NOFOLLOW); ++ g_assert_cmpint (cwdfd, >=, 0); ++ ++ for (size_t i = 0; i < G_N_ELEMENTS (test_paths); i++) ++ check_chase (cwdfd, test_paths[i], 0, expected_ino); ++ ++ check_chase_error (cwdfd, "nope", 0, G_IO_ERROR, G_IO_ERROR_NOT_FOUND); ++} ++ ++static void ++test_chase_absolute (void) ++{ ++ g_autoptr(GError) error = NULL; ++ glnx_autofd int dfd = -1; ++ int expected_ino; ++ glnx_autofd int cwdfd = -1; ++ g_autofree char *proc_fd_path = NULL; ++ g_autofree char *cwd_path = NULL; ++ ++ g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755, ++ &dfd, ++ NULL, &error)); ++ g_assert_no_error (error); ++ g_assert_cmpint (dfd, >=, 0); ++ ++ expected_ino = get_ino (dfd); ++ ++ cwdfd = openat (AT_FDCWD, ".", O_PATH | O_CLOEXEC | O_NOFOLLOW); ++ g_assert_cmpint (cwdfd, >=, 0); ++ ++ cwd_path = get_abspath (cwdfd, ""); ++ ++ for (size_t i = 0; i < G_N_ELEMENTS (test_paths); i++) ++ { ++ g_autofree char *abspath = NULL; ++ ++ abspath = g_strdup_printf ("%s/%s", cwd_path, test_paths[i]); ++ check_chase (AT_FDCWD, abspath, 0, expected_ino); ++ } ++ ++ check_chase_error (AT_FDCWD, "/nope/nope/nope/345298308497623012313243543", 0, ++ G_IO_ERROR, G_IO_ERROR_NOT_FOUND); ++} ++ ++static void ++test_chase_link (void) ++{ ++ g_autoptr(GError) error = NULL; ++ glnx_autofd int dfd = -1; ++ int link_ino; ++ int target_ino; ++ ++ g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755, ++ &dfd, ++ NULL, &error)); ++ g_assert_no_error (error); ++ g_assert_cmpint (dfd, >=, 0); ++ ++ g_assert_cmpint (symlinkat ("file/baz", AT_FDCWD, "link"), ==, 0); ++ ++ target_ino = get_ino (dfd); ++ link_ino = path_get_ino ("link"); ++ ++ check_chase (AT_FDCWD, "link", 0, target_ino); ++ check_chase (AT_FDCWD, "link/", 0, target_ino); ++ check_chase (AT_FDCWD, "link///", 0, target_ino); ++ check_chase (AT_FDCWD, "link/.//.", 0, target_ino); ++ check_chase (AT_FDCWD, "link", 0, target_ino); ++ ++ check_chase (AT_FDCWD, "link", GLNX_CHASE_NOFOLLOW, link_ino); ++ check_chase (AT_FDCWD, "./file/../link", GLNX_CHASE_NOFOLLOW, link_ino); ++ check_chase (AT_FDCWD, "link/", GLNX_CHASE_NOFOLLOW, target_ino); ++ check_chase (AT_FDCWD, "././link/.", GLNX_CHASE_NOFOLLOW, target_ino); ++ check_chase (AT_FDCWD, "link/.//", GLNX_CHASE_NOFOLLOW, target_ino); ++ ++ check_chase (AT_FDCWD, "link", ++ GLNX_CHASE_NOFOLLOW | GLNX_CHASE_RESOLVE_NO_SYMLINKS, ++ link_ino); ++ check_chase_error (AT_FDCWD, "link", ++ GLNX_CHASE_RESOLVE_NO_SYMLINKS, ++ G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS); ++} ++ ++static void ++test_chase_resolve (void) ++{ ++ g_autoptr(GError) error = NULL; ++ glnx_autofd int foo_dfd = -1; ++ glnx_autofd int bar_dfd = -1; ++ g_autofree char *foo_abspath = NULL; ++ int ino; ++ ++ g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "foo", 0755, ++ &foo_dfd, ++ NULL, &error)); ++ g_assert_no_error (error); ++ g_assert_cmpint (foo_dfd, >=, 0); ++ ++ g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "foo/bar", 0755, ++ &bar_dfd, ++ NULL, &error)); ++ g_assert_no_error (error); ++ g_assert_cmpint (bar_dfd, >=, 0); ++ ++ foo_abspath = get_abspath (foo_dfd, ""); ++ ++ g_assert_cmpint (symlinkat ("..", foo_dfd, "link1"), ==, 0); ++ g_assert_cmpint (symlinkat ("bar/../..", foo_dfd, "link2"), ==, 0); ++ g_assert_cmpint (symlinkat (foo_abspath, foo_dfd, "link3"), ==, 0); ++ g_assert_cmpint (symlinkat ("/bar", foo_dfd, "link4"), ==, 0); ++ g_assert_cmpint (symlinkat ("link1/foo", foo_dfd, "link5"), ==, 0); ++ g_assert_cmpint (symlinkat ("link7", foo_dfd, "link6"), ==, 0); ++ g_assert_cmpint (symlinkat ("link6", foo_dfd, "link7"), ==, 0); ++ ++ ino = get_ino (bar_dfd); ++ ++ /* A bunch of different ways to get from CWD and foo to bar */ ++ check_chase (foo_dfd, "./bar", 0, ino); ++ check_chase (foo_dfd, "../foo/bar", 0, ino); ++ check_chase (foo_dfd, "link1/foo/bar", 0, ino); ++ check_chase (AT_FDCWD, "foo/link1/foo/bar", 0, ino); ++ check_chase (foo_dfd, "link2/foo/bar", 0, ino); ++ check_chase (AT_FDCWD, ".///foo/./link2/foo/bar", 0, ino); ++ check_chase (foo_dfd, "link3/bar", 0, ino); ++ check_chase (AT_FDCWD, ".///foo/./link3/bar", 0, ino); ++ check_chase (foo_dfd, "link5/bar", 0, ino); ++ ++ /* check that NO_SYMLINKS works with a component in the middle */ ++ check_chase_error (AT_FDCWD, "foo/link3/bar", ++ GLNX_CHASE_RESOLVE_NO_SYMLINKS, ++ G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS); ++ ++ /* link6 points to link 7, points to link6, ... This should error out! */ ++ check_chase_error (foo_dfd, "link6/bar", 0, ++ G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS); ++ ++ /* Test with links which never go below the dfd */ ++ check_chase (AT_FDCWD, "foo/link1/foo/bar", ++ GLNX_CHASE_RESOLVE_BENEATH, ++ ino); ++ check_chase (AT_FDCWD, "foo/link2/foo/bar", ++ GLNX_CHASE_RESOLVE_BENEATH, ++ ino); ++ /* An absolute link is always below the dfd */ ++ check_chase_error (AT_FDCWD, "foo/link3/foo/bar", ++ GLNX_CHASE_RESOLVE_BENEATH, ++ G_IO_ERROR, G_IO_ERROR_FAILED); ++ ++ /* Same, but from foo instead of cwd */ ++ check_chase_error (foo_dfd, "link1/foo/bar", ++ GLNX_CHASE_RESOLVE_BENEATH, ++ G_IO_ERROR, G_IO_ERROR_FAILED); ++ check_chase_error (foo_dfd, "link2/foo/bar", ++ GLNX_CHASE_RESOLVE_BENEATH, ++ G_IO_ERROR, G_IO_ERROR_FAILED); ++ check_chase_error (foo_dfd, "link3/foo/bar", ++ GLNX_CHASE_RESOLVE_BENEATH, ++ G_IO_ERROR, G_IO_ERROR_FAILED); ++ ++ /* Check that trying to be below the dfd with RESOLVE_IN_ROOT resolves to the ++ * dfd itself */ ++ check_chase (foo_dfd, "link1/bar", ++ GLNX_CHASE_RESOLVE_IN_ROOT, ++ ino); ++ check_chase (foo_dfd, "link2/bar", ++ GLNX_CHASE_RESOLVE_IN_ROOT, ++ ino); ++ /* The absolute link is relative to dfd with RESOLVE_IN_ROOT, so this ++ * fails... */ ++ check_chase_error (foo_dfd, "link3", ++ GLNX_CHASE_RESOLVE_IN_ROOT, ++ G_IO_ERROR, G_IO_ERROR_NOT_FOUND); ++ /* ... but the link /bar resolves correctly from foo as dfd. */ ++ check_chase (foo_dfd, "link4", ++ GLNX_CHASE_RESOLVE_IN_ROOT, ++ ino); ++} ++ ++static void ++test_chase_resolve_in_root_absolute (void) ++{ ++ g_autoptr(GError) error = NULL; ++ glnx_autofd int foo_dfd = -1; ++ glnx_autofd int bar_dfd = -1; ++ glnx_autofd int baz_dfd = -1; ++ ++ g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "foo", 0755, ++ &foo_dfd, ++ NULL, &error)); ++ g_assert_no_error (error); ++ g_assert_cmpint (foo_dfd, >=, 0); ++ ++ g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "foo/bar", 0755, ++ &bar_dfd, ++ NULL, &error)); ++ g_assert_no_error (error); ++ g_assert_cmpint (bar_dfd, >=, 0); ++ ++ g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "foo/bar/baz", 0755, ++ &baz_dfd, ++ NULL, &error)); ++ g_assert_no_error (error); ++ g_assert_cmpint (baz_dfd, >=, 0); ++ ++ /* Test the absolute symlink doesn't break tracking of the root level */ ++ g_assert_cmpint (symlinkat ("/..", baz_dfd, "link1"), ==, 0); ++ ++ /* We should not be able to break out of the root! */ ++ check_chase (bar_dfd, "./baz/link1", GLNX_CHASE_RESOLVE_IN_ROOT, get_ino (bar_dfd)); ++} ++ ++static void ++check_chase_and_statxat (int dfd, ++ const char *path, ++ GlnxChaseFlags flags, ++ ino_t expected_ino, ++ mode_t expected_type) ++{ ++ g_autoptr(GError) error = NULL; ++ glnx_autofd int chase_fd = -1; ++ struct glnx_statx stx; ++ ++ /* let's try to test the openat2 impl */ ++ chase_fd = glnx_chase_and_statxat (dfd, path, flags, ++ GLNX_STATX_TYPE | GLNX_STATX_INO, ++ &stx, &error); ++ g_assert_cmpint (chase_fd, >=, 0); ++ g_assert_no_error (error); ++ g_assert_cmpint (stx.stx_ino, ==, expected_ino); ++ g_assert_cmpint (stx.stx_mode & S_IFMT, ==, expected_type); ++ g_clear_fd (&chase_fd, NULL); ++ ++ /* let's try to test the open_tree impl */ ++ chase_fd = glnx_chase_and_statxat (dfd, path, ++ flags | GLNX_CHASE_DEBUG_NO_OPENAT2, ++ GLNX_STATX_TYPE | GLNX_STATX_INO, ++ &stx, &error); ++ g_assert_cmpint (chase_fd, >=, 0); ++ g_assert_no_error (error); ++ g_assert_cmpint (stx.stx_ino, ==, expected_ino); ++ g_assert_cmpint (stx.stx_mode & S_IFMT, ==, expected_type); ++ g_clear_fd (&chase_fd, NULL); ++ ++ /* let's try to test the openat impl */ ++ chase_fd = glnx_chase_and_statxat (dfd, path, ++ flags | ++ GLNX_CHASE_DEBUG_NO_OPENAT2 | ++ GLNX_CHASE_DEBUG_NO_OPEN_TREE, ++ GLNX_STATX_TYPE | GLNX_STATX_INO, ++ &stx, &error); ++ g_assert_cmpint (chase_fd, >=, 0); ++ g_assert_no_error (error); ++ g_assert_cmpint (stx.stx_ino, ==, expected_ino); ++ g_assert_cmpint (stx.stx_mode & S_IFMT, ==, expected_type); ++ g_clear_fd (&chase_fd, NULL); ++} ++ ++static void ++check_chase_and_statxat_error (int dfd, ++ const char *path, ++ GlnxChaseFlags flags, ++ GQuark err_domain, ++ gint err_code) ++{ ++ g_autoptr(GError) error = NULL; ++ glnx_autofd int chase_fd = -1; ++ struct glnx_statx stx; ++ ++ /* let's try to test the openat2 impl */ ++ chase_fd = glnx_chase_and_statxat (dfd, path, flags, ++ GLNX_STATX_TYPE | GLNX_STATX_INO, ++ &stx, &error); ++ g_assert_cmpint (chase_fd, <, 0); ++ g_assert_error (error, err_domain, err_code); ++ g_clear_error (&error); ++ ++ /* let's try to test the open_tree impl */ ++ chase_fd = glnx_chase_and_statxat (dfd, path, ++ flags | GLNX_CHASE_DEBUG_NO_OPENAT2, ++ GLNX_STATX_TYPE | GLNX_STATX_INO, ++ &stx, &error); ++ g_assert_cmpint (chase_fd, <, 0); ++ g_assert_error (error, err_domain, err_code); ++ g_clear_error (&error); ++ ++ /* let's try to test the openat impl */ ++ chase_fd = glnx_chase_and_statxat (dfd, path, ++ flags | ++ GLNX_CHASE_DEBUG_NO_OPENAT2 | ++ GLNX_CHASE_DEBUG_NO_OPEN_TREE, ++ GLNX_STATX_TYPE | GLNX_STATX_INO, ++ &stx, &error); ++ g_assert_cmpint (chase_fd, <, 0); ++ g_assert_error (error, err_domain, err_code); ++ g_clear_error (&error); ++} ++ ++static void ++test_chase_and_statxat_basic (void) ++{ ++ g_autoptr(GError) error = NULL; ++ glnx_autofd int dfd = -1; ++ glnx_autofd int file_fd = -1; ++ ino_t expected_ino; ++ ++ g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755, ++ &dfd, ++ NULL, &error)); ++ g_assert_no_error (error); ++ g_assert_cmpint (dfd, >=, 0); ++ ++ expected_ino = get_ino (dfd); ++ ++ /* Test with various path forms */ ++ for (size_t i = 0; i < G_N_ELEMENTS (test_paths); i++) ++ check_chase_and_statxat (AT_FDCWD, test_paths[i], 0, expected_ino, S_IFDIR); ++ ++ /* Create a regular file and test it */ ++ file_fd = openat (dfd, "testfile", O_WRONLY | O_CREAT | O_CLOEXEC, 0644); ++ g_assert_cmpint (file_fd, >=, 0); ++ g_clear_fd (&file_fd, NULL); ++ ++ expected_ino = path_get_ino ("file/baz/testfile"); ++ check_chase_and_statxat (AT_FDCWD, "file/baz/testfile", 0, expected_ino, S_IFREG); ++ ++ /* Test error cases */ ++ check_chase_and_statxat_error (AT_FDCWD, "nope", 0, G_IO_ERROR, G_IO_ERROR_NOT_FOUND); ++} ++ ++static void ++test_chase_and_statxat_symlink (void) ++{ ++ g_autoptr(GError) error = NULL; ++ glnx_autofd int dfd = -1; ++ glnx_autofd int chase_fd = -1; ++ ino_t link_ino; ++ ino_t target_ino; ++ struct glnx_statx stx; ++ ++ g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755, ++ &dfd, ++ NULL, &error)); ++ g_assert_no_error (error); ++ g_assert_cmpint (dfd, >=, 0); ++ ++ g_assert_cmpint (symlinkat ("file/baz", AT_FDCWD, "fstatlink"), ==, 0); ++ ++ target_ino = get_ino (dfd); ++ link_ino = path_get_ino ("fstatlink"); ++ ++ /* Following symlinks should give us the directory */ ++ check_chase_and_statxat (AT_FDCWD, "fstatlink", 0, target_ino, S_IFDIR); ++ check_chase_and_statxat (AT_FDCWD, "fstatlink/", 0, target_ino, S_IFDIR); ++ ++ /* With NOFOLLOW, we should get the symlink itself */ ++ check_chase_and_statxat (AT_FDCWD, "fstatlink", GLNX_CHASE_NOFOLLOW, link_ino, S_IFLNK); ++ ++ /* Verify we can distinguish between regular files, directories, and symlinks */ ++ chase_fd = glnx_chase_and_statxat (AT_FDCWD, "fstatlink", GLNX_CHASE_NOFOLLOW, ++ GLNX_STATX_TYPE | GLNX_STATX_INO, ++ &stx, &error); ++ g_assert_cmpint (chase_fd, >=, 0); ++ g_assert_no_error (error); ++ g_assert_true (S_ISLNK (stx.stx_mode)); ++ g_clear_fd (&chase_fd, NULL); ++ ++ chase_fd = glnx_chase_and_statxat (AT_FDCWD, "fstatlink", 0, ++ GLNX_STATX_TYPE | GLNX_STATX_INO, ++ &stx, &error); ++ g_assert_cmpint (chase_fd, >=, 0); ++ g_assert_no_error (error); ++ g_assert_true (S_ISDIR (stx.stx_mode)); ++ g_clear_fd (&chase_fd, NULL); ++ ++ /* Test with RESOLVE_NO_SYMLINKS */ ++ check_chase_and_statxat_error (AT_FDCWD, "fstatlink", ++ GLNX_CHASE_RESOLVE_NO_SYMLINKS, ++ G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS); ++} ++ ++static void ++test_chase_and_statxat_permissions (void) ++{ ++ g_autoptr(GError) error = NULL; ++ glnx_autofd int dfd = -1; ++ glnx_autofd int file_fd = -1; ++ glnx_autofd int chase_fd = -1; ++ struct glnx_statx stx; ++ mode_t expected_mode = 0640; ++ ++ g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "permtest", 0755, ++ &dfd, ++ NULL, &error)); ++ g_assert_no_error (error); ++ ++ /* Create a file with specific permissions */ ++ file_fd = openat (dfd, "testfile", O_WRONLY | O_CREAT | O_CLOEXEC, expected_mode); ++ g_assert_cmpint (file_fd, >=, 0); ++ g_clear_fd (&file_fd, NULL); ++ ++ /* Verify that glnx_chase_and_statxat returns the correct permissions */ ++ chase_fd = glnx_chase_and_statxat (dfd, "testfile", 0, ++ GLNX_STATX_TYPE | GLNX_STATX_MODE, ++ &stx, &error); ++ g_assert_cmpint (chase_fd, >=, 0); ++ g_assert_no_error (error); ++ g_assert_cmpint (stx.stx_mode & 0777, ==, expected_mode); ++ g_assert_true (S_ISREG (stx.stx_mode)); ++ g_clear_fd (&chase_fd, NULL); ++} ++ ++int main (int argc, char **argv) ++{ ++ _GLNX_TEST_SCOPED_TEMP_DIR; ++ int ret; ++ ++ g_test_init (&argc, &argv, NULL); ++ ++ g_test_add_func ("/chase-relative", test_chase_relative); ++ g_test_add_func ("/chase-relative-fd", test_chase_relative_fd); ++ g_test_add_func ("/chase-absolute", test_chase_absolute); ++ g_test_add_func ("/chase-link", test_chase_link); ++ g_test_add_func ("/chase-resolve", test_chase_resolve); ++ g_test_add_func ("/chase-resolve-in-root-absolute", test_chase_resolve_in_root_absolute); ++ g_test_add_func ("/chase-and-statxat-basic", test_chase_and_statxat_basic); ++ g_test_add_func ("/chase-and-statxat-symlink", test_chase_and_statxat_symlink); ++ g_test_add_func ("/chase-and-statxat-permissions", test_chase_and_statxat_permissions); ++ ++ ret = g_test_run(); ++ ++ return ret; ++} +diff --git a/subprojects/libglnx/tests/test-libglnx-fdio.c b/subprojects/libglnx/tests/test-libglnx-fdio.c +index b9aa682a..cfa5b648 100644 +--- a/subprojects/libglnx/tests/test-libglnx-fdio.c ++++ b/subprojects/libglnx/tests/test-libglnx-fdio.c +@@ -286,6 +286,158 @@ test_filecopy_procfs (void) + } + } + ++static void ++test_fd_reopen (void) ++{ ++ g_autoptr(GError) error = NULL; ++ glnx_autofd int dfd = -1; ++ glnx_autofd int opath_fd = -1; ++ glnx_autofd int regular_fd = -1; ++ glnx_autofd int testfile_fd = -1; ++ glnx_autofd int link_opath_fd = -1; ++ glnx_autofd int reopened_fd = -1; ++ struct stat st1, st2; ++ const char *test_data = "test content"; ++ char buf[100]; ++ ssize_t n; ++ gboolean ok; ++ int flags; ++ ++ /* Create a test directory and file */ ++ ok = glnx_shutil_mkdir_p_at_open (AT_FDCWD, "reopen_test", 0755, &dfd, NULL, &error); ++ g_assert_no_error (error); ++ g_assert_true (ok); ++ g_assert_no_errno (dfd); ++ ++ glnx_file_replace_contents_at (dfd, "testfile", ++ (const void *) test_data, strlen (test_data), ++ GLNX_FILE_REPLACE_NODATASYNC, NULL, &error); ++ g_assert_no_error (error); ++ ++ /* Test 1: Reopen O_PATH fd as regular fd for reading and writing */ ++ opath_fd = openat (dfd, "testfile", O_PATH | O_CLOEXEC); ++ g_assert_no_errno (opath_fd); ++ ++ regular_fd = glnx_fd_reopen (opath_fd, O_RDWR, &error); ++ g_assert_no_errno (regular_fd); ++ g_assert_no_error (error); ++ ++ flags = fcntl (regular_fd, F_GETFL); ++ g_assert_no_errno (flags); ++ g_assert_cmpint (flags & (O_RDONLY | O_WRONLY | O_RDWR), ==, O_RDWR); ++ g_assert_cmpint (flags & (O_PATH | O_DIRECTORY | O_NOFOLLOW), ==, 0); ++ flags = fcntl (regular_fd, F_GETFD); ++ g_assert_no_errno (flags); ++ g_assert_cmpint (flags & FD_CLOEXEC, ==, FD_CLOEXEC); ++ ++ /* Verify we can read from the reopened fd */ ++ n = read (regular_fd, buf, sizeof (buf)); ++ g_assert_cmpmem (buf, n, test_data, strlen (test_data)); ++ ++ g_clear_fd (®ular_fd, NULL); ++ g_clear_fd (&opath_fd, NULL); ++ ++ /* Test 2: Reopen directory fd with O_DIRECTORY */ ++ opath_fd = openat (AT_FDCWD, "reopen_test", O_PATH | O_CLOEXEC); ++ g_assert_no_errno (opath_fd); ++ ++ reopened_fd = glnx_fd_reopen (opath_fd, O_RDONLY | O_DIRECTORY, &error); ++ g_assert_no_error (error); ++ g_assert_no_errno (reopened_fd); ++ ++ flags = fcntl (reopened_fd, F_GETFL); ++ g_assert_no_errno (flags); ++ g_assert_cmpint (flags & (O_RDONLY | O_WRONLY | O_RDWR), ==, O_RDONLY); ++ g_assert_cmpint (flags & (O_PATH | O_DIRECTORY | O_NOFOLLOW), ==, O_DIRECTORY); ++ flags = fcntl (reopened_fd, F_GETFD); ++ g_assert_no_errno (flags); ++ g_assert_cmpint (flags & FD_CLOEXEC, ==, FD_CLOEXEC); ++ ++ /* Verify both fds point to the same inode */ ++ g_assert_no_errno (fstat (opath_fd, &st1)); ++ g_assert_no_errno (fstat (reopened_fd, &st2)); ++ g_assert_cmpint (st1.st_ino, ==, st2.st_ino); ++ ++ g_clear_fd (&reopened_fd, NULL); ++ g_clear_fd (&opath_fd, NULL); ++ ++ /* Test 3: Reopen AT_FDCWD */ ++ reopened_fd = glnx_fd_reopen (AT_FDCWD, O_RDONLY | O_DIRECTORY, &error); ++ g_assert_no_error (error); ++ g_assert_no_errno (reopened_fd); ++ ++ g_clear_fd (&reopened_fd, NULL); ++ ++ /* Test 4: Test that O_NOFOLLOW is rejected */ ++ opath_fd = openat (dfd, "testfile", O_PATH | O_CLOEXEC); ++ g_assert_no_errno (opath_fd); ++ ++ regular_fd = glnx_fd_reopen (opath_fd, O_RDONLY | O_NOFOLLOW, &error); ++ g_assert_cmpint (regular_fd, <, 0); ++ g_assert_error (error, G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS); ++ g_clear_error (&error); ++ ++ g_clear_fd (&opath_fd, NULL); ++ ++ /* Test 5: Reopen O_PATH fd to symlink with O_PATH (should work) */ ++ g_assert_no_errno (symlinkat ("testfile", dfd, "testlink")); ++ ++ link_opath_fd = openat (dfd, "testlink", O_PATH | O_NOFOLLOW); ++ g_assert_no_errno (link_opath_fd); ++ ++ /* Verify it's a symlink */ ++ g_assert_no_errno (fstatat (link_opath_fd, "", &st1, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)); ++ g_assert_true (S_ISLNK (st1.st_mode)); ++ ++ /* Reopen with O_PATH should work */ ++ reopened_fd = glnx_fd_reopen (link_opath_fd, O_PATH, &error); ++ g_assert_no_error (error); ++ g_assert_no_errno (reopened_fd); ++ ++ flags = fcntl (reopened_fd, F_GETFL); ++ g_assert_no_errno (flags); ++ g_assert_cmpint (flags & (O_RDONLY | O_WRONLY | O_RDWR), ==, O_RDONLY); ++ g_assert_cmpint (flags & (O_PATH | O_DIRECTORY | O_NOFOLLOW), ==, O_PATH); ++ flags = fcntl (reopened_fd, F_GETFD); ++ g_assert_no_errno (flags); ++ g_assert_cmpint (flags & FD_CLOEXEC, ==, FD_CLOEXEC); ++ ++ /* Verify both point to the same symlink */ ++ g_assert_no_errno (fstatat (reopened_fd, "", &st2, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)); ++ g_assert_cmpint (st1.st_ino, ==, st2.st_ino); ++ g_assert_true (S_ISLNK (st2.st_mode)); ++ ++ g_clear_fd (&reopened_fd, NULL); ++ ++ /* Test 6: Reopening O_PATH fd to symlink without O_PATH should fail with ELOOP */ ++ reopened_fd = glnx_fd_reopen (link_opath_fd, O_RDONLY, &error); ++ g_assert_cmpint (reopened_fd, <, 0); ++ g_assert_error (error, G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS); ++ g_clear_error (&error); ++ ++ g_clear_fd (&link_opath_fd, NULL); ++ ++ /* Test 7: Verify read index is reset */ ++ testfile_fd = openat (dfd, "testfile", O_RDONLY | O_CLOEXEC); ++ g_assert_no_errno (testfile_fd); ++ ++ /* Read some data to advance the read index */ ++ n = read (testfile_fd, buf, 4); ++ g_assert_cmpint (n, ==, 4); ++ ++ /* Reopen should reset the read index */ ++ reopened_fd = glnx_fd_reopen (testfile_fd, O_RDONLY, &error); ++ g_assert_no_error (error); ++ g_assert_no_errno (reopened_fd); ++ ++ /* Should read from the beginning again */ ++ n = read (reopened_fd, buf, sizeof (buf)); ++ g_assert_cmpmem (buf, n, test_data, strlen (test_data)); ++ ++ g_clear_fd (&reopened_fd, NULL); ++ g_clear_fd (&testfile_fd, NULL); ++} ++ + int main (int argc, char **argv) + { + _GLNX_TEST_SCOPED_TEMP_DIR; +@@ -300,6 +452,7 @@ int main (int argc, char **argv) + g_test_add_func ("/renameat2-noreplace", test_renameat2_noreplace); + g_test_add_func ("/renameat2-exchange", test_renameat2_exchange); + g_test_add_func ("/fstat", test_fstatat); ++ g_test_add_func ("/fd-reopen", test_fd_reopen); + + ret = g_test_run(); + +-- +2.52.0 + + diff --git a/CVE-2026-34078-10-run-fix-checking-wrong-variable-in-runtime-fd-selection.patch b/CVE-2026-34078-10-run-fix-checking-wrong-variable-in-runtime-fd-selection.patch new file mode 100644 index 0000000..e59ad9c --- /dev/null +++ b/CVE-2026-34078-10-run-fix-checking-wrong-variable-in-runtime-fd-selection.patch @@ -0,0 +1,38 @@ +From d19f44408000b53b6d61d3e60a3c65104dc5a8c6 Mon Sep 17 00:00:00 2001 +From: Xiangzhe +Date: Wed, 8 Apr 2026 12:27:28 +0800 +Subject: [PATCH] run: Fix checking wrong variable in runtime fd selection + +In flatpak_run_app(), the else-if branch that handles +FLATPAK_RUN_APP_DEPLOY_USR_ORIGINAL was checking custom_app_fd instead +of custom_runtime_fd. When custom_app_fd is APP_EMPTY (-3) and +custom_runtime_fd is USR_ORIGINAL (-2), the condition would not match +and fall through to g_assert_not_reached(), aborting the process. + +This broke sub-sandbox spawning with --app-path="" (empty app), which +is used by steam-runtime-check-requirements to verify that Flatpak's +sub-sandbox mechanism works. + +Fixes: ac62ebe3 "run: Use O_PATH fds for the runtime and app deploy directories" +Helps: https://github.com/flatpak/flatpak/issues/6568 +(cherry picked from commit 066babba75d355d077ea11091e5f65d3b0e0d818) +--- + common/flatpak-run.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/common/flatpak-run.c b/common/flatpak-run.c +index 055c721d..f44c66ff 100644 +--- a/common/flatpak-run.c ++++ b/common/flatpak-run.c +@@ -3203,7 +3203,7 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + runtime_fd = custom_runtime_fd; + runtime_files = custom_runtime_files; + } +- else if (custom_app_fd == FLATPAK_RUN_APP_DEPLOY_USR_ORIGINAL) ++ else if (custom_runtime_fd == FLATPAK_RUN_APP_DEPLOY_USR_ORIGINAL) + { + original_runtime_target_path = "/usr"; + runtime_fd = original_runtime_fd; +-- +2.54.0 + diff --git a/CVE-2026-34078-11-run-mount-original-app-on-run-parent-app-when-using-app-path.patch b/CVE-2026-34078-11-run-mount-original-app-on-run-parent-app-when-using-app-path.patch new file mode 100644 index 0000000..bf7344d --- /dev/null +++ b/CVE-2026-34078-11-run-mount-original-app-on-run-parent-app-when-using-app-path.patch @@ -0,0 +1,34 @@ +From ccd9c8a7fa8bee1b728e683c2d478f4408cd651c Mon Sep 17 00:00:00 2001 +From: Simon McVittie +Date: Wed, 8 Apr 2026 09:44:55 +0100 +Subject: [PATCH] run: Mount original app on /run/parent/app when using + --app-path="" + +Before addressing CVE-2026-34078, we would always mount the original app +*somewhere*, either /app (in the normal case) or /run/parent/app (when +using a custom or empty /app for the subsandbox). The empty-app case +regressed during the fix for CVE-2026-34078; bring back previous behaviour. + +Fixes: ac62ebe3 "run: Use O_PATH fds for the runtime and app deploy directories" +Resolves: https://github.com/flatpak/flatpak/issues/6568 +Signed-off-by: Simon McVittie +(cherry picked from commit fde4716f67b6620da57fd74481694eb58795d589) +--- + common/flatpak-run.c | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/common/flatpak-run.c b/common/flatpak-run.c +index f44c66ff..b90af672 100644 +--- a/common/flatpak-run.c ++++ b/common/flatpak-run.c +@@ -3335,6 +3335,7 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + } + else if (custom_app_fd == FLATPAK_RUN_APP_DEPLOY_APP_EMPTY) + { ++ original_app_target_path = "/run/parent/app"; + app_fd = -1; + app_files = NULL; + } +-- +2.54.0 + diff --git a/CVE-2026-34078-12-portal-update-max-fd-after-creating-the-instance-id-pipe.patch b/CVE-2026-34078-12-portal-update-max-fd-after-creating-the-instance-id-pipe.patch new file mode 100644 index 0000000..5e9a0f2 --- /dev/null +++ b/CVE-2026-34078-12-portal-update-max-fd-after-creating-the-instance-id-pipe.patch @@ -0,0 +1,31 @@ +From 2e2f4a430b784163614403b333ab3990c9354db3 Mon Sep 17 00:00:00 2001 +From: Alberto Garcia +Date: Wed, 8 Apr 2026 19:28:32 +0200 +Subject: [PATCH] portal: update max_fd after creating the instance ID pipe + +fd_map_remap_fd() is called several times after this, and without this +change it can allocate a target fd that collides with instance_id_fd. + +Only the write end of the pipe needs to be considered because that's +the one passed to the child. + +Closes: https://github.com/flatpak/flatpak/issues/6570 +--- + portal/flatpak-portal.c | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/portal/flatpak-portal.c b/portal/flatpak-portal.c +index 5c0935a1..a67ee330 100644 +--- a/portal/flatpak-portal.c ++++ b/portal/flatpak-portal.c +@@ -1150,6 +1150,7 @@ handle_spawn (PortalFlatpak *object, + + g_ptr_array_add (flatpak_argv, g_strdup_printf ("--instance-id-fd=%d", pipe_fds[1])); + child_setup_data.instance_id_fd = pipe_fds[1]; ++ max_fd = MAX(max_fd, pipe_fds[1]); + } + + if (devel) +-- +2.54.0 + diff --git a/CVE-2026-34078-13-run-fix-fd-tracking-in-flatpak-run-add-app-info-args.patch b/CVE-2026-34078-13-run-fix-fd-tracking-in-flatpak-run-add-app-info-args.patch new file mode 100644 index 0000000..c325b14 --- /dev/null +++ b/CVE-2026-34078-13-run-fix-fd-tracking-in-flatpak-run-add-app-info-args.patch @@ -0,0 +1,123 @@ +From bd41305b98373d762cc02395d9fe7b9486da20d4 Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Wed, 8 Apr 2026 17:47:48 +0200 +Subject: [PATCH] run: Fix fd tracking in flatpak_run_add_app_info_args + +Calls to flatpak_bwrap_add_args_data_fd take ownership over the fd they +take. Closing them while they are still in the bwrap struct will abort +later when the bwrap struct gets freed and it tries to close the already +closed fd. + +Fix this by using glnx_autofd and g_steal_fd. +--- + common/flatpak-run.c | 33 ++++++++++++++++----------------- + 1 file changed, 16 insertions(+), 17 deletions(-) + +diff --git a/common/flatpak-run.c b/common/flatpak-run.c +index b90af672..5882b2a9 100644 +--- a/common/flatpak-run.c ++++ b/common/flatpak-run.c +@@ -1366,7 +1366,7 @@ flatpak_run_add_app_info_args (FlatpakBwrap *bwrap, + gboolean build, + gboolean devel, + char **app_info_path_out, +- int instance_id_fd, ++ int instance_id_fd_arg, + char **instance_id_host_dir_out, + char **instance_id_host_private_dir_out, + char **instance_id_out, +@@ -1374,7 +1374,11 @@ flatpak_run_add_app_info_args (FlatpakBwrap *bwrap, + { + g_autofree char *info_path = NULL; + g_autofree char *bwrapinfo_path = NULL; +- int fd, fd2, fd3; ++ glnx_autofd int fd1 = -1; ++ glnx_autofd int fd2 = -1; ++ glnx_autofd int fd3 = -1; ++ int info_fd; ++ glnx_autofd int instance_id_fd = instance_id_fd_arg; + g_autoptr(GKeyFile) keyfile = NULL; + g_autofree char *runtime_path = NULL; + const char *group; +@@ -1523,8 +1527,8 @@ flatpak_run_add_app_info_args (FlatpakBwrap *bwrap, + This way even if the bind-mount is unmounted we can find the real data. + */ + +- fd = open (info_path, O_RDONLY); +- if (fd == -1) ++ fd1 = info_fd = open (info_path, O_RDONLY); ++ if (fd1 == -1) + { + int errsv = errno; + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errsv), +@@ -1535,7 +1539,6 @@ flatpak_run_add_app_info_args (FlatpakBwrap *bwrap, + fd2 = open (info_path, O_RDONLY); + if (fd2 == -1) + { +- close (fd); + int errsv = errno; + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errsv), + _("Failed to open flatpak-info file: %s"), g_strerror (errsv)); +@@ -1544,9 +1547,9 @@ flatpak_run_add_app_info_args (FlatpakBwrap *bwrap, + + flatpak_bwrap_add_args (bwrap, "--perms", "0600", NULL); + flatpak_bwrap_add_args_data_fd (bwrap, +- "--file", fd, "/.flatpak-info"); ++ "--file", g_steal_fd (&fd1), "/.flatpak-info"); + flatpak_bwrap_add_args_data_fd (bwrap, +- "--ro-bind-data", fd2, "/.flatpak-info"); ++ "--ro-bind-data", g_steal_fd (&fd2), "/.flatpak-info"); + + /* Tell the application that it's running under Flatpak in a generic way. */ + flatpak_bwrap_add_args (bwrap, +@@ -1563,8 +1566,6 @@ flatpak_run_add_app_info_args (FlatpakBwrap *bwrap, + fd3 = open (bwrapinfo_path, O_RDWR | O_CREAT, 0644); + if (fd3 == -1) + { +- close (fd); +- close (fd2); + int errsv = errno; + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errsv), + _("Failed to open bwrapinfo.json file: %s"), g_strerror (errsv)); +@@ -1587,10 +1588,6 @@ flatpak_run_add_app_info_args (FlatpakBwrap *bwrap, + if (errsv == EINTR) + continue; + +- close (fd); +- close (fd2); +- close (fd3); +- + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errsv), + _("Failed to write to instance id fd: %s"), g_strerror (errsv)); + return FALSE; +@@ -1600,13 +1597,14 @@ flatpak_run_add_app_info_args (FlatpakBwrap *bwrap, + instance_id_size -= bytes_written; + } + +- close (instance_id_fd); ++ /* explicitly close this as soon as we're done to notify the other side */ ++ g_clear_fd (&instance_id_fd, NULL); + } + +- flatpak_bwrap_add_args_data_fd (bwrap, "--info-fd", fd3, NULL); ++ flatpak_bwrap_add_args_data_fd (bwrap, "--info-fd", g_steal_fd (&fd3), NULL); + + if (app_info_path_out != NULL) +- *app_info_path_out = g_strdup_printf ("/proc/self/fd/%d", fd); ++ *app_info_path_out = g_strdup_printf ("/proc/self/fd/%d", info_fd); + + if (instance_id_host_dir_out != NULL) + *instance_id_host_dir_out = g_steal_pointer (&instance_id_host_dir); +@@ -3556,7 +3554,8 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + app_id, flatpak_decomposed_get_branch (app_ref), + runtime_ref, app_id_dir, app_context, extra_context, + sandboxed, FALSE, flags & FLATPAK_RUN_FLAG_DEVEL, +- &app_info_path, instance_id_fd, ++ &app_info_path, ++ g_steal_fd (&instance_id_fd), + &instance_id_host_dir, &instance_id_host_private_dir, + &instance_id, error)) + return FALSE; +-- +2.54.0 + diff --git a/CVE-2026-34078-14-utils-improve-error-message-when-passing-an-fd-numer-which-is-not-a-fd.patch b/CVE-2026-34078-14-utils-improve-error-message-when-passing-an-fd-numer-which-is-not-a-fd.patch new file mode 100644 index 0000000..f4a7cde --- /dev/null +++ b/CVE-2026-34078-14-utils-improve-error-message-when-passing-an-fd-numer-which-is-not-a-fd.patch @@ -0,0 +1,28 @@ +From e2ef538e8d70f8ddaf1adf201c3bb1578d0444f1 Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Wed, 8 Apr 2026 18:15:42 +0200 +Subject: [PATCH] utils: Improve error message when passing an FD numer which + is not a FD + +--- + common/flatpak-utils.c | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/common/flatpak-utils.c b/common/flatpak-utils.c +index dbc46951..7e69e8be 100644 +--- a/common/flatpak-utils.c ++++ b/common/flatpak-utils.c +@@ -2520,8 +2520,8 @@ flatpak_parse_fd (const char *fd_string, + + fd = (int) parsed; + +- if (!glnx_fstat (fd, &stbuf, error)) +- return -1; ++ if (!glnx_fstat (fd, &stbuf, NULL)) ++ return glnx_fd_throw (error, "Not an open file descriptor: %d", fd); + + return fd; + } +-- +2.54.0 + diff --git a/CVE-2026-34078-15-run-do-not-close-bind-ro-bind.patch b/CVE-2026-34078-15-run-do-not-close-bind-ro-bind.patch new file mode 100644 index 0000000..275e182 --- /dev/null +++ b/CVE-2026-34078-15-run-do-not-close-bind-ro-bind.patch @@ -0,0 +1,32 @@ +From e654ebade0b9154830563c55c0a5f426b41fd46d Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Wed, 8 Apr 2026 18:14:19 +0200 +Subject: [PATCH] run: Do not close --bind/--ro-bind + +--- + app/flatpak-builtins-run.c | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/app/flatpak-builtins-run.c b/app/flatpak-builtins-run.c +index 0bfc311d..256f7828 100644 +--- a/app/flatpak-builtins-run.c ++++ b/app/flatpak-builtins-run.c +@@ -82,6 +82,7 @@ option_bind_fd_cb (const char *option_name, + return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); + + g_array_append_val (opt_bind_fds, fd); ++ fd = -1; /* ownership transferred to GArray */ + return TRUE; + } + +@@ -101,6 +102,7 @@ option_ro_bind_fd_cb (const char *option_name, + return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); + + g_array_append_val (opt_ro_bind_fds, fd); ++ fd = -1; /* ownership transferred to GArray */ + return TRUE; + } + +-- +2.54.0 + diff --git a/CVE-2026-34078-16-run-use-the-same-fd-validation-for-all-fd-options.patch b/CVE-2026-34078-16-run-use-the-same-fd-validation-for-all-fd-options.patch new file mode 100644 index 0000000..014576d --- /dev/null +++ b/CVE-2026-34078-16-run-use-the-same-fd-validation-for-all-fd-options.patch @@ -0,0 +1,95 @@ +From c89a0c509ad315ccf5157e9faebb4e243bed165c Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Wed, 8 Apr 2026 18:19:20 +0200 +Subject: [PATCH] run: Use the same FD validation for all FD options + +--- + app/flatpak-builtins-run.c | 63 ++++++++++++++++++++++++++++++++++++-- + 1 file changed, 60 insertions(+), 3 deletions(-) + +diff --git a/app/flatpak-builtins-run.c b/app/flatpak-builtins-run.c +index 256f7828..bca4d865 100644 +--- a/app/flatpak-builtins-run.c ++++ b/app/flatpak-builtins-run.c +@@ -106,6 +106,63 @@ option_ro_bind_fd_cb (const char *option_name, + return TRUE; + } + ++static gboolean ++opt_instance_id_fd_cb (const char *option_name, ++ const char *value, ++ gpointer data, ++ GError **error) ++{ ++ glnx_autofd int fd = -1; ++ ++ fd = flatpak_parse_fd (value, error); ++ if (fd < 0) ++ return FALSE; ++ ++ if (fd < 3) ++ return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); ++ ++ opt_instance_id_fd = g_steal_fd (&fd); ++ return TRUE; ++} ++ ++static gboolean ++opt_app_fd_cb (const char *option_name, ++ const char *value, ++ gpointer data, ++ GError **error) ++{ ++ glnx_autofd int fd = -1; ++ ++ fd = flatpak_parse_fd (value, error); ++ if (fd < 0) ++ return FALSE; ++ ++ if (fd < 3) ++ return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); ++ ++ opt_app_fd = g_steal_fd (&fd); ++ return TRUE; ++} ++ ++static gboolean ++opt_usr_fd_cb (const char *option_name, ++ const char *value, ++ gpointer data, ++ GError **error) ++{ ++ glnx_autofd int fd = -1; ++ ++ fd = flatpak_parse_fd (value, error); ++ if (fd < 0) ++ return FALSE; ++ ++ if (fd < 3) ++ return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); ++ ++ opt_usr_fd = g_steal_fd (&fd); ++ return TRUE; ++} ++ + static GOptionEntry options[] = { + { "arch", 0, 0, G_OPTION_ARG_STRING, &opt_arch, N_("Arch to use"), N_("ARCH") }, + { "command", 0, 0, G_OPTION_ARG_STRING, &opt_command, N_("Command to run"), N_("COMMAND") }, +@@ -130,11 +187,11 @@ static GOptionEntry options[] = { + { "parent-pid", 0, 0, G_OPTION_ARG_INT, &opt_parent_pid, N_("Use PID as parent pid for sharing namespaces"), N_("PID") }, + { "parent-expose-pids", 0, 0, G_OPTION_ARG_NONE, &opt_parent_expose_pids, N_("Make processes visible in parent namespace"), NULL }, + { "parent-share-pids", 0, 0, G_OPTION_ARG_NONE, &opt_parent_share_pids, N_("Share process ID namespace with parent"), NULL }, +- { "instance-id-fd", 0, 0, G_OPTION_ARG_INT, &opt_instance_id_fd, N_("Write the instance ID to the given file descriptor"), NULL }, ++ { "instance-id-fd", 0, 0, G_OPTION_ARG_CALLBACK, &opt_instance_id_fd_cb, N_("Write the instance ID to the given file descriptor"), NULL }, + { "app-path", 0, 0, G_OPTION_ARG_FILENAME, &opt_app_path, N_("Use PATH instead of the app's /app"), N_("PATH") }, +- { "app-fd", 0, 0, G_OPTION_ARG_INT, &opt_app_fd, N_("Use FD instead of the app's /app"), N_("FD") }, ++ { "app-fd", 0, 0, G_OPTION_ARG_CALLBACK, &opt_app_fd_cb, N_("Use FD instead of the app's /app"), N_("FD") }, + { "usr-path", 0, 0, G_OPTION_ARG_FILENAME, &opt_usr_path, N_("Use PATH instead of the runtime's /usr"), N_("PATH") }, +- { "usr-fd", 0, 0, G_OPTION_ARG_INT, &opt_usr_fd, N_("Use FD instead of the runtime's /usr"), N_("FD") }, ++ { "usr-fd", 0, 0, G_OPTION_ARG_INT, &opt_usr_fd_cb, N_("Use FD instead of the runtime's /usr"), N_("FD") }, + { "bind-fd", 0, 0, G_OPTION_ARG_CALLBACK | G_OPTION_FLAG_HIDDEN, &option_bind_fd_cb, N_("Bind mount the file or directory referred to by FD to its canonicalized path"), N_("FD") }, + { "ro-bind-fd", 0, 0, G_OPTION_ARG_CALLBACK | G_OPTION_FLAG_HIDDEN, &option_ro_bind_fd_cb, N_("Bind mount the file or directory referred to by FD read-only to its canonicalized path"), N_("FD") }, + { NULL } +-- +2.54.0 + diff --git a/CVE-2026-34078-17-run-add-bind-fd-and-ro-bind-fd-binds-after-all-other-binds.patch b/CVE-2026-34078-17-run-add-bind-fd-and-ro-bind-fd-binds-after-all-other-binds.patch new file mode 100644 index 0000000..8ae228d --- /dev/null +++ b/CVE-2026-34078-17-run-add-bind-fd-and-ro-bind-fd-binds-after-all-other-binds.patch @@ -0,0 +1,97 @@ +From 6118b415e4ed1e35852736c9a28585f4093bf246 Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Wed, 8 Apr 2026 21:59:19 +0200 +Subject: [PATCH] run: Add bind-fd and ro-bind-fd binds after all other binds + +This is only moving it a bit down because +flatpak_run_add_environment_args still adds a whole bunch of binds which +then can over-mount the user requested binds (bind-fd, ro-bind-fd). +--- + common/flatpak-run.c | 64 ++++++++++++++++++++++---------------------- + 1 file changed, 32 insertions(+), 32 deletions(-) + +diff --git a/common/flatpak-run.c b/common/flatpak-run.c +index 5882b2a9..a8088a92 100644 +--- a/common/flatpak-run.c ++++ b/common/flatpak-run.c +@@ -3585,6 +3585,38 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + flatpak_bwrap_add_arg_printf (bwrap, "/run/user/%d", getuid ()); + } + ++ if (!flatpak_run_add_dconf_args (bwrap, app_id, metakey, error)) ++ return FALSE; ++ ++ if (!sandboxed && !(flags & FLATPAK_RUN_FLAG_NO_DOCUMENTS_PORTAL)) ++ add_document_portal_args (bwrap, app_id, &doc_mount_path); ++ ++ if (!flatpak_run_add_environment_args (bwrap, app_info_path, flags, ++ app_id, app_context, app_id_dir, previous_app_id_dirs, ++ per_app_dir_lock_fd, instance_id, ++ &exports, cancellable, error)) ++ return FALSE; ++ ++ if (per_app_dir_lock_path != NULL) ++ { ++ static const char lock[] = "/run/flatpak/per-app-dirs-ref"; ++ ++ flatpak_bwrap_add_args (bwrap, ++ "--ro-bind", per_app_dir_lock_path, lock, ++ "--lock-file", lock, ++ NULL); ++ } ++ ++ flatpak_run_add_socket_args_late (bwrap, app_context->shares); ++ add_font_path_args (bwrap); ++ add_icon_path_args (bwrap); ++ ++ flatpak_bwrap_add_args (bwrap, ++ /* Not in base, because we don't want this for flatpak build */ ++ "--symlink", "/app/lib/debug/source", "/run/build", ++ "--symlink", "/usr/lib/debug/source", "/run/build-runtime", ++ NULL); ++ + for (i = 0; bind_fds && i < bind_fds->len; i++) + { + int fd = g_array_index (bind_fds, int, i); +@@ -3619,38 +3651,6 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + return FALSE; + } + +- if (!flatpak_run_add_dconf_args (bwrap, app_id, metakey, error)) +- return FALSE; +- +- if (!sandboxed && !(flags & FLATPAK_RUN_FLAG_NO_DOCUMENTS_PORTAL)) +- add_document_portal_args (bwrap, app_id, &doc_mount_path); +- +- if (!flatpak_run_add_environment_args (bwrap, app_info_path, flags, +- app_id, app_context, app_id_dir, previous_app_id_dirs, +- per_app_dir_lock_fd, instance_id, +- &exports, cancellable, error)) +- return FALSE; +- +- if (per_app_dir_lock_path != NULL) +- { +- static const char lock[] = "/run/flatpak/per-app-dirs-ref"; +- +- flatpak_bwrap_add_args (bwrap, +- "--ro-bind", per_app_dir_lock_path, lock, +- "--lock-file", lock, +- NULL); +- } +- +- flatpak_run_add_socket_args_late (bwrap, app_context->shares); +- add_font_path_args (bwrap); +- add_icon_path_args (bwrap); +- +- flatpak_bwrap_add_args (bwrap, +- /* Not in base, because we don't want this for flatpak build */ +- "--symlink", "/app/lib/debug/source", "/run/build", +- "--symlink", "/usr/lib/debug/source", "/run/build-runtime", +- NULL); +- + if (cwd) + flatpak_bwrap_add_args (bwrap, "--chdir", cwd, NULL); + +-- +2.54.0 + diff --git a/CVE-2026-34078-18-portal-use-g-array-index-to-read-from-expose-fds-expose-fds-ro.patch b/CVE-2026-34078-18-portal-use-g-array-index-to-read-from-expose-fds-expose-fds-ro.patch new file mode 100644 index 0000000..8228f39 --- /dev/null +++ b/CVE-2026-34078-18-portal-use-g-array-index-to-read-from-expose-fds-expose-fds-ro.patch @@ -0,0 +1,38 @@ +From 8e71548ab56a97b88c7d34d68a6b04f6d430b2aa Mon Sep 17 00:00:00 2001 +From: Alberto Garcia +Date: Wed, 8 Apr 2026 19:44:29 +0200 +Subject: [PATCH] portal: use g_array_index() to read from expose_fds / + expose_fds_ro + +The data field of a GArray is a gchar* but we're storing integers +here, so use the proper method to ensure that we're getting the +element at the right offset and with the correct type. +--- + portal/flatpak-portal.c | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/portal/flatpak-portal.c b/portal/flatpak-portal.c +index a67ee330..0550a379 100644 +--- a/portal/flatpak-portal.c ++++ b/portal/flatpak-portal.c +@@ -1294,7 +1294,7 @@ handle_spawn (PortalFlatpak *object, + { + int remapped_fd; + +- remapped_fd = fd_map_remap_fd (fd_map, &max_fd, expose_fds->data[i]); ++ remapped_fd = fd_map_remap_fd (fd_map, &max_fd, g_array_index (expose_fds, int, i)); + + g_ptr_array_add (flatpak_argv, g_strdup_printf ("--bind-fd=%d", + remapped_fd)); +@@ -1304,7 +1304,7 @@ handle_spawn (PortalFlatpak *object, + { + int remapped_fd; + +- remapped_fd = fd_map_remap_fd (fd_map, &max_fd, expose_fds_ro->data[i]); ++ remapped_fd = fd_map_remap_fd (fd_map, &max_fd, g_array_index (expose_fds_ro, int, i)); + + g_ptr_array_add (flatpak_argv, g_strdup_printf ("--ro-bind-fd=%d", + remapped_fd)); +-- +2.54.0 + diff --git a/CVE-2026-34078-19-run-fix-backport-mistake.patch b/CVE-2026-34078-19-run-fix-backport-mistake.patch new file mode 100644 index 0000000..d53b480 --- /dev/null +++ b/CVE-2026-34078-19-run-fix-backport-mistake.patch @@ -0,0 +1,28 @@ +From ac540672bb6598285fd333e3863335d27236762b Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Thu, 9 Apr 2026 00:56:40 +0200 +Subject: [PATCH] run: Fix backport mistake + +Not even sure how this happened. Whoops. It's time to get some sleep. + +Fixes: c89a0c50 ("run: Use the same FD validation for all FD options") +--- + app/flatpak-builtins-run.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/app/flatpak-builtins-run.c b/app/flatpak-builtins-run.c +index bca4d865..23b9f368 100644 +--- a/app/flatpak-builtins-run.c ++++ b/app/flatpak-builtins-run.c +@@ -191,7 +191,7 @@ static GOptionEntry options[] = { + { "app-path", 0, 0, G_OPTION_ARG_FILENAME, &opt_app_path, N_("Use PATH instead of the app's /app"), N_("PATH") }, + { "app-fd", 0, 0, G_OPTION_ARG_CALLBACK, &opt_app_fd_cb, N_("Use FD instead of the app's /app"), N_("FD") }, + { "usr-path", 0, 0, G_OPTION_ARG_FILENAME, &opt_usr_path, N_("Use PATH instead of the runtime's /usr"), N_("PATH") }, +- { "usr-fd", 0, 0, G_OPTION_ARG_INT, &opt_usr_fd_cb, N_("Use FD instead of the runtime's /usr"), N_("FD") }, ++ { "usr-fd", 0, 0, G_OPTION_ARG_CALLBACK, &opt_usr_fd_cb, N_("Use FD instead of the runtime's /usr"), N_("FD") }, + { "bind-fd", 0, 0, G_OPTION_ARG_CALLBACK | G_OPTION_FLAG_HIDDEN, &option_bind_fd_cb, N_("Bind mount the file or directory referred to by FD to its canonicalized path"), N_("FD") }, + { "ro-bind-fd", 0, 0, G_OPTION_ARG_CALLBACK | G_OPTION_FLAG_HIDDEN, &option_ro_bind_fd_cb, N_("Bind mount the file or directory referred to by FD read-only to its canonicalized path"), N_("FD") }, + { NULL } +-- +2.54.0 + diff --git a/CVE-2026-34078-2-flatpak-bwrap-add-dup-ing-variant-flatpak-bwrap-add-args-data-fd-dup.patch b/CVE-2026-34078-2-flatpak-bwrap-add-dup-ing-variant-flatpak-bwrap-add-args-data-fd-dup.patch new file mode 100644 index 0000000..ab97027 --- /dev/null +++ b/CVE-2026-34078-2-flatpak-bwrap-add-dup-ing-variant-flatpak-bwrap-add-args-data-fd-dup.patch @@ -0,0 +1,61 @@ +From 36bd977772ea0df7159ff207f89564bfccd3b0ca Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Fri, 6 Feb 2026 17:56:21 +0100 +Subject: [PATCH] flatpak-bwrap: Add dup-ing variant + flatpak_bwrap_add_args_data_fd_dup + +--- + common/flatpak-bwrap-private.h | 5 +++++ + common/flatpak-bwrap.c | 20 ++++++++++++++++++++ + 2 files changed, 25 insertions(+) + +diff --git a/common/flatpak-bwrap-private.h b/common/flatpak-bwrap-private.h +index 9effed8a..d7a73304 100644 +--- a/common/flatpak-bwrap-private.h ++++ b/common/flatpak-bwrap-private.h +@@ -63,6 +63,11 @@ void flatpak_bwrap_append_bwrap (FlatpakBwrap *bwrap, + FlatpakBwrap *other); /* Steals the fds */ + void flatpak_bwrap_append_args (FlatpakBwrap *bwrap, + GPtrArray *other_array); ++gboolean flatpak_bwrap_add_args_data_fd_dup (FlatpakBwrap *bwrap, ++ const char *op, ++ int fd, ++ const char *path_optional, ++ GError **error); + void flatpak_bwrap_add_args_data_fd (FlatpakBwrap *bwrap, + const char *op, + int fd, +diff --git a/common/flatpak-bwrap.c b/common/flatpak-bwrap.c +index f7e8b10e..a3d29782 100644 +--- a/common/flatpak-bwrap.c ++++ b/common/flatpak-bwrap.c +@@ -146,6 +146,26 @@ flatpak_bwrap_add_fd (FlatpakBwrap *bwrap, + g_array_append_val (bwrap->fds, fd); + } + ++gboolean ++flatpak_bwrap_add_args_data_fd_dup (FlatpakBwrap *bwrap, ++ const char *op, ++ int fd, ++ const char *path_optional, ++ GError **error) ++{ ++ glnx_autofd int fd_dup = -1; ++ ++ fd_dup = fcntl (fd, F_DUPFD_CLOEXEC, 3); ++ if (fd_dup < 0) ++ return glnx_throw_errno_prefix (error, "Failed to dup fd %d", fd); ++ ++ flatpak_bwrap_add_args_data_fd (bwrap, ++ op, ++ g_steal_fd (&fd_dup), ++ path_optional); ++ return TRUE; ++} ++ + void + flatpak_bwrap_add_arg_printf (FlatpakBwrap *bwrap, const char *format, ...) + { +-- +2.54.0 + diff --git a/CVE-2026-34078-20-tests-test-run-custom-test-usr-path-usr-fd-app-path-app-fd.patch b/CVE-2026-34078-20-tests-test-run-custom-test-usr-path-usr-fd-app-path-app-fd.patch new file mode 100644 index 0000000..27f8ddd --- /dev/null +++ b/CVE-2026-34078-20-tests-test-run-custom-test-usr-path-usr-fd-app-path-app-fd.patch @@ -0,0 +1,190 @@ +From ab8ca4d6a02900974c08694f54903b2616875bf0 Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Wed, 8 Apr 2026 14:20:53 +0200 +Subject: [PATCH] tests/test-run-custom: Test --usr-path, --usr-fd, --app-path, + --app-fd + +--- + tests/test-matrix/meson.build | 2 + + tests/test-run-custom.sh | 156 ++++++++++++++++++++++++++++++++++ + 2 files changed, 158 insertions(+) + create mode 100755 tests/test-run-custom.sh + +diff --git a/tests/test-matrix/meson.build b/tests/test-matrix/meson.build +index fd0b5034..ef85b7ec 100644 +--- a/tests/test-matrix/meson.build ++++ b/tests/test-matrix/meson.build +@@ -45,4 +45,6 @@ wrapped_tests += {'name' : 'test-unused.sh', 'script' : 'test-unused.sh'} + wrapped_tests += {'name' : 'test-prune.sh', 'script' : 'test-prune.sh'} + wrapped_tests += {'name' : 'test-seccomp.sh', 'script' : 'test-seccomp.sh'} + wrapped_tests += {'name' : 'test-repair.sh', 'script' : 'test-repair.sh'} + wrapped_tests += {'name' : 'test-preinstall.sh', 'script' : 'test-preinstall.sh'} ++wrapped_tests += {'name' : 'test-run-custom@user.wrap', 'script' : 'test-run-custom.sh'} ++wrapped_tests += {'name' : 'test-run-custom@system.wrap', 'script' : 'test-run-custom.sh'} +\ No newline at end of file +diff --git a/tests/test-run-custom.sh b/tests/test-run-custom.sh +new file mode 100755 +index 00000000..f8a11f03 +--- /dev/null ++++ b/tests/test-run-custom.sh +@@ -0,0 +1,156 @@ ++#!/bin/bash ++# ++# Copyright (C) 2026 Red Hat, Inc. ++# ++# This library is free software; you can redistribute it and/or ++# modify it under the terms of the GNU Lesser General Public ++# License as published by the Free Software Foundation; either ++# version 2 of the License, or (at your option) any later version. ++# ++# This library is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++# Lesser General Public License for more details. ++# ++# You should have received a copy of the GNU Lesser General Public ++# License along with this library; if not, write to the ++# Free Software Foundation, Inc., 59 Temple Place - Suite 330, ++# Boston, MA 02111-1307, USA. ++ ++set -euo pipefail ++ ++. "$(dirname "$0")/libtest.sh" ++ ++skip_without_bwrap ++skip_revokefs_without_fuse ++ ++echo "1..10" ++ ++# Use stable rather than master as the branch so we can test that the run ++# command automatically finds the branch correctly ++setup_repo "" "" stable ++install_repo "" stable ++ ++setup_repo_no_add custom org.test.Collection.Custom master ++make_updated_runtime custom org.test.Collection.Custom master CUSTOM ++make_updated_app custom org.test.Collection.Custom master CUSTOM ++ ++ostree checkout -U --repo=repos/custom runtime/org.test.Platform/${ARCH}/master custom-runtime >&2 ++ostree checkout -U --repo=repos/custom app/org.test.Hello/$ARCH/master custom-app >&2 ++ ++cat custom-runtime/files/bin/runtime_hello.sh > runtime_hello ++assert_file_has_content runtime_hello "runtimeCUSTOM" ++cat custom-app/files/bin/hello.sh > app_hello ++assert_file_has_content app_hello "sandboxCUSTOM" ++ ++run org.test.Hello > hello_out ++assert_file_has_content hello_out '^Hello world, from a sandbox$' ++ ++run --command=/usr/bin/runtime_hello.sh org.test.Hello > hello_out ++assert_file_has_content hello_out '^Hello world, from a runtime$' ++ ++ok "setup" ++ ++! run --app-path="" --command=/app/bin/hello.sh org.test.Hello > /dev/null ++ ++run --app-path="" --command=/usr/bin/runtime_hello.sh org.test.Hello > hello_out ++assert_file_has_content hello_out '^Hello world, from a runtime$' ++ ++run --app-path="" --command=/run/parent/app/bin/hello.sh org.test.Hello > hello_out ++assert_file_has_content hello_out '^Hello world, from a sandbox$' ++ ++! run --app-path="" --command=/run/parent/usr/bin/runtime_hello.sh org.test.Hello > /dev/null ++ ++ok "empty app path" ++ ++run --app-path=custom-app/files --command=/app/bin/hello.sh org.test.Hello > hello_out ++assert_file_has_content hello_out '^Hello world, from a sandboxCUSTOM$' ++ ++run --app-path=custom-app/files --command=/usr/bin/runtime_hello.sh org.test.Hello > hello_out ++assert_file_has_content hello_out '^Hello world, from a runtime$' ++ ++run --app-path=custom-app/files --command=/run/parent/app/bin/hello.sh org.test.Hello > hello_out ++assert_file_has_content hello_out '^Hello world, from a sandbox$' ++ ++! run --app-path=custom-app/files --command=/run/parent/usr/bin/runtime_hello.sh org.test.Hello > /dev/null ++ ++ok "custom app path" ++ ++! run --app-path=path-which-does-not-exist org.test.Hello > /dev/null ++ ++ok "bad custom app path" ++ ++exec 3< custom-app/files ++run --app-fd=3 --command=/app/bin/hello.sh org.test.Hello > hello_out ++assert_file_has_content hello_out '^Hello world, from a sandboxCUSTOM$' ++exec 3>&- ++ ++! run --app-fd=3 --command=/app/bin/hello.sh org.test.Hello > /dev/null ++ ++ok "custom app fd" ++ ++run --usr-path=custom-runtime/files --command=/app/bin/hello.sh org.test.Hello > hello_out ++assert_file_has_content hello_out '^Hello world, from a sandbox$' ++ ++run --usr-path=custom-runtime/files --command=/usr/bin/runtime_hello.sh org.test.Hello > hello_out ++assert_file_has_content hello_out '^Hello world, from a runtimeCUSTOM$' ++ ++! run --usr-path=custom-runtime/files --command=/run/parent/app/bin/hello.sh org.test.Hello > /dev/null ++ ++run --usr-path=custom-runtime/files --command=/run/parent/usr/bin/runtime_hello.sh org.test.Hello > hello_out ++assert_file_has_content hello_out '^Hello world, from a runtime$' ++ ++ok "custom usr path" ++ ++! run --usr-path=path-which-does-not-exist org.test.Hello > /dev/null ++ ++ok "bad custom usr path" ++ ++exec 3< custom-runtime/files ++run --usr-fd=3 --command=/app/bin/hello.sh org.test.Hello > hello_out ++assert_file_has_content hello_out '^Hello world, from a sandbox$' ++exec 3>&- ++ ++exec 3< custom-runtime/files ++run --usr-fd=3 --command=/usr/bin/runtime_hello.sh org.test.Hello > hello_out ++assert_file_has_content hello_out '^Hello world, from a runtimeCUSTOM$' ++exec 3>&- ++ ++! run --usr-fd=3 --command=/app/bin/hello.sh org.test.Hello > /dev/null ++ ++ok "custom usr fd" ++ ++run --usr-path=custom-runtime/files --app-path=custom-app/files \ ++ --command=/app/bin/hello.sh org.test.Hello > hello_out ++assert_file_has_content hello_out '^Hello world, from a sandboxCUSTOM$' ++ ++run --usr-path=custom-runtime/files --app-path=custom-app/files \ ++ --command=/usr/bin/runtime_hello.sh org.test.Hello > hello_out ++assert_file_has_content hello_out '^Hello world, from a runtimeCUSTOM$' ++ ++run --usr-path=custom-runtime/files --app-path=custom-app/files \ ++ --command=/run/parent/app/bin/hello.sh org.test.Hello > hello_out ++assert_file_has_content hello_out '^Hello world, from a sandbox$' ++ ++run --usr-path=custom-runtime/files --app-path=custom-app/files \ ++ --command=/run/parent/usr/bin/runtime_hello.sh org.test.Hello > hello_out ++assert_file_has_content hello_out '^Hello world, from a runtime$' ++ ++ok "custom usr and app path" ++ ++! run --usr-path=custom-runtime/files --app-path="" \ ++ --command=/app/bin/hello.sh org.test.Hello > /dev/null ++ ++run --usr-path=custom-runtime/files --app-path="" \ ++ --command=/usr/bin/runtime_hello.sh org.test.Hello > hello_out ++assert_file_has_content hello_out '^Hello world, from a runtimeCUSTOM$' ++ ++run --usr-path=custom-runtime/files --app-path="" \ ++ --command=/run/parent/app/bin/hello.sh org.test.Hello > hello_out ++assert_file_has_content hello_out '^Hello world, from a sandbox$' ++ ++run --usr-path=custom-runtime/files --app-path="" \ ++ --command=/run/parent/usr/bin/runtime_hello.sh org.test.Hello > hello_out ++assert_file_has_content hello_out '^Hello world, from a runtime$' ++ ++ok "custom usr and empty app path" +\ No newline at end of file +-- +2.54.0 + diff --git a/CVE-2026-34078-21-tests-test-run-custom-test-bind-fd-and-ro-bind-fd.patch b/CVE-2026-34078-21-tests-test-run-custom-test-bind-fd-and-ro-bind-fd.patch new file mode 100644 index 0000000..7447855 --- /dev/null +++ b/CVE-2026-34078-21-tests-test-run-custom-test-bind-fd-and-ro-bind-fd.patch @@ -0,0 +1,56 @@ +From 8ebffc2ad37fb5f2c03641506e8091aee53defd8 Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Wed, 8 Apr 2026 21:55:22 +0200 +Subject: [PATCH] tests/test-run-custom: Test --bind-fd and --ro-bind-fd + +--- + tests/test-run-custom.sh | 27 +++++++++++++++++++++++++-- + 1 file changed, 25 insertions(+), 2 deletions(-) + +diff --git a/tests/test-run-custom.sh b/tests/test-run-custom.sh +index f8a11f03..94c32416 100755 +--- a/tests/test-run-custom.sh ++++ b/tests/test-run-custom.sh +@@ -24,7 +24,7 @@ set -euo pipefail + skip_without_bwrap + skip_revokefs_without_fuse + +-echo "1..10" ++echo "1..11" + + # Use stable rather than master as the branch so we can test that the run + # command automatically finds the branch correctly +@@ -153,4 +153,27 @@ run --usr-path=custom-runtime/files --app-path="" \ + --command=/run/parent/usr/bin/runtime_hello.sh org.test.Hello > hello_out + assert_file_has_content hello_out '^Hello world, from a runtime$' + +-ok "custom usr and empty app path" +\ No newline at end of file ++ok "custom usr and empty app path" ++ ++path="$(readlink -f .)/foo" ++echo "bar" > "${path}" ++ ++exec 3< "${path}" ++run --bind-fd=3 --command=cat org.test.Hello "${path}" > hello_out ++assert_file_has_content hello_out '^bar$' ++ ++exec 3< "${path}" ++run --bind-fd=3 --command=bash org.test.Hello -c "echo baz > ${path}" > /dev/null ++assert_file_has_content "${path}" '^baz$' ++exec 3>&- ++ ++exec 3< "${path}" ++run --ro-bind-fd=3 --command=cat org.test.Hello "${path}" > hello_out ++assert_file_has_content hello_out '^baz$' ++exec 3>&- ++ ++exec 3< "${path}" ++! run --ro-bind-fd=3 --command=bash org.test.Hello -c "echo baz > ${path}" > /dev/null ++exec 3>&- ++ ++ok "bind-fd and ro-bind-fd" +\ No newline at end of file +-- +2.54.0 + diff --git a/CVE-2026-34078-22-run-cope-with-an-empty-runtime.patch b/CVE-2026-34078-22-run-cope-with-an-empty-runtime.patch new file mode 100644 index 0000000..cb28fed --- /dev/null +++ b/CVE-2026-34078-22-run-cope-with-an-empty-runtime.patch @@ -0,0 +1,110 @@ +From b942152bde980cde51e933359e4ba3be527ae3ed Mon Sep 17 00:00:00 2001 +From: Simon McVittie +Date: Thu, 9 Apr 2026 18:45:04 +0100 +Subject: [PATCH] run: Cope with an empty runtime + +When FlatpakDir runs extra-data helpers in apply_extra_data(), +if the helper is statically linked, it might not need a runtime at all. +For example the helper for openh264 falls into this category. + +Fixes: ac62ebe3 "run: Use O_PATH fds for the runtime and app deploy directories" +Helps: https://github.com/flatpak/flatpak/issues/6583 +Signed-off-by: Simon McVittie +(cherry picked from commit aa1a54c9dae25fd13ebc936e06996f8db39f4aa5) +--- + common/flatpak-run.c | 30 +++++++++++++++++++++++------- + 1 file changed, 23 insertions(+), 7 deletions(-) + +diff --git a/common/flatpak-run.c b/common/flatpak-run.c +index a8088a92..f176e884 100644 +--- a/common/flatpak-run.c ++++ b/common/flatpak-run.c +@@ -1618,6 +1618,10 @@ flatpak_run_add_app_info_args (FlatpakBwrap *bwrap, + return TRUE; + } + ++/* ++ * @runtime_fd: the /usr for the runtime, or -1 if running with no runtime, ++ * perhaps to unpack extra-data ++ */ + static void + add_tzdata_args (FlatpakBwrap *bwrap, + int runtime_fd) +@@ -1630,6 +1634,8 @@ add_tzdata_args (FlatpakBwrap *bwrap, + glnx_autofd int zoneinfo_fd = -1; + g_autoptr(GError) error = NULL; + ++ g_return_if_fail (runtime_fd >= -1); ++ + raw_timezone = flatpak_get_timezone (); + timezone_content = g_strdup_printf ("%s\n", raw_timezone); + localtime_content = g_strconcat ("../usr/share/zoneinfo/", raw_timezone, NULL); +@@ -1638,10 +1644,11 @@ add_tzdata_args (FlatpakBwrap *bwrap, + + tzdir_fd = glnx_chaseat (AT_FDCWD, tzdir, GLNX_CHASE_MUST_BE_DIRECTORY, NULL); + +- zoneinfo_fd = glnx_chaseat (runtime_fd, "share/zoneinfo", +- GLNX_CHASE_RESOLVE_BENEATH | +- GLNX_CHASE_MUST_BE_DIRECTORY, +- NULL); ++ if (runtime_fd >= 0) ++ zoneinfo_fd = glnx_chaseat (runtime_fd, "share/zoneinfo", ++ GLNX_CHASE_RESOLVE_BENEATH | ++ GLNX_CHASE_MUST_BE_DIRECTORY, ++ NULL); + + /* Check for host /usr/share/zoneinfo */ + if (tzdir_fd >= 0 && zoneinfo_fd >= 0) +@@ -1652,7 +1659,7 @@ add_tzdata_args (FlatpakBwrap *bwrap, + "--symlink", localtime_content, "/etc/localtime", + NULL); + } +- else ++ else if (runtime_fd >= 0) + { + g_autofree char *runtime_zoneinfo = NULL; + glnx_autofd int runtime_zoneinfo_fd = -1; +@@ -2163,6 +2170,10 @@ setup_seccomp (FlatpakBwrap *bwrap, + } + #endif + ++/* ++ * @runtime_fd: the /usr for the runtime, or -1 if running with no runtime, ++ * perhaps to unpack extra-data ++ */ + static void + flatpak_run_setup_usr_links (FlatpakBwrap *bwrap, + int runtime_fd, +@@ -2226,6 +2237,10 @@ static const char *const sysfs_dirs[] = + "/sys/devices" + }; + ++/* ++ * @runtime_fd: the /usr for the runtime, or -1 if running with no runtime, ++ * perhaps to unpack extra-data ++ */ + gboolean + flatpak_run_setup_base_argv (FlatpakBwrap *bwrap, + int runtime_fd, +@@ -2246,7 +2261,7 @@ flatpak_run_setup_base_argv (FlatpakBwrap *bwrap, + gboolean bwrap_unprivileged = flatpak_bwrap_is_unprivileged (); + gsize i; + +- g_return_val_if_fail (runtime_fd >= 0, FALSE); ++ g_return_val_if_fail (runtime_fd >= -1, FALSE); + + /* Disable recursive userns for all flatpak processes, as we need this + * to guarantee that the sandbox can't restructure the filesystem. +@@ -2355,7 +2370,8 @@ flatpak_run_setup_base_argv (FlatpakBwrap *bwrap, + else if (g_file_test ("/var/lib/dbus/machine-id", G_FILE_TEST_EXISTS)) + flatpak_bwrap_add_args (bwrap, "--ro-bind", "/var/lib/dbus/machine-id", "/etc/machine-id", NULL); + +- if ((flags & FLATPAK_RUN_FLAG_WRITABLE_ETC) == 0) ++ if (runtime_fd >= 0 ++ && (flags & FLATPAK_RUN_FLAG_WRITABLE_ETC) == 0) + { + g_auto(GLnxDirFdIterator) dfd_iter = { 0, }; + struct dirent *dent; +-- +2.54.0 + diff --git a/CVE-2026-34078-23-dir-in-apply-extra-data-don-t-assume-there-is-always-a-runtime.patch b/CVE-2026-34078-23-dir-in-apply-extra-data-don-t-assume-there-is-always-a-runtime.patch new file mode 100644 index 0000000..0768377 --- /dev/null +++ b/CVE-2026-34078-23-dir-in-apply-extra-data-don-t-assume-there-is-always-a-runtime.patch @@ -0,0 +1,44 @@ +From a140dd16297df7dee73bfd6c99c278d7a3fa580c Mon Sep 17 00:00:00 2001 +From: Simon McVittie +Date: Thu, 9 Apr 2026 18:47:40 +0100 +Subject: [PATCH] dir: In apply_extra_data(), don't assume there is always a + runtime + +org.freedesktop.Platform.openh264 is one example of an extension that +runs a statically-linked extra-data helper, with no runtime. Only open +the runtime if there is one. + +Fixes: ac62ebe3 "run: Use O_PATH fds for the runtime and app deploy directories" +Resolves: https://github.com/flatpak/flatpak/issues/6583 +Signed-off-by: Simon McVittie +(cherry picked from commit c14ad3722940706730a76997c6925f9998106f90) +--- + common/flatpak-dir.c | 12 ++++++++---- + 1 file changed, 8 insertions(+), 4 deletions(-) + +diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c +index 7e8fbf6b..e381b16d 100644 +--- a/common/flatpak-dir.c ++++ b/common/flatpak-dir.c +@@ -8476,10 +8476,14 @@ apply_extra_data (FlatpakDir *self, + run_flags |= FLATPAK_RUN_FLAG_NO_PROC; + + glnx_autofd int usr_fd = -1; +- usr_fd = open (flatpak_file_get_path_cached (runtime_files), +- O_PATH | O_CLOEXEC | O_NOFOLLOW); +- if (usr_fd < 0) +- return glnx_throw_errno_prefix (error, "Failed to open runtime files"); ++ ++ if (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)) +-- +2.54.0 + diff --git a/CVE-2026-34078-24-tests-add-test-extra-data-sh-to-test-extra-data-installation.patch b/CVE-2026-34078-24-tests-add-test-extra-data-sh-to-test-extra-data-installation.patch new file mode 100644 index 0000000..6bd3ff3 --- /dev/null +++ b/CVE-2026-34078-24-tests-add-test-extra-data-sh-to-test-extra-data-installation.patch @@ -0,0 +1,126 @@ +From 2bf787c496c8ea0349cee668c90fd52824157a01 Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Thu, 21 Aug 2025 18:40:33 +0200 +Subject: [PATCH] tests: Add test-extra-data.sh to test extra-data installation + +[smcv: Remove OCI test because #3790 was only solved in 1.17.x, +only keep libostree test] + +(cherry picked from commit e9e713fa0ddd0d1af4b8cdec6aa9124d471ed33e) +--- + tests/meson.build | 1 + + tests/test-extra-data.sh | 63 +++++++++++++++++++++++++++++++++++ + tests/test-matrix/meson.build | 2 ++ + tests/update-test-matrix | 1 + + 4 files changed, 67 insertions(+) + create mode 100755 tests/test-extra-data.sh + +diff --git a/tests/meson.build b/tests/meson.build +index cb187d71..8f96e5f0 100644 +--- a/tests/meson.build ++++ b/tests/meson.build +@@ -362,6 +362,7 @@ foreach testcase : wrapped_tests + 'test-summaries.sh' : 60, + 'test-unused.sh' : 90, + 'test-update-portal.sh' : 90, ++ 'test-extra-data.sh' : 90, + }.get(script, 30) + + is_parallel = { +diff --git a/tests/test-extra-data.sh b/tests/test-extra-data.sh +new file mode 100755 +index 00000000..41c9b913 +--- /dev/null ++++ b/tests/test-extra-data.sh +@@ -0,0 +1,63 @@ ++#!/bin/bash ++# ++# Copyright (C) 2025 Red Hat, Inc ++# ++# This library is free software; you can redistribute it and/or ++# modify it under the terms of the GNU Lesser General Public ++# License as published by the Free Software Foundation; either ++# version 2 of the License, or (at your option) any later version. ++# ++# This library is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++# Lesser General Public License for more details. ++# ++# You should have received a copy of the GNU Lesser General Public ++# License along with this library; if not, write to the ++# Free Software Foundation, Inc., 59 Temple Place - Suite 330, ++# Boston, MA 02111-1307, USA. ++ ++set -euo pipefail ++ ++. "$(dirname $0)/libtest.sh" ++ ++skip_without_bwrap ++ ++REPONAME="test" ++BRANCH="master" ++COLLECTION_ID="org.test.Collection.${REPONAME}" ++ ++setup_repo ${REPONAME} ${COLLECTION_ID} ++ ++# create the extra data ++EXTRA_DATA_FILE="extra-data-test" ++EXTRA_DATA_DIR="${TEST_DATA_DIR}/extra-data-server/" ++mkdir -p "${EXTRA_DATA_DIR}" ++echo "extra-data-test-content" > "${EXTRA_DATA_DIR}/${EXTRA_DATA_FILE}" ++ ++# serve the extra data ++httpd web-server.py "${EXTRA_DATA_DIR}" ++EXTRA_DATA_URL="http://127.0.0.1:$(cat httpd-port)/${EXTRA_DATA_FILE}" ++ ++# download to get the size and sha256 sum ++DOWNLOADED_EXTRA_DATA="${TEST_DATA_DIR}/downloaded-extra-data" ++curl "${EXTRA_DATA_URL}" -o "${DOWNLOADED_EXTRA_DATA}" ++EXTRA_DATA_SIZE=$(stat --printf="%s" "${DOWNLOADED_EXTRA_DATA}") ++EXTRA_DATA_SHA256=$(sha256sum "${DOWNLOADED_EXTRA_DATA}" | cut -f1 -d' ') ++ ++echo "1..1" ++ ++# build the app with the extra data ++EXTRA_DATA="--extra-data=test:${EXTRA_DATA_SHA256}:${EXTRA_DATA_SIZE}:${EXTRA_DATA_SIZE}:${EXTRA_DATA_URL}" ++BUILD_FINISH_ARGS=${EXTRA_DATA} make_updated_app ${REPONAME} ${COLLECTION_ID} ${BRANCH} UPDATE1 ++ ++# ensure it installs correctly ++install_repo ${REPONAME} ${BRANCH} ++ ++# ensure the right extra-data got downloaded ++${FLATPAK} run --command=sh org.test.Hello -c "cat /app/extra/test" > out ++assert_file_has_content out "extra-data-test-content" ++ ++${FLATPAK} ${U} uninstall -y org.test.Hello >&2 ++ ++ok "install extra data app with ostree" +diff --git a/tests/test-matrix/meson.build b/tests/test-matrix/meson.build +index ef85b7ec..037500eb 100644 +--- a/tests/test-matrix/meson.build ++++ b/tests/test-matrix/meson.build +@@ -29,6 +29,8 @@ wrapped_tests += {'name' : 'test-summaries@user.wrap', 'script' : 'test-summarie + wrapped_tests += {'name' : 'test-summaries@system.wrap', 'script' : 'test-summaries.sh'} + wrapped_tests += {'name' : 'test-subset@user.wrap', 'script' : 'test-subset.sh'} + wrapped_tests += {'name' : 'test-subset@system.wrap', 'script' : 'test-subset.sh'} ++wrapped_tests += {'name' : 'test-extra-data@user.wrap', 'script' : 'test-extra-data.sh'} ++wrapped_tests += {'name' : 'test-extra-data@system.wrap', 'script' : 'test-extra-data.sh'} + wrapped_tests += {'name' : 'test-basic.sh', 'script' : 'test-basic.sh'} + wrapped_tests += {'name' : 'test-completion.sh', 'script' : 'test-completion.sh'} + wrapped_tests += {'name' : 'test-config.sh', 'script' : 'test-config.sh'} +diff --git a/tests/update-test-matrix b/tests/update-test-matrix +index 3a51d0ba..2e26a6f2 100755 +--- a/tests/update-test-matrix ++++ b/tests/update-test-matrix +@@ -34,6 +34,7 @@ TEST_MATRIX_SOURCE=( + 'tests/test-prune.sh' \ + 'tests/test-seccomp.sh' \ + 'tests/test-repair.sh' \ ++ 'tests/test-extra-data.sh{user+system}' \ + ) + + "${tests_srcdir}/expand-test-matrix.sh" --meson "${TEST_MATRIX_SOURCE[*]}" > "${tests_srcdir}/test-matrix/meson.build" +-- +2.54.0 + diff --git a/CVE-2026-34078-25-tests-add-test-for-noruntime-extra-data-app.patch b/CVE-2026-34078-25-tests-add-test-for-noruntime-extra-data-app.patch new file mode 100644 index 0000000..7d1cc31 --- /dev/null +++ b/CVE-2026-34078-25-tests-add-test-for-noruntime-extra-data-app.patch @@ -0,0 +1,121 @@ +From b9ec9cab240ffdfd8768dc1dd6272d40719ecdcd Mon Sep 17 00:00:00 2001 +From: bbhtt +Date: Fri, 10 Apr 2026 10:14:23 +0530 +Subject: [PATCH] tests: Add test for NoRuntime extra-data app + +(cherry picked from commit 79cb10e8803a3f13fe5bd8329dec37e58cfa2a84) +--- + tests/apply-extra-static.c | 15 +++++++++++++ + tests/meson.build | 15 +++++++++++++ + tests/test-extra-data.sh | 43 +++++++++++++++++++++++++++++++++++++- + 3 files changed, 72 insertions(+), 1 deletion(-) + create mode 100644 tests/apply-extra-static.c + +diff --git a/tests/apply-extra-static.c b/tests/apply-extra-static.c +new file mode 100644 +index 00000000..5be3993a +--- /dev/null ++++ b/tests/apply-extra-static.c +@@ -0,0 +1,15 @@ ++#include ++#include ++#include ++ ++static const char msg[] = "noruntime-extra-data\n"; ++ ++int main(void) { ++ mkdir("/app/extra", 0755); ++ int fd = open("/app/extra/ran", O_WRONLY | O_CREAT | O_TRUNC, 0644); ++ if (fd < 0) ++ return 1; ++ write(fd, msg, sizeof(msg) - 1); ++ close(fd); ++ return 0; ++} +diff --git a/tests/meson.build b/tests/meson.build +index 8f96e5f0..464ed0bb 100644 +--- a/tests/meson.build ++++ b/tests/meson.build +@@ -291,6 +291,21 @@ executable( + install_dir : installed_testdir, + ) + ++if cc.links('#include \nint main(void) { printf("test"); return 0; }', ++ args: ['-static'], ++ name: 'static libc') ++ executable( ++ 'apply-extra-static', ++ 'apply-extra-static.c', ++ install : get_option('installed_tests'), ++ install_dir : installed_testdir, ++ override_options : ['b_sanitize=none'], ++ link_args : ['-static'], ++ ) ++else ++ warning('static libc not available, apply-extra-static will be skipped') ++endif ++ + subdir('share/xdg-desktop-portal/portals') + subdir('services') + subdir('test-keyring') +diff --git a/tests/test-extra-data.sh b/tests/test-extra-data.sh +index 41c9b913..73fe3ce9 100755 +--- a/tests/test-extra-data.sh ++++ b/tests/test-extra-data.sh +@@ -45,7 +45,7 @@ curl "${EXTRA_DATA_URL}" -o "${DOWNLOADED_EXTRA_DATA}" + EXTRA_DATA_SIZE=$(stat --printf="%s" "${DOWNLOADED_EXTRA_DATA}") + EXTRA_DATA_SHA256=$(sha256sum "${DOWNLOADED_EXTRA_DATA}" | cut -f1 -d' ') + +-echo "1..1" ++echo "1..2" + + # build the app with the extra data + EXTRA_DATA="--extra-data=test:${EXTRA_DATA_SHA256}:${EXTRA_DATA_SIZE}:${EXTRA_DATA_SIZE}:${EXTRA_DATA_URL}" +@@ -61,3 +61,44 @@ assert_file_has_content out "extra-data-test-content" + ${FLATPAK} ${U} uninstall -y org.test.Hello >&2 + + ok "install extra data app with ostree" ++ ++build_extra_data_noruntime_app() { ++ local repo="$1" ++ local branch="$2" ++ ++ DIR="$(mktemp -d)" ++ ARCH="$(flatpak --default-arch)" ++ ++ mkdir -p "${DIR}/files/bin" ++ ++ cp "${G_TEST_BUILDDIR}/apply-extra-static" "${DIR}/files/bin/apply_extra" ++ chmod +x "${DIR}/files/bin/apply_extra" ++ ++ cat > "${DIR}/metadata" <&2 ++ flatpak build-export ${FL_GPGARGS} "${repo}" "${DIR}" "${branch}" >&2 ++ rm -rf "${DIR}" ++} ++ ++if test -f "${G_TEST_BUILDDIR}/apply-extra-static"; then ++ build_extra_data_noruntime_app repos/test ${BRANCH} ++ update_repo ${REPONAME} ${COLLECTION_ID} ++ ${FLATPAK} ${U} install --or-update -y ${REPONAME}-repo org.test.ExtraDataNoRuntime ${BRANCH} >&2 ++ DEPLOY_DIR="$(${FLATPAK} ${U} info --show-location org.test.ExtraDataNoRuntime)" ++ assert_file_has_content "${DEPLOY_DIR}/files/extra/ran" "^noruntime-extra-data$" ++ ${FLATPAK} ${U} uninstall -y org.test.ExtraDataNoRuntime >&2 ++ ok "install+run extra data app with NoRuntime" ++else ++ ok "# SKIP install+run extra data app with NoRuntime apply-extra-static not found" ++fi +-- +2.54.0 + diff --git a/CVE-2026-34078-26-utils-add-flatpak-set-cloexec.patch b/CVE-2026-34078-26-utils-add-flatpak-set-cloexec.patch new file mode 100644 index 0000000..2f5ae9c --- /dev/null +++ b/CVE-2026-34078-26-utils-add-flatpak-set-cloexec.patch @@ -0,0 +1,52 @@ +From 90625f0ef612364ded91a67327bdfe5b59e03b27 Mon Sep 17 00:00:00 2001 +From: Simon McVittie +Date: Fri, 10 Apr 2026 09:58:05 +0100 +Subject: [PATCH] utils: Add flatpak_set_cloexec() + +Helps: https://github.com/flatpak/flatpak/issues/6582 +Signed-off-by: Simon McVittie +(cherry picked from commit 8a989c790d9121f53ada88fd001a3997b9e40632) +--- + common/flatpak-utils-private.h | 2 ++ + common/flatpak-utils.c | 17 +++++++++++++++++ + 2 files changed, 19 insertions(+) + +diff --git a/common/flatpak-utils-private.h b/common/flatpak-utils-private.h +index 8f970ea2..e7fa348c 100644 +--- a/common/flatpak-utils-private.h ++++ b/common/flatpak-utils-private.h +@@ -353,4 +353,6 @@ int flatpak_parse_fd (const char *fd_string, + + #define FLATPAK_MESSAGE_ID "c7b39b1e006b464599465e105b361485" + ++gboolean flatpak_set_cloexec (int fd); ++ + #endif /* __FLATPAK_UTILS_H__ */ +diff --git a/common/flatpak-utils.c b/common/flatpak-utils.c +index 7e69e8be..706a9cb2 100644 +--- a/common/flatpak-utils.c ++++ b/common/flatpak-utils.c +@@ -2525,3 +2525,20 @@ flatpak_parse_fd (const char *fd_string, + + return fd; + } ++ ++/* Sets errno on failure. */ ++gboolean ++flatpak_set_cloexec (int fd) ++{ ++ int flags = fcntl (fd, F_GETFD); ++ ++ if (flags == -1) ++ return FALSE; ++ ++ flags |= FD_CLOEXEC; ++ ++ if (fcntl (fd, F_SETFD, flags) < 0) ++ return FALSE; ++ ++ return TRUE; ++} +-- +2.54.0 + diff --git a/CVE-2026-34078-27-run-context-mark-fd-arguments-as-close-on-exec.patch b/CVE-2026-34078-27-run-context-mark-fd-arguments-as-close-on-exec.patch new file mode 100644 index 0000000..5007d3f --- /dev/null +++ b/CVE-2026-34078-27-run-context-mark-fd-arguments-as-close-on-exec.patch @@ -0,0 +1,102 @@ +From a58978e40e7650c9c2cda716dd6d387fd6b5ea8e Mon Sep 17 00:00:00 2001 +From: Simon McVittie +Date: Fri, 10 Apr 2026 10:07:14 +0100 +Subject: [PATCH] run, context: Mark fd arguments as close-on-exec + +On entry to `flatpak run`, these fds have been inheritable (not +FD_CLOEXEC), otherwise they would not have been inherited; but we don't +want the "payload" command to inherit them, so set them as +non-close-on-exec as soon as we receive them. In the cases where we pass +them down to the underlying bwrap command, we'll either dup them, or +set them to be inheritable again (in practice we dup them). + +In particular, Chromium-derived web browsers get very upset when their +subsandbox processes inherit unexpected fds, which has been causing crashes +with no useful diagnostic information since CVE-2026-34078 was fixed. + +Fixes: 1b5e886d "run: Add --usr-fd and --app-fd options" +Fixes: b5ae89ed "run: Add --(ro-)bind-fd options" +Resolves: https://github.com/flatpak/flatpak/issues/6582 +Signed-off-by: Simon McVittie +(cherry picked from commit 0902090726c2e51b1c6f22c64d708a4895a196e7) +--- + app/flatpak-builtins-run.c | 15 +++++++++++++++ + common/flatpak-context.c | 8 ++++++++ + 2 files changed, 23 insertions(+) + +diff --git a/app/flatpak-builtins-run.c b/app/flatpak-builtins-run.c +index 23b9f368..48968fbb 100644 +--- a/app/flatpak-builtins-run.c ++++ b/app/flatpak-builtins-run.c +@@ -81,6 +81,9 @@ option_bind_fd_cb (const char *option_name, + if (fd < 3) + return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); + ++ if (!flatpak_set_cloexec (fd)) ++ return glnx_throw_errno_prefix (error, "--bind-fd"); ++ + g_array_append_val (opt_bind_fds, fd); + fd = -1; /* ownership transferred to GArray */ + return TRUE; +@@ -101,6 +104,9 @@ option_ro_bind_fd_cb (const char *option_name, + if (fd < 3) + return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); + ++ if (!flatpak_set_cloexec (fd)) ++ return glnx_throw_errno_prefix (error, "--ro-bind-fd"); ++ + g_array_append_val (opt_ro_bind_fds, fd); + fd = -1; /* ownership transferred to GArray */ + return TRUE; +@@ -121,6 +127,9 @@ opt_instance_id_fd_cb (const char *option_name, + if (fd < 3) + return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); + ++ if (!flatpak_set_cloexec (fd)) ++ return glnx_throw_errno_prefix (error, "--instance-id-fd"); ++ + opt_instance_id_fd = g_steal_fd (&fd); + return TRUE; + } +@@ -140,6 +149,9 @@ opt_app_fd_cb (const char *option_name, + if (fd < 3) + return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); + ++ if (!flatpak_set_cloexec (fd)) ++ return glnx_throw_errno_prefix (error, "--app-fd"); ++ + opt_app_fd = g_steal_fd (&fd); + return TRUE; + } +@@ -159,6 +171,9 @@ opt_usr_fd_cb (const char *option_name, + if (fd < 3) + return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); + ++ if (!flatpak_set_cloexec (fd)) ++ return glnx_throw_errno_prefix (error, "--usr-fd"); ++ + opt_usr_fd = g_steal_fd (&fd); + return TRUE; + } +diff --git a/common/flatpak-context.c b/common/flatpak-context.c +index b5206aab..5c66a9ee 100644 +--- a/common/flatpak-context.c ++++ b/common/flatpak-context.c +@@ -1391,6 +1391,14 @@ option_env_fd_cb (const gchar *option_name, + if (fd < 3) + return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); + ++ /* This is not strictly necessary, because we're going to close it after ++ * parsing the environment block, but let's be consistent with other fd ++ * arguments that we need to avoid being inherited by the "payload" ++ * command. This is also a convenient place to validate that it's an ++ * open fd. */ ++ if (!flatpak_set_cloexec (fd)) ++ return glnx_throw_errno_prefix (error, "--env-fd"); ++ + return flatpak_context_parse_env_fd (context, fd, error); + } + +-- +2.54.0 + diff --git a/CVE-2026-34078-28-utils-move-flatpak-get-path-for-fd-to-here.patch b/CVE-2026-34078-28-utils-move-flatpak-get-path-for-fd-to-here.patch new file mode 100644 index 0000000..2420083 --- /dev/null +++ b/CVE-2026-34078-28-utils-move-flatpak-get-path-for-fd-to-here.patch @@ -0,0 +1,179 @@ +From ccaf86c854aeed47c2b346666b14e52d0f821b7f Mon Sep 17 00:00:00 2001 +From: Simon McVittie +Date: Thu, 9 Apr 2026 20:06:18 +0100 +Subject: [PATCH] utils: Move flatpak_get_path_for_fd to here + +This was originally in flatpak-portal, then was duplicated into +flatpak-run in commit ac62ebe3 "run: Use O_PATH fds for the runtime and +app deploy directories", and subsequently removed from the portal in +commit 3c500145 "portal: Use --bind-fd, --app-fd and --usr-fd options to +avoid races". Now we want to use it in the portal again. + +Helps: https://github.com/flatpak/flatpak/issues/6584 +Co-authored-by: Sebastian Wick +Signed-off-by: Simon McVittie +(cherry picked from commit 15dc818874514ffbece4c080405353ed396b54a9) +--- + common/flatpak-run.c | 44 +++--------------------------- + common/flatpak-utils-private.h | 3 ++ + common/flatpak-utils.c | 50 ++++++++++++++++++++++++++++++++++ + 3 files changed, 57 insertions(+), 40 deletions(-) + +diff --git a/common/flatpak-run.c b/common/flatpak-run.c +index f176e884..7d46dd6b 100644 +--- a/common/flatpak-run.c ++++ b/common/flatpak-run.c +@@ -2939,42 +2939,6 @@ open_namespace_fd_if_needed (const char *path, + return -1; + } + +-static char * +-get_path_for_fd (int fd, +- GError **error) +-{ +- g_autofree char *proc_path = NULL; +- g_autofree char *path = NULL; +- +- proc_path = g_strdup_printf ("/proc/self/fd/%d", fd); +- path = glnx_readlinkat_malloc (AT_FDCWD, proc_path, NULL, error); +- if (path == NULL) +- return NULL; +- +- /* All normal paths start with /, but some weird things +- don't, such as socket:[27345] or anon_inode:[eventfd]. +- We don't support any of these */ +- if (path[0] != '/') +- { +- return glnx_null_throw (error, "%s resolves to non-absolute path %s", +- proc_path, path); +- } +- +- /* File descriptors to actually deleted files have " (deleted)" +- appended to them. This also happens to some fake fd types +- like shmem which are "/ (deleted)". All such +- files are considered invalid. Unfortunately this also +- matches files with filenames that actually end in " (deleted)", +- but there is not much to do about this. */ +- if (g_str_has_suffix (path, " (deleted)")) +- { +- return glnx_null_throw (error, "%s resolves to deleted path %s", +- proc_path, path); +- } +- +- return g_steal_pointer (&path); +-} +- + gboolean + flatpak_run_app (FlatpakDecomposed *app_ref, + FlatpakDeploy *app_deploy, +@@ -3203,7 +3167,7 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + { + g_autofree char *path = NULL; + +- path = get_path_for_fd (custom_runtime_fd, &my_error); ++ path = flatpak_get_path_for_fd (custom_runtime_fd, &my_error); + if (path == NULL) + { + return flatpak_fail_error (error, FLATPAK_ERROR, +@@ -3331,7 +3295,7 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + { + g_autofree char *path = NULL; + +- path = get_path_for_fd (custom_app_fd, error); ++ path = flatpak_get_path_for_fd (custom_app_fd, error); + if (path == NULL) + return glnx_prefix_error (error, "Cannot convert custom app fd to path"); + +@@ -3640,7 +3604,7 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + + /* We get the path the fd refers to, to determine to mount point + * destination inside the sandbox */ +- path = get_path_for_fd (fd, error); ++ path = flatpak_get_path_for_fd (fd, error); + if (!path) + return FALSE; + +@@ -3657,7 +3621,7 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + + /* We get the path the fd refers to, to determine to mount point + * destination inside the sandbox */ +- path = get_path_for_fd (fd, error); ++ path = flatpak_get_path_for_fd (fd, error); + if (!path) + return FALSE; + +diff --git a/common/flatpak-utils-private.h b/common/flatpak-utils-private.h +index e7fa348c..8cad6672 100644 +--- a/common/flatpak-utils-private.h ++++ b/common/flatpak-utils-private.h +@@ -351,6 +351,9 @@ gboolean flatpak_is_debugging (void); + int flatpak_parse_fd (const char *fd_string, + GError **error); + ++char * flatpak_get_path_for_fd (int fd, ++ GError **error); ++ + #define FLATPAK_MESSAGE_ID "c7b39b1e006b464599465e105b361485" + + gboolean flatpak_set_cloexec (int fd); +diff --git a/common/flatpak-utils.c b/common/flatpak-utils.c +index 706a9cb2..b8bfcfdb 100644 +--- a/common/flatpak-utils.c ++++ b/common/flatpak-utils.c +@@ -2542,3 +2542,53 @@ flatpak_set_cloexec (int fd) + + return TRUE; + } ++ ++/* ++ * Attempt to discover the filesystem path corresponding to @fd. ++ * ++ * If @fd points to an existing file, return the absolute path of that ++ * file in the environment where it was opened. Note that this is not ++ * necessarily a valid path in the current namespace, if it was ++ * transferred via fd-passing from a process in a different filesystem ++ * namespace. ++ * ++ * If @fd points to a deleted file, or to a socket, fifo, memfd or similar ++ * non-filesystem object, set an error and return %NULL. ++ * ++ * Returns: (type filename) (transfer full) (nullable): ++ */ ++char * ++flatpak_get_path_for_fd (int fd, ++ GError **error) ++{ ++ g_autofree char *proc_path = NULL; ++ g_autofree char *path = NULL; ++ ++ proc_path = g_strdup_printf ("/proc/self/fd/%d", fd); ++ path = glnx_readlinkat_malloc (AT_FDCWD, proc_path, NULL, error); ++ if (path == NULL) ++ return NULL; ++ ++ /* All normal paths start with /, but some weird things ++ don't, such as socket:[27345] or anon_inode:[eventfd]. ++ We don't support any of these */ ++ if (path[0] != '/') ++ { ++ return glnx_null_throw (error, "%s resolves to non-absolute path %s", ++ proc_path, path); ++ } ++ ++ /* File descriptors to actually deleted files have " (deleted)" ++ appended to them. This also happens to some fake fd types ++ like shmem which are "/ (deleted)". All such ++ files are considered invalid. Unfortunately this also ++ matches files with filenames that actually end in " (deleted)", ++ but there is not much to do about this. */ ++ if (g_str_has_suffix (path, " (deleted)")) ++ { ++ return glnx_null_throw (error, "%s resolves to deleted path %s", ++ proc_path, path); ++ } ++ ++ return g_steal_pointer (&path); ++} +-- +2.54.0 + diff --git a/CVE-2026-34078-29-portal-avoid-crash-if-sandbox-expose-ro-fd-is-out-of-range.patch b/CVE-2026-34078-29-portal-avoid-crash-if-sandbox-expose-ro-fd-is-out-of-range.patch new file mode 100644 index 0000000..80c54e0 --- /dev/null +++ b/CVE-2026-34078-29-portal-avoid-crash-if-sandbox-expose-ro-fd-is-out-of-range.patch @@ -0,0 +1,66 @@ +From 5f73d530a25a119222b42b0680766faba3875777 Mon Sep 17 00:00:00 2001 +From: Simon McVittie +Date: Thu, 9 Apr 2026 20:16:16 +0100 +Subject: [PATCH] portal: Avoid crash if sandbox-expose-[ro-]fd is out of range + +If the handle is not in the range `0 <= handle < fds_len`, but no +GError is set, we'd have crashed when we dereferenced error->message. +Instead, log an error and early-return, matching what we do for +app-fd, usr-fd and the array of inheritable fds. + +Fixes: 3c500145 "portal: Use --bind-fd, --app-fd and --usr-fd options to avoid races" +Helps: https://github.com/flatpak/flatpak/issues/6584 +Co-authored-by: Sebastian Wick +Signed-off-by: Simon McVittie +(cherry picked from commit 4ef2421bd22d8fbf8f17cf9bf5da1dd95aedef8d) +--- + portal/flatpak-portal.c | 26 ++++++++++++++++++++++---- + 1 file changed, 22 insertions(+), 4 deletions(-) + +diff --git a/portal/flatpak-portal.c b/portal/flatpak-portal.c +index 0550a379..be4d40a8 100644 +--- a/portal/flatpak-portal.c ++++ b/portal/flatpak-portal.c +@@ -1247,8 +1247,17 @@ handle_spawn (PortalFlatpak *object, + gint32 handle; + + g_variant_get_child (sandbox_expose_fd, i, "h", &handle); +- if (handle >= 0 && handle < fds_len && +- validate_opath_fd (fds[handle], TRUE, &error)) ++ if (handle >= fds_len || handle < 0) ++ { ++ g_debug ("Invalid sandbox-expose-fd handle %d", handle); ++ g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, ++ G_DBUS_ERROR_INVALID_ARGS, ++ "No file descriptor for handle %d", ++ handle); ++ return G_DBUS_METHOD_INVOCATION_HANDLED; ++ } ++ ++ if (validate_opath_fd (fds[handle], TRUE, &error)) + { + g_array_append_val (expose_fds, fds[handle]); + } +@@ -1273,8 +1282,17 @@ handle_spawn (PortalFlatpak *object, + gint32 handle; + + g_variant_get_child (sandbox_expose_fd_ro, i, "h", &handle); +- if (handle >= 0 && handle < fds_len && +- validate_opath_fd (fds[handle], FALSE, &error)) ++ if (handle >= fds_len || handle < 0) ++ { ++ g_debug ("Invalid sandbox-expose-ro-fd handle %d", handle); ++ g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, ++ G_DBUS_ERROR_INVALID_ARGS, ++ "No file descriptor for handle %d", ++ handle); ++ return G_DBUS_METHOD_INVOCATION_HANDLED; ++ } ++ ++ if (validate_opath_fd (fds[handle], FALSE, &error)) + { + g_array_append_val (expose_fds_ro, fds[handle]); + } +-- +2.54.0 + diff --git a/CVE-2026-34078-3-utils-add-flatpak-parse-fd.patch b/CVE-2026-34078-3-utils-add-flatpak-parse-fd.patch new file mode 100644 index 0000000..735985e --- /dev/null +++ b/CVE-2026-34078-3-utils-add-flatpak-parse-fd.patch @@ -0,0 +1,94 @@ +From fbe5a2faa776e0f5bdd6160c1dd69b0b97c2d8eb Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Fri, 6 Feb 2026 17:14:49 +0100 +Subject: [PATCH] utils: Add flatpak_parse_fd + +This is meant to parse file descriptor strings passed via the command +line. It is not a security mechanism and will happily accept fds 0-3 as +well. +--- + common/flatpak-context.c | 19 +++++++------------ + common/flatpak-utils-private.h | 3 +++ + common/flatpak-utils.c | 22 ++++++++++++++++++++++ + 3 files changed, 32 insertions(+), 12 deletions(-) + +diff --git a/common/flatpak-context.c b/common/flatpak-context.c +index 646c5ec5..b5206aab 100644 +--- a/common/flatpak-context.c ++++ b/common/flatpak-context.c +@@ -1382,21 +1382,16 @@ option_env_fd_cb (const gchar *option_name, + GError **error) + { + FlatpakContext *context = data; +- guint64 fd; +- gchar *endptr; +- gboolean ret; ++ glnx_autofd int fd = -1; + +- fd = g_ascii_strtoull (value, &endptr, 10); +- +- if (endptr == NULL || *endptr != '\0' || fd > G_MAXINT) +- return glnx_throw (error, "Not a valid file descriptor: %s", value); +- +- ret = flatpak_context_parse_env_fd (context, (int) fd, error); ++ fd = flatpak_parse_fd (value, error); ++ if (fd < 0) ++ return FALSE; + +- if (fd >= 3) +- close (fd); ++ if (fd < 3) ++ return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); + +- return ret; ++ return flatpak_context_parse_env_fd (context, fd, error); + } + + static gboolean +diff --git a/common/flatpak-utils-private.h b/common/flatpak-utils-private.h +index 7fe509f3..8f970ea2 100644 +--- a/common/flatpak-utils-private.h ++++ b/common/flatpak-utils-private.h +@@ -348,6 +348,9 @@ gboolean running_under_sudo_root (void); + void flatpak_set_debugging (gboolean debugging); + gboolean flatpak_is_debugging (void); + ++int flatpak_parse_fd (const char *fd_string, ++ GError **error); ++ + #define FLATPAK_MESSAGE_ID "c7b39b1e006b464599465e105b361485" + + #endif /* __FLATPAK_UTILS_H__ */ +diff --git a/common/flatpak-utils.c b/common/flatpak-utils.c +index 7bfa2ad1..7a30969c 100644 +--- a/common/flatpak-utils.c ++++ b/common/flatpak-utils.c +@@ -2473,3 +2473,25 @@ flatpak_is_debugging (void) + + return is_debugging; + } ++ ++int ++flatpak_parse_fd (const char *fd_string, ++ GError **error) ++{ ++ guint64 parsed; ++ char *endptr; ++ int fd; ++ struct stat stbuf; ++ ++ parsed = g_ascii_strtoull (fd_string, &endptr, 10); ++ ++ if (endptr == NULL || *endptr != '\0' || parsed > G_MAXINT) ++ return glnx_fd_throw (error, "Not a valid file descriptor: %s", fd_string); ++ ++ fd = (int) parsed; ++ ++ if (!glnx_fstat (fd, &stbuf, error)) ++ return -1; ++ ++ return fd; ++} +-- +2.54.0 + diff --git a/CVE-2026-34078-30-portal-log-and-ignore-unusable-sandbox-expose-fds-instead-of-erroring.patch b/CVE-2026-34078-30-portal-log-and-ignore-unusable-sandbox-expose-fds-instead-of-erroring.patch new file mode 100644 index 0000000..3e48bce --- /dev/null +++ b/CVE-2026-34078-30-portal-log-and-ignore-unusable-sandbox-expose-fds-instead-of-erroring.patch @@ -0,0 +1,65 @@ +From 4b6ee5e5c9ca33638b238c15e865864ed1437fb8 Mon Sep 17 00:00:00 2001 +From: Simon McVittie +Date: Thu, 9 Apr 2026 20:24:48 +0100 +Subject: [PATCH] portal: Log and ignore unusable sandbox-expose fds instead of + erroring + +For the sandbox expose fds, a historical quirk of this code is that if +the checks in get_path_for_fd() failed, we would merely log at g_info() +level (usually only shown when debugging the portal), and otherwise +silently ignore the request to expose the fd in the sandbox. + +With hindsight this was probably not the right thing to do, but apps +could well be relying on it now. For example, there are indications +that Epiphany might send a memfd from the main instance to a subsandbox, +which never actually worked, but will break that subsandbox process +if that's treated as a fatal error. + +Fixes: 3c500145 "portal: Use --bind-fd, --app-fd and --usr-fd options to avoid races" +Helps: https://github.com/flatpak/flatpak/issues/6584 +Co-authored-by: Sebastian Wick +Signed-off-by: Simon McVittie +(cherry picked from commit 75ab6eebb857fd26172613b69e55f04830ad0d82) +--- + portal/flatpak-portal.c | 18 ++++++------------ + 1 file changed, 6 insertions(+), 12 deletions(-) + +diff --git a/portal/flatpak-portal.c b/portal/flatpak-portal.c +index be4d40a8..1803dad1 100644 +--- a/portal/flatpak-portal.c ++++ b/portal/flatpak-portal.c +@@ -1263,12 +1263,9 @@ handle_spawn (PortalFlatpak *object, + } + else + { +- g_debug ("Invalid sandbox expose fd: %s", error->message); +- g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, +- G_DBUS_ERROR_INVALID_ARGS, +- "No valid file descriptor for handle %d", +- handle); +- return G_DBUS_METHOD_INVOCATION_HANDLED; ++ g_info ("unable to validate sandbox-expose-fd %d, ignoring: %s", ++ fds[handle], error->message); ++ g_clear_error (&error); + } + } + } +@@ -1298,12 +1295,9 @@ handle_spawn (PortalFlatpak *object, + } + else + { +- g_debug ("Invalid sandbox expose ro fd: %s", error->message); +- g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, +- G_DBUS_ERROR_INVALID_ARGS, +- "No file descriptor for handle %d", +- handle); +- return G_DBUS_METHOD_INVOCATION_HANDLED; ++ g_info ("unable to validate sandbox-expose-ro-fd %d, ignoring: %s", ++ fds[handle], error->message); ++ g_clear_error (&error); + } + } + } +-- +2.54.0 + diff --git a/CVE-2026-34078-31-portal-reinstate-flatpak-get-path-for-fd-checks.patch b/CVE-2026-34078-31-portal-reinstate-flatpak-get-path-for-fd-checks.patch new file mode 100644 index 0000000..96f3f5c --- /dev/null +++ b/CVE-2026-34078-31-portal-reinstate-flatpak-get-path-for-fd-checks.patch @@ -0,0 +1,69 @@ +From 0ef48d4a4bd7479d35c2526104292e36d9ea1500 Mon Sep 17 00:00:00 2001 +From: Simon McVittie +Date: Thu, 9 Apr 2026 20:28:57 +0100 +Subject: [PATCH] portal: Reinstate flatpak_get_path_for_fd() checks + +As with the previous commit, historically we would debug-log but +otherwise silently ignore attempts to expose a file in a sandboxed +subsandbox that doesn't have a suitable path. + +For example, org.gnome.Epiphany (or possibly WebKitGTK) asks to expose +files from /app and /usr in the subsandbox. When we ignored those +requests (because /app and /usr have a different meaning on the host +system), the app worked as intended anyway, because the subsandbox has +access to the app's /app and the runtime's /usr whether they're +explicitly added or not, so it all worked out OK. However, treating +this as a fatal error (as it arguably should have been) broke +Epiphany's subsandboxes. + +Fixes: 3c500145 "portal: Use --bind-fd, --app-fd and --usr-fd options to avoid races" +Resolves: https://github.com/flatpak/flatpak/issues/6584 +Co-authored-by: Sebastian Wick +Signed-off-by: Simon McVittie +(cherry picked from commit 28634c7f52e57df7091007973d1bb5e1f87f1e9d) +--- + portal/flatpak-portal.c | 20 ++++++++++++++++++++ + 1 file changed, 20 insertions(+) + +diff --git a/portal/flatpak-portal.c b/portal/flatpak-portal.c +index 1803dad1..e2f59b73 100644 +--- a/portal/flatpak-portal.c ++++ b/portal/flatpak-portal.c +@@ -558,7 +558,9 @@ validate_opath_fd (int fd, + { + int fd_flags; + struct stat st_buf; ++ struct stat real_st_buf; + int access_mode; ++ g_autofree char *path = NULL; + + /* Must be able to get fd flags */ + fd_flags = fcntl (fd, F_GETFL); +@@ -577,6 +579,24 @@ validate_opath_fd (int fd, + if (fstat (fd, &st_buf) < 0) + return glnx_throw_errno_prefix (error, "Failed to fstat"); + ++ path = flatpak_get_path_for_fd (fd, error); ++ if (path == NULL) ++ return FALSE; ++ ++ /* Verify that this is the same file as the app opened. ++ * Note that this is not security relevant because flatpak-run/bwrap will ++ * check things and abort if something is off. We do this only for backwards ++ * compatibility reasons: we need to be able to ignore the issue instead of ++ * aborting the entire sandbox setup later. */ ++ if (stat (path, &real_st_buf) < 0 || ++ st_buf.st_dev != real_st_buf.st_dev || ++ st_buf.st_ino != real_st_buf.st_ino) ++ { ++ /* Different files on the inside and the outside, reject the request */ ++ return glnx_throw (error, ++ "different file inside and outside sandbox"); ++ } ++ + access_mode = R_OK; + if (S_ISDIR (st_buf.st_mode)) + access_mode |= X_OK; +-- +2.54.0 + diff --git a/CVE-2026-34078-32-libtest-allow-adding-a-new-ref-to-an-existing-temporary-ostree-repo.patch b/CVE-2026-34078-32-libtest-allow-adding-a-new-ref-to-an-existing-temporary-ostree-repo.patch new file mode 100644 index 0000000..366ec96 --- /dev/null +++ b/CVE-2026-34078-32-libtest-allow-adding-a-new-ref-to-an-existing-temporary-ostree-repo.patch @@ -0,0 +1,37 @@ +From b7806fe7838e6a9114163fbcd20c11e7636d695f Mon Sep 17 00:00:00 2001 +From: Simon McVittie +Date: Fri, 10 Apr 2026 11:38:12 +0100 +Subject: [PATCH] libtest: Allow adding a new ref to an existing temporary + ostree repo + +When we run `tests/test-run-custom.sh` as a build-time test, +we expect to already have the necessary runtimes, apps, etc. in +`${builddir}/tests/runtime-repo`. However, when running "as-installed" +tests, we're using a fresh temporary ostree repo for each test. +Merely having the repo exist is not enough: for some tests, and in +particular `tests/test-run-custom.sh`, it needs to have more than one +runtime available. + +Resolves: https://github.com/flatpak/flatpak/issues/6591 +Signed-off-by: Simon McVittie +(cherry picked from commit 50dda82eb054695b3d3758d0a88ef68c8dd79dc4) +--- + tests/libtest.sh | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/tests/libtest.sh b/tests/libtest.sh +index 823b1be2..7004f2d1 100644 +--- a/tests/libtest.sh ++++ b/tests/libtest.sh +@@ -312,7 +312,7 @@ make_runtime () { + RUNTIME_REPO=${TEST_DATA_DIR}/runtime-repo + ( + flock -s 200 +- if [ ! -d ${RUNTIME_REPO} ]; then ++ if [ ! -f "${RUNTIME_REPO}/refs/heads/${RUNTIME_REF}" ]; then + $(dirname $0)/make-test-runtime.sh ${RUNTIME_REPO} org.test.Platform ${BRANCH} "" "" > /dev/null + fi + ) 200>${TEST_DATA_DIR}/runtime-repo-lock +-- +2.54.0 + diff --git a/CVE-2026-34078-33-tests-check-that-flatpak-run-fd-arguments-do-not-leak-to-the-command.patch b/CVE-2026-34078-33-tests-check-that-flatpak-run-fd-arguments-do-not-leak-to-the-command.patch new file mode 100644 index 0000000..68ff92b --- /dev/null +++ b/CVE-2026-34078-33-tests-check-that-flatpak-run-fd-arguments-do-not-leak-to-the-command.patch @@ -0,0 +1,61 @@ +From 4491406f509707feb755982fdda24b2fd9234903 Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Fri, 10 Apr 2026 15:44:44 +0200 +Subject: [PATCH] tests: Check that flatpak-run fd-arguments do not leak to the + command + +flatpak-run takes a number of arguments which are file descriptor +numbers. Those file descriptors are supposed to set something up in the +way the instance gets spawned, but should never make it to the wrapper +command. + +(cherry picked from commit 136452768368613f3712c2f7cba79a7c3e7bee96) +--- + tests/test-run-custom.sh | 25 +++++++++++++++++++++++-- + 1 file changed, 23 insertions(+), 2 deletions(-) + +diff --git a/tests/test-run-custom.sh b/tests/test-run-custom.sh +index 94c32416..4868b8b5 100755 +--- a/tests/test-run-custom.sh ++++ b/tests/test-run-custom.sh +@@ -24,7 +24,7 @@ set -euo pipefail + skip_without_bwrap + skip_revokefs_without_fuse + +-echo "1..11" ++echo "1..12" + + # Use stable rather than master as the branch so we can test that the run + # command automatically finds the branch correctly +@@ -176,4 +176,25 @@ exec 3< "${path}" + ! run --ro-bind-fd=3 --command=bash org.test.Hello -c "echo baz > ${path}" > /dev/null + exec 3>&- + +-ok "bind-fd and ro-bind-fd" +\ No newline at end of file ++ok "bind-fd and ro-bind-fd" ++ ++exec 3< custom-app/files ++exec 4< custom-runtime/files ++exec 5< "${path}" ++exec 6< "${path}" ++run --app-fd=3 --usr-fd=4 --bind-fd=5 --ro-bind-fd=6 \ ++ --command=sh org.test.Hello \ ++ -c 'for fd in $(ls /proc/self/fd); do readlink -f /proc/self/fd/$fd; done' > hello_out ++exec 6>&- ++exec 5>&- ++exec 4>&- ++exec 3>&- ++ ++wd="$(readlink -f .)" ++while read fdpath; do ++ if [[ "$fdpath" == "$wd"* && "$fdpath" != "$wd/hello_out" ]]; then ++ assert_not_reached "A fd for '$fdpath' unexpectedly made it to the app" ++ fi ++done < hello_out ++ ++ok "check no fd leak" +\ No newline at end of file +-- +2.54.0 + diff --git a/CVE-2026-34078-34-app-context-never-close-fds-0-1-or-2.patch b/CVE-2026-34078-34-app-context-never-close-fds-0-1-or-2.patch new file mode 100644 index 0000000..b7a0d89 --- /dev/null +++ b/CVE-2026-34078-34-app-context-never-close-fds-0-1-or-2.patch @@ -0,0 +1,103 @@ +From 7f5306876adc6f322f88c01801fd5aab2742c7b4 Mon Sep 17 00:00:00 2001 +From: Simon McVittie +Date: Fri, 10 Apr 2026 14:00:14 +0100 +Subject: [PATCH] app, context: Never close fds 0, 1 or 2 + +These fds are stdin, stdout and stderr respectively, and are expected +to remain open at all times (if they are not needed then they can point +to /dev/null, but they should always be open). If the user gives us +`--env-fd=2` or similar, we don't want to close fd 2 before exiting +unsuccessfully: that would give us nowhere to display the error message. + +Signed-off-by: Simon McVittie +(cherry picked from commit c4ab58cd2e66c4bcf193919ef9cbdce1dac042da) +--- + app/flatpak-builtins-run.c | 26 +++++++++++++++++++++----- + common/flatpak-context.c | 6 +++++- + 2 files changed, 26 insertions(+), 6 deletions(-) + +diff --git a/app/flatpak-builtins-run.c b/app/flatpak-builtins-run.c +index 48968fbb..f1827a13 100644 +--- a/app/flatpak-builtins-run.c ++++ b/app/flatpak-builtins-run.c +@@ -79,7 +79,11 @@ option_bind_fd_cb (const char *option_name, + return FALSE; + + if (fd < 3) +- return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); ++ { ++ /* Don't close these fds! */ ++ fd = -1; ++ return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); ++ } + + if (!flatpak_set_cloexec (fd)) + return glnx_throw_errno_prefix (error, "--bind-fd"); +@@ -102,7 +106,10 @@ option_ro_bind_fd_cb (const char *option_name, + return FALSE; + + if (fd < 3) +- return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); ++ { ++ fd = -1; ++ return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); ++ } + + if (!flatpak_set_cloexec (fd)) + return glnx_throw_errno_prefix (error, "--ro-bind-fd"); +@@ -125,7 +132,10 @@ opt_instance_id_fd_cb (const char *option_name, + return FALSE; + + if (fd < 3) +- return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); ++ { ++ fd = -1; ++ return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); ++ } + + if (!flatpak_set_cloexec (fd)) + return glnx_throw_errno_prefix (error, "--instance-id-fd"); +@@ -147,7 +157,10 @@ opt_app_fd_cb (const char *option_name, + return FALSE; + + if (fd < 3) +- return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); ++ { ++ fd = -1; ++ return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); ++ } + + if (!flatpak_set_cloexec (fd)) + return glnx_throw_errno_prefix (error, "--app-fd"); +@@ -169,7 +182,10 @@ opt_usr_fd_cb (const char *option_name, + return FALSE; + + if (fd < 3) +- return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); ++ { ++ fd = -1; ++ return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); ++ } + + if (!flatpak_set_cloexec (fd)) + return glnx_throw_errno_prefix (error, "--usr-fd"); +diff --git a/common/flatpak-context.c b/common/flatpak-context.c +index 5c66a9ee..af113946 100644 +--- a/common/flatpak-context.c ++++ b/common/flatpak-context.c +@@ -1389,7 +1389,11 @@ option_env_fd_cb (const gchar *option_name, + return FALSE; + + if (fd < 3) +- return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); ++ { ++ /* Don't close these fds! */ ++ fd = -1; ++ return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); ++ } + + /* This is not strictly necessary, because we're going to close it after + * parsing the environment block, but let's be consistent with other fd +-- +2.54.0 + diff --git a/CVE-2026-34078-35-app-context-factor-out-flatpak-accept-fd-argument.patch b/CVE-2026-34078-35-app-context-factor-out-flatpak-accept-fd-argument.patch new file mode 100644 index 0000000..ced9c08 --- /dev/null +++ b/CVE-2026-34078-35-app-context-factor-out-flatpak-accept-fd-argument.patch @@ -0,0 +1,235 @@ +From 817888730861a21bf930f7fbadc28155c2f3de39 Mon Sep 17 00:00:00 2001 +From: Simon McVittie +Date: Fri, 10 Apr 2026 15:02:43 +0100 +Subject: [PATCH] app, context: Factor out flatpak_accept_fd_argument() + +Signed-off-by: Simon McVittie +(cherry picked from commit d42037c5267ac7967ce285b9052b25fe7a968368) +--- + app/flatpak-builtins-run.c | 61 ++++++---------------------------- + common/flatpak-context.c | 18 ++-------- + common/flatpak-utils-private.h | 4 +++ + common/flatpak-utils.c | 47 ++++++++++++++++++++++++++ + 4 files changed, 63 insertions(+), 67 deletions(-) + +diff --git a/app/flatpak-builtins-run.c b/app/flatpak-builtins-run.c +index f1827a13..b6185ca7 100644 +--- a/app/flatpak-builtins-run.c ++++ b/app/flatpak-builtins-run.c +@@ -74,20 +74,11 @@ option_bind_fd_cb (const char *option_name, + { + glnx_autofd int fd = -1; + +- fd = flatpak_parse_fd (value, error); ++ fd = flatpak_accept_fd_argument (option_name, value, error); ++ + if (fd < 0) + return FALSE; + +- if (fd < 3) +- { +- /* Don't close these fds! */ +- fd = -1; +- return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); +- } +- +- if (!flatpak_set_cloexec (fd)) +- return glnx_throw_errno_prefix (error, "--bind-fd"); +- + g_array_append_val (opt_bind_fds, fd); + fd = -1; /* ownership transferred to GArray */ + return TRUE; +@@ -101,19 +92,11 @@ option_ro_bind_fd_cb (const char *option_name, + { + glnx_autofd int fd = -1; + +- fd = flatpak_parse_fd (value, error); ++ fd = flatpak_accept_fd_argument (option_name, value, error); ++ + if (fd < 0) + return FALSE; + +- if (fd < 3) +- { +- fd = -1; +- return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); +- } +- +- if (!flatpak_set_cloexec (fd)) +- return glnx_throw_errno_prefix (error, "--ro-bind-fd"); +- + g_array_append_val (opt_ro_bind_fds, fd); + fd = -1; /* ownership transferred to GArray */ + return TRUE; +@@ -127,19 +110,11 @@ opt_instance_id_fd_cb (const char *option_name, + { + glnx_autofd int fd = -1; + +- fd = flatpak_parse_fd (value, error); ++ fd = flatpak_accept_fd_argument (option_name, value, error); ++ + if (fd < 0) + return FALSE; + +- if (fd < 3) +- { +- fd = -1; +- return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); +- } +- +- if (!flatpak_set_cloexec (fd)) +- return glnx_throw_errno_prefix (error, "--instance-id-fd"); +- + opt_instance_id_fd = g_steal_fd (&fd); + return TRUE; + } +@@ -152,19 +127,11 @@ opt_app_fd_cb (const char *option_name, + { + glnx_autofd int fd = -1; + +- fd = flatpak_parse_fd (value, error); ++ fd = flatpak_accept_fd_argument (option_name, value, error); ++ + if (fd < 0) + return FALSE; + +- if (fd < 3) +- { +- fd = -1; +- return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); +- } +- +- if (!flatpak_set_cloexec (fd)) +- return glnx_throw_errno_prefix (error, "--app-fd"); +- + opt_app_fd = g_steal_fd (&fd); + return TRUE; + } +@@ -177,19 +144,11 @@ opt_usr_fd_cb (const char *option_name, + { + glnx_autofd int fd = -1; + +- fd = flatpak_parse_fd (value, error); ++ fd = flatpak_accept_fd_argument (option_name, value, error); ++ + if (fd < 0) + return FALSE; + +- if (fd < 3) +- { +- fd = -1; +- return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); +- } +- +- if (!flatpak_set_cloexec (fd)) +- return glnx_throw_errno_prefix (error, "--usr-fd"); +- + opt_usr_fd = g_steal_fd (&fd); + return TRUE; + } +diff --git a/common/flatpak-context.c b/common/flatpak-context.c +index af113946..59e8d9d4 100644 +--- a/common/flatpak-context.c ++++ b/common/flatpak-context.c +@@ -1384,25 +1384,11 @@ option_env_fd_cb (const gchar *option_name, + FlatpakContext *context = data; + glnx_autofd int fd = -1; + +- fd = flatpak_parse_fd (value, error); ++ fd = flatpak_accept_fd_argument (option_name, value, error); ++ + if (fd < 0) + return FALSE; + +- if (fd < 3) +- { +- /* Don't close these fds! */ +- fd = -1; +- return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); +- } +- +- /* This is not strictly necessary, because we're going to close it after +- * parsing the environment block, but let's be consistent with other fd +- * arguments that we need to avoid being inherited by the "payload" +- * command. This is also a convenient place to validate that it's an +- * open fd. */ +- if (!flatpak_set_cloexec (fd)) +- return glnx_throw_errno_prefix (error, "--env-fd"); +- + return flatpak_context_parse_env_fd (context, fd, error); + } + +diff --git a/common/flatpak-utils-private.h b/common/flatpak-utils-private.h +index 8cad6672..75538399 100644 +--- a/common/flatpak-utils-private.h ++++ b/common/flatpak-utils-private.h +@@ -358,4 +358,8 @@ char * flatpak_get_path_for_fd (int fd, + + gboolean flatpak_set_cloexec (int fd); + ++int flatpak_accept_fd_argument (const char *option_name, ++ const char *value, ++ GError **error); ++ + #endif /* __FLATPAK_UTILS_H__ */ +diff --git a/common/flatpak-utils.c b/common/flatpak-utils.c +index b8bfcfdb..09102fd8 100644 +--- a/common/flatpak-utils.c ++++ b/common/flatpak-utils.c +@@ -2543,6 +2543,53 @@ flatpak_set_cloexec (int fd) + return TRUE; + } + ++/* ++ * flatpak_accept_fd_argument: ++ * @option_name: Name of a command-line option such as `--env-fd` ++ * @value: Value of the command-line option ++ * ++ * Parse a command-line argument whose value is a file descriptor to be ++ * used internally by Flatpak. ++ * ++ * The file descriptor must be 3 or higher (cannot be stdin, stdout ++ * or stderr). ++ * ++ * The file descriptor is set to be close-on-execute (CLOEXEC). ++ * If child processes are meant to inherit it, the caller must clear the ++ * close-on-execute flag, or duplicate the fd. ++ * ++ * Returns: A file descriptor to be closed by the caller, or -1 on error ++ */ ++int ++flatpak_accept_fd_argument (const char *option_name, ++ const char *value, ++ GError **error) ++{ ++ glnx_autofd int fd = -1; ++ ++ fd = flatpak_parse_fd (value, error); ++ ++ if (fd < 0) ++ { ++ g_prefix_error (error, "%s: ", option_name); ++ return -1; ++ } ++ ++ if (fd < 3) ++ { ++ /* We don't want to close stdin, stdout or stderr */ ++ fd = -1; ++ return glnx_fd_throw (error, ++ "%s: Cannot use reserved file descriptor 0, 1 or 2", ++ option_name); ++ } ++ ++ if (!flatpak_set_cloexec (fd)) ++ return glnx_fd_throw_errno_prefix (error, "%s", option_name); ++ ++ return g_steal_fd (&fd); ++} ++ + /* + * Attempt to discover the filesystem path corresponding to @fd. + * +-- +2.54.0 + diff --git a/CVE-2026-34078-4-flatpak-bwrap-use-glnx-close-fd-as-clear-func.patch b/CVE-2026-34078-4-flatpak-bwrap-use-glnx-close-fd-as-clear-func.patch new file mode 100644 index 0000000..def0959 --- /dev/null +++ b/CVE-2026-34078-4-flatpak-bwrap-use-glnx-close-fd-as-clear-func.patch @@ -0,0 +1,48 @@ +From 5a7e677294c92c04ea8c7c09a2b1abd2988f03df Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Fri, 6 Feb 2026 16:32:50 +0100 +Subject: [PATCH] flatpak-bwrap: Use glnx_close_fd as clear func + +We already have a function which clears a fd that a pointer points to, +so let's use it instead of duplicating the code. + +Will become useful in a later commit as well. +--- + common/flatpak-bwrap.c | 13 ++----------- + 1 file changed, 2 insertions(+), 11 deletions(-) + +diff --git a/common/flatpak-bwrap.c b/common/flatpak-bwrap.c +index a3d29782..a0eddfd4 100644 +--- a/common/flatpak-bwrap.c ++++ b/common/flatpak-bwrap.c +@@ -42,15 +42,6 @@ + #include "flatpak-utils-private.h" + #include "flatpak-utils-base-private.h" + +-static void +-clear_fd (gpointer data) +-{ +- int *fd_p = data; +- +- if (fd_p != NULL && *fd_p != -1) +- close (*fd_p); +-} +- + char *flatpak_bwrap_empty_env[] = { NULL }; + + FlatpakBwrap * +@@ -60,9 +51,9 @@ flatpak_bwrap_new (char **env) + + bwrap->argv = g_ptr_array_new_with_free_func (g_free); + bwrap->noinherit_fds = g_array_new (FALSE, TRUE, sizeof (int)); +- g_array_set_clear_func (bwrap->noinherit_fds, clear_fd); ++ g_array_set_clear_func (bwrap->noinherit_fds, (GDestroyNotify) glnx_close_fd); + bwrap->fds = g_array_new (FALSE, TRUE, sizeof (int)); +- g_array_set_clear_func (bwrap->fds, clear_fd); ++ g_array_set_clear_func (bwrap->fds, (GDestroyNotify) glnx_close_fd); + + if (env) + bwrap->envp = g_strdupv (env); +-- +2.54.0 + diff --git a/CVE-2026-34078-5-run-use-o-path-fds-for-the-runtime-and-app-deploy-directories.patch b/CVE-2026-34078-5-run-use-o-path-fds-for-the-runtime-and-app-deploy-directories.patch new file mode 100644 index 0000000..9e61176 --- /dev/null +++ b/CVE-2026-34078-5-run-use-o-path-fds-for-the-runtime-and-app-deploy-directories.patch @@ -0,0 +1,915 @@ +From 9c9fe3fdf3920428822cacbb249fd75e6d8dbe48 Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Fri, 6 Feb 2026 20:54:22 +0100 +Subject: [PATCH] run: Use O_PATH fds for the runtime and app deploy + directories + +This also allows us to use glnx_chaseat, and other at-functions to +traverse the filesystem tree in a safe way. + +This is important because the app and runtime deploy directories can be +under an attackers control. The flatpak portal for example allows +sandboxed apps to provide them. + +In particular, attacks where the deploy dirs get replaced by a symlink +pointing into the host system will be stopped by this. + +Note that this change alone is not enough to avoid the attack, and the +portal has to be changed as well. +--- + app/flatpak-builtins-build.c | 8 +- + app/flatpak-builtins-run.c | 37 ++- + common/flatpak-dir.c | 8 +- + common/flatpak-installation.c | 3 +- + common/flatpak-run-private.h | 11 +- + common/flatpak-run.c | 471 +++++++++++++++++++++++----------- + 6 files changed, 382 insertions(+), 156 deletions(-) + +diff --git a/app/flatpak-builtins-build.c b/app/flatpak-builtins-build.c +index 585f8f4..8625b36 100644 +--- a/app/flatpak-builtins-build.c ++++ b/app/flatpak-builtins-build.c +@@ -458,7 +458,13 @@ flatpak_builtin_build (int argc, char **argv, GCancellable *cancellable, GError + /* Never set up an a11y bus for builds */ + run_flags |= FLATPAK_RUN_FLAG_NO_A11Y_BUS_PROXY; + +- if (!flatpak_run_setup_base_argv (bwrap, runtime_files, app_id_dir, arch, ++ glnx_autofd int usr_fd = -1; ++ usr_fd = open (flatpak_file_get_path_cached (runtime_files), ++ O_PATH | O_CLOEXEC | O_NOFOLLOW); ++ if (usr_fd < 0) ++ return glnx_throw_errno_prefix (error, "Failed to open runtime files"); ++ ++ if (!flatpak_run_setup_base_argv (bwrap, usr_fd, app_id_dir, arch, + run_flags, error)) + return FALSE; + +diff --git a/app/flatpak-builtins-run.c b/app/flatpak-builtins-run.c +index d0a1fb0..aebc345 100644 +--- a/app/flatpak-builtins-run.c ++++ b/app/flatpak-builtins-run.c +@@ -111,6 +111,8 @@ flatpak_builtin_run (int argc, char **argv, GCancellable *cancellable, GError ** + g_autoptr(GError) local_error = NULL; + g_autoptr(GPtrArray) dirs = NULL; + FlatpakRunFlags flags = 0; ++ glnx_autofd int app_fd = -1; ++ glnx_autofd int usr_fd = -1; + + run_environ = g_get_environ (); + +@@ -309,14 +311,45 @@ flatpak_builtin_run (int argc, char **argv, GCancellable *cancellable, GError ** + if (!opt_session_bus) + flags |= FLATPAK_RUN_FLAG_NO_SESSION_BUS_PROXY; + ++ if (opt_app_path != NULL) ++ { ++ if (g_strcmp0 (opt_app_path, "") == 0) ++ { ++ app_fd = FLATPAK_RUN_APP_DEPLOY_APP_EMPTY; ++ } ++ else ++ { ++ app_fd = open (opt_app_path, O_PATH | O_CLOEXEC | O_NOFOLLOW); ++ ++ if (app_fd < 0) ++ return glnx_throw_errno_prefix (error, "Failed to open app-path"); ++ } ++ } ++ else ++ { ++ app_fd = FLATPAK_RUN_APP_DEPLOY_APP_ORIGINAL; ++ } ++ ++ if (opt_usr_path != NULL) ++ { ++ usr_fd = open (opt_usr_path, O_PATH | O_CLOEXEC | O_NOFOLLOW); ++ ++ if (usr_fd < 0) ++ return glnx_throw_errno_prefix (error, "Failed to open usr-path"); ++ } ++ else ++ { ++ usr_fd = FLATPAK_RUN_APP_DEPLOY_USR_ORIGINAL; ++ } ++ + if (!flatpak_run_app (app_deploy ? app_ref : runtime_ref, + app_deploy, +- opt_app_path, ++ app_fd, + arg_context, + opt_runtime, + opt_runtime_version, + opt_runtime_commit, +- opt_usr_path, ++ usr_fd, + opt_parent_pid, + flags, + opt_cwd, +diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c +index 6936d45..a1d71a2 100644 +--- a/common/flatpak-dir.c ++++ b/common/flatpak-dir.c +@@ -8468,7 +8468,13 @@ apply_extra_data (FlatpakDir *self, + * Disable /proc entirely in this context. */ + run_flags |= FLATPAK_RUN_FLAG_NO_PROC; + +- if (!flatpak_run_setup_base_argv (bwrap, runtime_files, NULL, runtime_arch, ++ glnx_autofd int usr_fd = -1; ++ usr_fd = open (flatpak_file_get_path_cached (runtime_files), ++ O_PATH | O_CLOEXEC | O_NOFOLLOW); ++ if (usr_fd < 0) ++ return glnx_throw_errno_prefix (error, "Failed to open runtime files"); ++ ++ if (!flatpak_run_setup_base_argv (bwrap, usr_fd, NULL, runtime_arch, + run_flags, error)) + return FALSE; + +diff --git a/common/flatpak-installation.c b/common/flatpak-installation.c +index c7d2e1d..00c023f 100644 +--- a/common/flatpak-installation.c ++++ b/common/flatpak-installation.c +@@ -703,9 +703,10 @@ flatpak_installation_launch_full (FlatpakInstallation *self, + + if (!flatpak_run_app (app_ref, + app_deploy, ++ FLATPAK_RUN_APP_DEPLOY_APP_ORIGINAL, + NULL, +- NULL, NULL, + NULL, NULL, NULL, ++ FLATPAK_RUN_APP_DEPLOY_USR_ORIGINAL, + 0, + run_flags, + NULL, +diff --git a/common/flatpak-run-private.h b/common/flatpak-run-private.h +index 40899e2..4c6fe16 100644 +--- a/common/flatpak-run-private.h ++++ b/common/flatpak-run-private.h +@@ -30,6 +30,11 @@ + #include "flatpak-utils-private.h" + #include "flatpak-exports-private.h" + ++#define FLATPAK_RUN_APP_DEPLOY_APP_ORIGINAL (-2) ++#define FLATPAK_RUN_APP_DEPLOY_APP_EMPTY (-3) ++ ++#define FLATPAK_RUN_APP_DEPLOY_USR_ORIGINAL (-2) ++ + gboolean flatpak_run_in_transient_unit (const char *app_id, + GError **error); + +@@ -71,7 +76,7 @@ gboolean flatpak_ensure_data_dir (GFile *app_id_dir, + GError **error); + + gboolean flatpak_run_setup_base_argv (FlatpakBwrap *bwrap, +- GFile *runtime_files, ++ int runtime_fd, + GFile *app_id_dir, + const char *arch, + FlatpakRunFlags flags, +@@ -103,12 +108,12 @@ gboolean flatpak_run_add_app_info_args (FlatpakBwrap *bwrap, + + gboolean flatpak_run_app (FlatpakDecomposed *app_ref, + FlatpakDeploy *app_deploy, +- const char *custom_app_path, ++ int custom_app_fd, + FlatpakContext *extra_context, + const char *custom_runtime, + const char *custom_runtime_version, + const char *custom_runtime_commit, +- const char *custom_usr_path, ++ int custom_usr_fd, + int parent_pid, + FlatpakRunFlags flags, + const char *cwd, +diff --git a/common/flatpak-run.c b/common/flatpak-run.c +index 8c92924..3a2028f 100644 +--- a/common/flatpak-run.c ++++ b/common/flatpak-run.c +@@ -1609,40 +1609,56 @@ flatpak_run_add_app_info_args (FlatpakBwrap *bwrap, + + static void + add_tzdata_args (FlatpakBwrap *bwrap, +- GFile *runtime_files) ++ int runtime_fd) + { +- g_autofree char *raw_timezone = flatpak_get_timezone (); +- g_autofree char *timezone_content = g_strdup_printf ("%s\n", raw_timezone); +- g_autofree char *localtime_content = g_strconcat ("../usr/share/zoneinfo/", raw_timezone, NULL); +- g_autoptr(GFile) runtime_zoneinfo = NULL; ++ g_autofree char *raw_timezone = NULL; ++ g_autofree char *timezone_content = NULL; ++ g_autofree char *localtime_content = NULL; ++ const char *tzdir; ++ glnx_autofd int tzdir_fd = -1; ++ glnx_autofd int zoneinfo_fd = -1; ++ g_autoptr(GError) error = NULL; + +- if (runtime_files) +- runtime_zoneinfo = g_file_resolve_relative_path (runtime_files, "share/zoneinfo"); ++ raw_timezone = flatpak_get_timezone (); ++ timezone_content = g_strdup_printf ("%s\n", raw_timezone); ++ localtime_content = g_strconcat ("../usr/share/zoneinfo/", raw_timezone, NULL); + +- /* Check for runtime /usr/share/zoneinfo */ +- if (runtime_zoneinfo != NULL && g_file_query_exists (runtime_zoneinfo, NULL)) ++ tzdir = flatpak_get_tzdir (); ++ ++ tzdir_fd = glnx_chaseat (AT_FDCWD, tzdir, GLNX_CHASE_MUST_BE_DIRECTORY, NULL); ++ ++ zoneinfo_fd = glnx_chaseat (runtime_fd, "share/zoneinfo", ++ GLNX_CHASE_RESOLVE_BENEATH | ++ GLNX_CHASE_MUST_BE_DIRECTORY, ++ NULL); ++ ++ /* Check for host /usr/share/zoneinfo */ ++ if (tzdir_fd >= 0 && zoneinfo_fd >= 0) ++ { ++ /* Here we assume the host timezone file exist in the host data */ ++ flatpak_bwrap_add_args (bwrap, ++ "--ro-bind", tzdir, "/usr/share/zoneinfo", ++ "--symlink", localtime_content, "/etc/localtime", ++ NULL); ++ } ++ else + { +- const char *tzdir = flatpak_get_tzdir (); ++ g_autofree char *runtime_zoneinfo = NULL; ++ glnx_autofd int runtime_zoneinfo_fd = -1; + +- /* Check for host /usr/share/zoneinfo */ +- if (g_file_test (tzdir, G_FILE_TEST_IS_DIR)) ++ runtime_zoneinfo = g_strconcat ("share/zoneinfo/", raw_timezone, NULL); ++ ++ /* Check for runtime /usr/share/zoneinfo */ ++ runtime_zoneinfo_fd = glnx_chaseat (runtime_fd, runtime_zoneinfo, ++ GLNX_CHASE_RESOLVE_BENEATH | ++ GLNX_CHASE_MUST_BE_REGULAR, ++ NULL); ++ if (runtime_zoneinfo_fd >= 0) + { +- /* Here we assume the host timezone file exist in the host data */ + flatpak_bwrap_add_args (bwrap, +- "--ro-bind", tzdir, "/usr/share/zoneinfo", + "--symlink", localtime_content, "/etc/localtime", + NULL); + } +- else +- { +- g_autoptr(GFile) runtime_tzfile = g_file_resolve_relative_path (runtime_zoneinfo, raw_timezone); +- +- /* Check if host timezone file exist in the runtime tzdata */ +- if (g_file_query_exists (runtime_tzfile, NULL)) +- flatpak_bwrap_add_args (bwrap, +- "--symlink", localtime_content, "/etc/localtime", +- NULL); +- } + } + + flatpak_bwrap_add_args_data (bwrap, "timezone", +@@ -2138,24 +2154,41 @@ setup_seccomp (FlatpakBwrap *bwrap, + + static void + flatpak_run_setup_usr_links (FlatpakBwrap *bwrap, +- GFile *runtime_files, ++ int runtime_fd, + const char *sysroot) + { + int i; + +- if (runtime_files == NULL) ++ g_return_if_fail (runtime_fd >= -1); ++ ++ if (runtime_fd < 0) + return; + + for (i = 0; flatpak_abs_usrmerged_dirs[i] != NULL; i++) + { + const char *subdir = flatpak_abs_usrmerged_dirs[i]; +- g_autoptr(GFile) runtime_subdir = NULL; ++ glnx_autofd int runtime_subdir_fd = -1; ++ g_autoptr(GError) local_error = NULL; + + g_assert (subdir[0] == '/'); ++ + /* Skip the '/' when using as a subdirectory of the runtime */ +- runtime_subdir = g_file_get_child (runtime_files, subdir + 1); ++ runtime_subdir_fd = glnx_chaseat (runtime_fd, subdir + 1, ++ GLNX_CHASE_RESOLVE_BENEATH | ++ GLNX_CHASE_NOFOLLOW, ++ &local_error); + +- if (g_file_query_exists (runtime_subdir, NULL)) ++ if (runtime_subdir_fd < 0 && ++ !g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) ++ { ++ g_warning ("Checking for usrmerged dir %s failed: %s", ++ subdir, local_error->message); ++ } ++ else if (runtime_subdir_fd < 0) ++ { ++ g_info ("%s does not exist in runtime", subdir); ++ } ++ else + { + g_autofree char *link = g_strconcat ("usr", subdir, NULL); + g_autofree char *create = NULL; +@@ -2169,11 +2202,6 @@ flatpak_run_setup_usr_links (FlatpakBwrap *bwrap, + "--symlink", link, create, + NULL); + } +- else +- { +- g_info ("%s does not exist", +- flatpak_file_get_path_cached (runtime_subdir)); +- } + } + } + +@@ -2189,7 +2217,7 @@ static const char *const sysfs_dirs[] = + + gboolean + flatpak_run_setup_base_argv (FlatpakBwrap *bwrap, +- GFile *runtime_files, ++ int runtime_fd, + GFile *app_id_dir, + const char *arch, + FlatpakRunFlags flags, +@@ -2202,12 +2230,13 @@ flatpak_run_setup_base_argv (FlatpakBwrap *bwrap, + struct group *g; + gulong pers; + gid_t gid = getgid (); +- g_autoptr(GFile) etc = NULL; + gboolean parent_expose_pids = (flags & FLATPAK_RUN_FLAG_PARENT_EXPOSE_PIDS) != 0; + gboolean parent_share_pids = (flags & FLATPAK_RUN_FLAG_PARENT_SHARE_PIDS) != 0; + gboolean bwrap_unprivileged = flatpak_bwrap_is_unprivileged (); + gsize i; + ++ g_return_val_if_fail (runtime_fd >= 0, FALSE); ++ + /* Disable recursive userns for all flatpak processes, as we need this + * to guarantee that the sandbox can't restructure the filesystem. + * Allowing to change e.g. /.flatpak-info would allow sandbox escape +@@ -2315,22 +2344,25 @@ flatpak_run_setup_base_argv (FlatpakBwrap *bwrap, + else if (g_file_test ("/var/lib/dbus/machine-id", G_FILE_TEST_EXISTS)) + flatpak_bwrap_add_args (bwrap, "--ro-bind", "/var/lib/dbus/machine-id", "/etc/machine-id", NULL); + +- if (runtime_files) +- etc = g_file_get_child (runtime_files, "etc"); +- if (etc != NULL && +- (flags & FLATPAK_RUN_FLAG_WRITABLE_ETC) == 0 && +- g_file_query_exists (etc, NULL)) ++ if ((flags & FLATPAK_RUN_FLAG_WRITABLE_ETC) == 0) + { + g_auto(GLnxDirFdIterator) dfd_iter = { 0, }; + struct dirent *dent; + gboolean inited; ++ g_autoptr(GError) local_error = NULL; + +- inited = glnx_dirfd_iterator_init_at (AT_FDCWD, flatpak_file_get_path_cached (etc), FALSE, &dfd_iter, NULL); ++ inited = glnx_dirfd_iterator_init_at (runtime_fd, "etc", FALSE, &dfd_iter, &local_error); ++ if (!inited && !g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) ++ { ++ g_propagate_error (error, g_steal_pointer (&local_error)); ++ return FALSE; ++ } + + while (inited) + { +- g_autofree char *src = NULL; + g_autofree char *dest = NULL; ++ glnx_autofd int src_fd = -1; ++ struct stat statbuf; + + if (!glnx_dirfd_iterator_next_dent_ensure_dtype (&dfd_iter, &dent, NULL, NULL) || dent == NULL) + break; +@@ -2347,9 +2379,19 @@ flatpak_run_setup_base_argv (FlatpakBwrap *bwrap, + strcmp (dent->d_name, "pkcs11") == 0) + continue; + +- src = g_build_filename (flatpak_file_get_path_cached (etc), dent->d_name, NULL); + dest = g_build_filename ("/etc", dent->d_name, NULL); +- if (dent->d_type == DT_LNK) ++ ++ src_fd = glnx_chaseat (dfd_iter.fd, dent->d_name, ++ GLNX_CHASE_NOFOLLOW | ++ GLNX_CHASE_RESOLVE_BENEATH, ++ error); ++ if (src_fd < 0) ++ return FALSE; ++ ++ if (!glnx_fstat (src_fd, &statbuf, error)) ++ return FALSE; ++ ++ if (S_ISLNK (statbuf.st_mode)) + { + g_autofree char *target = NULL; + +@@ -2360,9 +2402,12 @@ flatpak_run_setup_base_argv (FlatpakBwrap *bwrap, + + flatpak_bwrap_add_args (bwrap, "--symlink", target, dest, NULL); + } +- else ++ else if (src_fd >= 0) + { +- flatpak_bwrap_add_args (bwrap, "--ro-bind", src, dest, NULL); ++ flatpak_bwrap_add_args_data_fd (bwrap, ++ "--ro-bind-fd", ++ g_steal_fd (&src_fd), ++ dest); + } + } + } +@@ -2383,9 +2428,9 @@ flatpak_run_setup_base_argv (FlatpakBwrap *bwrap, + NULL); + } + +- flatpak_run_setup_usr_links (bwrap, runtime_files, NULL); ++ flatpak_run_setup_usr_links (bwrap, runtime_fd, NULL); + +- add_tzdata_args (bwrap, runtime_files); ++ add_tzdata_args (bwrap, runtime_fd); + + pers = PER_LINUX; + +@@ -2612,7 +2657,7 @@ regenerate_ld_cache (GPtrArray *base_argv_array, + GArray *base_fd_array, + GFile *app_id_dir, + const char *checksum, +- GFile *runtime_files, ++ int runtime_fd, + gboolean generate_ld_so_conf, + GCancellable *cancellable, + GError **error) +@@ -2652,7 +2697,7 @@ regenerate_ld_cache (GPtrArray *base_argv_array, + + flatpak_bwrap_append_args (bwrap, base_argv_array); + +- flatpak_run_setup_usr_links (bwrap, runtime_files, NULL); ++ flatpak_run_setup_usr_links (bwrap, runtime_fd, NULL); + + if (generate_ld_so_conf) + { +@@ -2867,15 +2912,51 @@ open_namespace_fd_if_needed (const char *path, + return -1; + } + ++static char * ++get_path_for_fd (int fd, ++ GError **error) ++{ ++ g_autofree char *proc_path = NULL; ++ g_autofree char *path = NULL; ++ ++ proc_path = g_strdup_printf ("/proc/self/fd/%d", fd); ++ path = glnx_readlinkat_malloc (AT_FDCWD, proc_path, NULL, error); ++ if (path == NULL) ++ return NULL; ++ ++ /* All normal paths start with /, but some weird things ++ don't, such as socket:[27345] or anon_inode:[eventfd]. ++ We don't support any of these */ ++ if (path[0] != '/') ++ { ++ return glnx_null_throw (error, "%s resolves to non-absolute path %s", ++ proc_path, path); ++ } ++ ++ /* File descriptors to actually deleted files have " (deleted)" ++ appended to them. This also happens to some fake fd types ++ like shmem which are "/ (deleted)". All such ++ files are considered invalid. Unfortunately this also ++ matches files with filenames that actually end in " (deleted)", ++ but there is not much to do about this. */ ++ if (g_str_has_suffix (path, " (deleted)")) ++ { ++ return glnx_null_throw (error, "%s resolves to deleted path %s", ++ proc_path, path); ++ } ++ ++ return g_steal_pointer (&path); ++} ++ + gboolean + flatpak_run_app (FlatpakDecomposed *app_ref, + FlatpakDeploy *app_deploy, +- const char *custom_app_path, ++ int custom_app_fd, + FlatpakContext *extra_context, + const char *custom_runtime, + const char *custom_runtime_version, + const char *custom_runtime_commit, +- const char *custom_usr_path, ++ int custom_runtime_fd, + int parent_pid, + FlatpakRunFlags flags, + const char *cwd, +@@ -2891,11 +2972,6 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + g_autoptr(FlatpakDeploy) runtime_deploy = NULL; + g_autoptr(GBytes) runtime_deploy_data = NULL; + g_autoptr(GBytes) app_deploy_data = NULL; +- g_autoptr(GFile) app_files = NULL; +- g_autoptr(GFile) original_app_files = NULL; +- g_autoptr(GFile) runtime_files = NULL; +- g_autoptr(GFile) original_runtime_files = NULL; +- g_autoptr(GFile) bin_ldconfig = NULL; + g_autoptr(GFile) app_id_dir = NULL; + g_autoptr(GFile) real_app_id_dir = NULL; + g_autofree char *default_runtime_pref = NULL; +@@ -2929,20 +3005,41 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + g_autofree char *per_app_dir_lock_path = NULL; + g_autofree char *shared_xdg_runtime_dir = NULL; + int ld_so_fd = -1; +- g_autoptr(GFile) runtime_ld_so_conf = NULL; + gboolean generate_ld_so_conf = TRUE; + gboolean use_ld_so_cache = TRUE; + gboolean sandboxed = (flags & FLATPAK_RUN_FLAG_SANDBOX) != 0; + gboolean parent_expose_pids = (flags & FLATPAK_RUN_FLAG_PARENT_EXPOSE_PIDS) != 0; + gboolean parent_share_pids = (flags & FLATPAK_RUN_FLAG_PARENT_SHARE_PIDS) != 0; +- const char *app_target_path = "/app"; +- const char *runtime_target_path = "/usr"; +- struct stat s; ++ glnx_autofd int original_runtime_fd = -1; ++ g_autoptr(GFile) original_runtime_files = NULL; ++ g_autoptr(GFile) custom_runtime_files = NULL; ++ /* borrows from either original_runtime_fd or custom_runtime_fd */ ++ int runtime_fd = -1; ++ /* borrows from either original_runtime_files or custom_runtime_files */ ++ GFile *runtime_files = NULL; ++ const char *original_runtime_target_path = NULL; ++ glnx_autofd int original_app_fd = -1; ++ g_autoptr(GFile) original_app_files = NULL; ++ g_autoptr(GFile) custom_app_files = NULL; ++ /* borrows from either original_app_fd or custom_app_fd */ ++ int app_fd = -1; ++ /* borrows from either original_app_files or custom_app_files */ ++ GFile *app_files = NULL; ++ const char *original_app_target_path = NULL; + + g_assert (run_environ != NULL); + + g_return_val_if_fail (app_ref != NULL, FALSE); + ++ g_return_val_if_fail (custom_app_fd == FLATPAK_RUN_APP_DEPLOY_APP_ORIGINAL || ++ custom_app_fd == FLATPAK_RUN_APP_DEPLOY_APP_EMPTY || ++ custom_app_fd >= 0, ++ FALSE); ++ ++ g_return_val_if_fail (custom_runtime_fd == FLATPAK_RUN_APP_DEPLOY_USR_ORIGINAL || ++ custom_runtime_fd >= 0, ++ FALSE); ++ + /* This check exists to stop accidental usage of `sudo flatpak run` + and is not to prevent running as root. + */ +@@ -3068,38 +3165,53 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + + flatpak_context_dump (app_context, "Final context"); + original_runtime_files = flatpak_deploy_get_files (runtime_deploy); ++ original_runtime_fd = open (flatpak_file_get_path_cached (original_runtime_files), ++ O_PATH | O_CLOEXEC); ++ if (original_runtime_fd < 0) ++ return glnx_throw_errno_prefix (error, "Failed to open original runtime"); + +- if (custom_usr_path != NULL) ++ if (custom_runtime_fd >= 0) + { +- runtime_files = g_file_new_for_path (custom_usr_path); +- /* Mount the original runtime below here instead of /usr */ +- runtime_target_path = "/run/parent/usr"; ++ g_autofree char *path = NULL; ++ ++ path = get_path_for_fd (custom_runtime_fd, &my_error); ++ if (path == NULL) ++ { ++ return flatpak_fail_error (error, FLATPAK_ERROR, ++ "Cannot convert custom usr fd to path: %s", ++ my_error->message); ++ } ++ ++ custom_runtime_files = g_file_new_for_path (path); ++ ++ original_runtime_target_path = "/run/parent/usr"; ++ runtime_fd = custom_runtime_fd; ++ runtime_files = custom_runtime_files; ++ } ++ else if (custom_app_fd == FLATPAK_RUN_APP_DEPLOY_USR_ORIGINAL) ++ { ++ original_runtime_target_path = "/usr"; ++ runtime_fd = original_runtime_fd; ++ runtime_files = original_runtime_files; + } + else + { +- runtime_files = g_object_ref (original_runtime_files); ++ g_assert_not_reached (); + } + +- bin_ldconfig = g_file_resolve_relative_path (runtime_files, "bin/ldconfig"); +- if (!g_file_query_exists (bin_ldconfig, NULL)) +- use_ld_so_cache = FALSE; +- +- /* We can't use the ld.so cache if we are using a custom /usr or /app, +- * because we don't have a unique ID for the /usr or /app, so we can't +- * do cache-invalidation correctly. The caller can either build their +- * own ld.so.cache before supplying us with the runtime, or supply +- * their own LD_LIBRARY_PATH. */ +- if (custom_usr_path != NULL || custom_app_path != NULL) +- use_ld_so_cache = FALSE; +- + if (app_deploy != NULL) + { + g_autofree const char **previous_ids = NULL; + gsize len = 0; + gboolean do_migrate; + +- real_app_id_dir = flatpak_get_data_dir (app_id); + original_app_files = flatpak_deploy_get_files (app_deploy); ++ original_app_fd = open (flatpak_file_get_path_cached (original_app_files), ++ O_PATH | O_CLOEXEC | O_NOFOLLOW); ++ if (original_app_fd < 0) ++ return glnx_throw_errno_prefix (error, "Failed to open original runtime"); ++ ++ real_app_id_dir = flatpak_get_data_dir (app_id); + + previous_app_id_dirs = g_ptr_array_new_with_free_func (g_object_unref); + previous_ids = flatpak_deploy_data_get_previous_ids (app_deploy_data, &len); +@@ -3186,19 +3298,60 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + app_id_dir = g_object_ref (real_app_id_dir); + } + +- if (custom_app_path != NULL) ++ if (custom_app_fd >= 0) + { +- if (strcmp (custom_app_path, "") == 0) +- app_files = NULL; +- else +- app_files = g_file_new_for_path (custom_app_path); ++ g_autofree char *path = NULL; ++ ++ path = get_path_for_fd (custom_app_fd, error); ++ if (path == NULL) ++ return glnx_prefix_error (error, "Cannot convert custom app fd to path"); ++ ++ custom_app_files = g_file_new_for_path (path); ++ ++ original_app_target_path = "/run/parent/app"; ++ app_fd = custom_app_fd; ++ app_files = custom_app_files; ++ } ++ else if (custom_app_fd == FLATPAK_RUN_APP_DEPLOY_APP_ORIGINAL) ++ { ++ original_app_target_path = "/app"; ++ app_fd = original_app_fd; ++ app_files = original_app_files; ++ } ++ else if (custom_app_fd == FLATPAK_RUN_APP_DEPLOY_APP_EMPTY) ++ { ++ app_fd = -1; ++ app_files = NULL; ++ } ++ else ++ { ++ g_assert_not_reached (); ++ } + +- /* Mount the original app below here */ +- app_target_path = "/run/parent/app"; ++ /* We can't use the ld.so cache if we are using a custom /usr or /app, ++ * because we don't have a unique ID for the /usr or /app, so we can't ++ * do cache-invalidation correctly. The caller can either build their ++ * own ld.so.cache before supplying us with the runtime, or supply ++ * their own LD_LIBRARY_PATH. */ ++ if (runtime_fd == custom_runtime_fd || app_fd == custom_app_fd) ++ { ++ use_ld_so_cache = FALSE; + } +- else if (original_app_files != NULL) ++ else + { +- app_files = g_object_ref (original_app_files); ++ glnx_autofd int ldconfig_fd = -1; ++ ++ ldconfig_fd = glnx_chaseat (runtime_fd, "bin/ldconfig", ++ GLNX_CHASE_RESOLVE_BENEATH | ++ GLNX_CHASE_MUST_BE_REGULAR, ++ &my_error); ++ if (ldconfig_fd < 0) ++ { ++ use_ld_so_cache = FALSE; ++ g_debug ("bin/ldconfig not found in runtime: %s", my_error->message); ++ } ++ ++ g_clear_error (&my_error); + } + + flatpak_run_apply_env_default (bwrap, use_ld_so_cache); +@@ -3211,75 +3364,86 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + flatpak_bwrap_set_env (bwrap, "FLATPAK_SANDBOX_DIR", flatpak_file_get_path_cached (sandbox_dir), TRUE); + } + +- flatpak_bwrap_add_args (bwrap, +- "--ro-bind", flatpak_file_get_path_cached (runtime_files), "/usr", +- NULL); +- +- if (runtime_files == original_runtime_files) +- { +- /* All true Flatpak runtimes have files/.ref */ +- flatpak_bwrap_add_args (bwrap, +- "--lock-file", "/usr/.ref", +- NULL); +- } +- else +- { +- g_autoptr(GFile) runtime_child = NULL; ++ if (!flatpak_bwrap_add_args_data_fd_dup (bwrap, ++ "--ro-bind-fd", runtime_fd, "/usr", ++ error)) ++ return FALSE; + +- runtime_child = g_file_get_child (runtime_files, ".ref"); ++ { ++ glnx_autofd int runtime_ref_fd = -1; + +- /* Lock ${usr}/.ref if it exists */ +- if (g_file_query_exists (runtime_child, NULL)) ++ runtime_ref_fd = glnx_chaseat (runtime_fd, ".ref", ++ GLNX_CHASE_RESOLVE_BENEATH | ++ GLNX_CHASE_MUST_BE_REGULAR, ++ NULL); ++ if (runtime_ref_fd >= 0) ++ { + flatpak_bwrap_add_args (bwrap, + "--lock-file", "/usr/.ref", + NULL); ++ } ++ } ++ ++ if (runtime_fd == custom_runtime_fd) ++ { ++ glnx_autofd int original_runtime_ref_fd = -1; ++ glnx_autofd int original_runtime_etc_fd = -1; + + /* Put the real Flatpak runtime in /run/parent, so that the + * replacement /usr can have symlinks into /run/parent in order + * to use the Flatpak runtime's graphics drivers etc. if desired */ +- flatpak_bwrap_add_args (bwrap, +- "--ro-bind", +- flatpak_file_get_path_cached (original_runtime_files), +- "/run/parent/usr", +- "--lock-file", "/run/parent/usr/.ref", +- NULL); +- flatpak_run_setup_usr_links (bwrap, original_runtime_files, +- "/run/parent"); ++ if (!flatpak_bwrap_add_args_data_fd_dup (bwrap, ++ "--ro-bind-fd", ++ original_runtime_fd, ++ "/run/parent/usr", ++ error)) ++ return FALSE; + +- g_clear_object (&runtime_child); +- runtime_child = g_file_get_child (original_runtime_files, "etc"); ++ original_runtime_ref_fd = glnx_chaseat (original_runtime_fd, ".ref", ++ GLNX_CHASE_RESOLVE_BENEATH | ++ GLNX_CHASE_MUST_BE_REGULAR, ++ NULL); ++ if (original_runtime_ref_fd >= 0) ++ { ++ flatpak_bwrap_add_args (bwrap, ++ "--lock-file", "/run/parent/usr/.ref", ++ NULL); ++ } + +- if (g_file_query_exists (runtime_child, NULL)) +- flatpak_bwrap_add_args (bwrap, +- "--symlink", "usr/etc", "/run/parent/etc", +- NULL); ++ original_runtime_etc_fd = glnx_chaseat (original_runtime_fd, "etc", ++ GLNX_CHASE_RESOLVE_BENEATH | ++ GLNX_CHASE_MUST_BE_REGULAR, ++ NULL); ++ if (original_runtime_etc_fd >= 0) ++ { ++ flatpak_bwrap_add_args (bwrap, ++ "--symlink", "usr/etc", "/run/parent/etc", ++ NULL); ++ } ++ ++ flatpak_run_setup_usr_links (bwrap, original_runtime_fd, ++ "/run/parent"); + } + +- if (app_files != NULL) ++ if (app_fd >= 0) + { +- flatpak_bwrap_add_args (bwrap, +- "--ro-bind", flatpak_file_get_path_cached (app_files), "/app", +- NULL); ++ glnx_autofd int app_ref_fd = -1; + +- if (app_files == original_app_files) ++ if (!flatpak_bwrap_add_args_data_fd_dup (bwrap, ++ "--ro-bind-fd", app_fd, "/app", ++ error)) ++ return FALSE; ++ ++ app_ref_fd = glnx_chaseat (app_fd, ".ref", ++ GLNX_CHASE_RESOLVE_BENEATH | ++ GLNX_CHASE_MUST_BE_REGULAR, ++ NULL); ++ if (app_ref_fd >= 0) + { +- /* All true Flatpak apps have files/.ref */ + flatpak_bwrap_add_args (bwrap, + "--lock-file", "/app/.ref", + NULL); + } +- else +- { +- g_autoptr(GFile) app_child = NULL; +- +- app_child = g_file_get_child (app_files, ".ref"); +- +- /* Lock ${app}/.ref if it exists */ +- if (g_file_query_exists (app_child, NULL)) +- flatpak_bwrap_add_args (bwrap, +- "--lock-file", "/app/.ref", +- NULL); +- } + } + else + { +@@ -3288,7 +3452,7 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + NULL); + } + +- if (original_app_files != NULL && app_files != original_app_files) ++ if (original_app_fd >= 0 && original_app_fd != app_fd) + { + /* Put the real Flatpak app in /run/parent/app */ + flatpak_bwrap_add_args (bwrap, +@@ -3301,26 +3465,37 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + + if (metakey != NULL && + !flatpak_run_add_extension_args (bwrap, metakey, app_ref, +- use_ld_so_cache, app_target_path, ++ use_ld_so_cache, original_app_target_path, + &app_extensions, &app_ld_path, + cancellable, error)) + return FALSE; + + if (!flatpak_run_add_extension_args (bwrap, runtime_metakey, runtime_ref, +- use_ld_so_cache, runtime_target_path, ++ use_ld_so_cache, original_runtime_target_path, + &runtime_extensions, &runtime_ld_path, + cancellable, error)) + return FALSE; + +- if (custom_usr_path == NULL) ++ if (runtime_fd == original_runtime_fd) + flatpak_run_extend_ld_path (bwrap, NULL, runtime_ld_path); + +- if (custom_app_path == NULL) ++ if (app_fd == original_app_fd) + flatpak_run_extend_ld_path (bwrap, app_ld_path, NULL); + +- runtime_ld_so_conf = g_file_resolve_relative_path (runtime_files, "etc/ld.so.conf"); +- if (lstat (flatpak_file_get_path_cached (runtime_ld_so_conf), &s) == 0) +- generate_ld_so_conf = S_ISREG (s.st_mode) && s.st_size == 0; ++ { ++ glnx_autofd int ld_so_conf_fd = -1; ++ struct glnx_statx stx; ++ ++ ld_so_conf_fd = glnx_chase_and_statxat (runtime_fd, "etc/ld.so.conf", ++ GLNX_CHASE_RESOLVE_BENEATH | ++ GLNX_CHASE_MUST_BE_REGULAR, ++ GLNX_STATX_SIZE, ++ &stx, NULL); ++ if (ld_so_conf_fd < 0 || ++ !(stx.stx_mask & GLNX_STATX_SIZE) || ++ stx.stx_size != 0) ++ generate_ld_so_conf = FALSE; ++ } + + /* At this point we have the minimal argv set up, with just the app, runtime and extensions. + We can reuse this to generate the ld.so.cache (if needed) */ +@@ -3332,7 +3507,7 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + bwrap->fds, + app_id_dir, + checksum, +- runtime_files, ++ runtime_fd, + generate_ld_so_conf, + cancellable, error); + if (ld_so_fd == -1) +@@ -3342,7 +3517,7 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + + flags |= flatpak_context_get_run_flags (app_context); + +- if (!flatpak_run_setup_base_argv (bwrap, runtime_files, app_id_dir, app_arch, flags, error)) ++ if (!flatpak_run_setup_base_argv (bwrap, runtime_fd, app_id_dir, app_arch, flags, error)) + return FALSE; + + if (generate_ld_so_conf) +-- +2.54.0 + diff --git a/CVE-2026-34078-6-run-add-usr-fd-and-app-fd-options.patch b/CVE-2026-34078-6-run-add-usr-fd-and-app-fd-options.patch new file mode 100644 index 0000000..4385d09 --- /dev/null +++ b/CVE-2026-34078-6-run-add-usr-fd-and-app-fd-options.patch @@ -0,0 +1,78 @@ +From 494c5ea687be884487fa5dc8b943130d9df7bb5f Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Fri, 6 Feb 2026 20:55:46 +0100 +Subject: [PATCH] run: Add --usr-fd and --app-fd options + +Exposes options to pass in a fd for the runtime and app deploy. The +flatpak portal will make use of this in a following commit. +--- + app/flatpak-builtins-run.c | 30 ++++++++++++++++++++++++++++-- + 1 file changed, 28 insertions(+), 2 deletions(-) + +diff --git a/app/flatpak-builtins-run.c b/app/flatpak-builtins-run.c +index aebc345c..a2644f89 100644 +--- a/app/flatpak-builtins-run.c ++++ b/app/flatpak-builtins-run.c +@@ -60,7 +60,9 @@ static gboolean opt_parent_expose_pids; + static gboolean opt_parent_share_pids; + static int opt_instance_id_fd = -1; + static char *opt_app_path; ++static int opt_app_fd = -1; + static char *opt_usr_path; ++static int opt_usr_fd = -1; + + static GOptionEntry options[] = { + { "arch", 0, 0, G_OPTION_ARG_STRING, &opt_arch, N_("Arch to use"), N_("ARCH") }, +@@ -88,7 +90,9 @@ static GOptionEntry options[] = { + { "parent-share-pids", 0, 0, G_OPTION_ARG_NONE, &opt_parent_share_pids, N_("Share process ID namespace with parent"), NULL }, + { "instance-id-fd", 0, 0, G_OPTION_ARG_INT, &opt_instance_id_fd, N_("Write the instance ID to the given file descriptor"), NULL }, + { "app-path", 0, 0, G_OPTION_ARG_FILENAME, &opt_app_path, N_("Use PATH instead of the app's /app"), N_("PATH") }, ++ { "app-fd", 0, 0, G_OPTION_ARG_INT, &opt_app_fd, N_("Use FD instead of the app's /app"), N_("FD") }, + { "usr-path", 0, 0, G_OPTION_ARG_FILENAME, &opt_usr_path, N_("Use PATH instead of the runtime's /usr"), N_("PATH") }, ++ { "usr-fd", 0, 0, G_OPTION_ARG_INT, &opt_usr_fd, N_("Use FD instead of the runtime's /usr"), N_("FD") }, + { NULL } + }; + +@@ -311,7 +315,18 @@ flatpak_builtin_run (int argc, char **argv, GCancellable *cancellable, GError ** + if (!opt_session_bus) + flags |= FLATPAK_RUN_FLAG_NO_SESSION_BUS_PROXY; + +- if (opt_app_path != NULL) ++ if (opt_app_fd >= 0 && opt_app_path != NULL) ++ { ++ flatpak_fail_error (error, FLATPAK_ERROR, ++ _("app-fd and app-path cannot both be used")); ++ return FALSE; ++ } ++ ++ if (opt_app_fd >= 0) ++ { ++ app_fd = opt_app_fd; ++ } ++ else if (opt_app_path != NULL) + { + if (g_strcmp0 (opt_app_path, "") == 0) + { +@@ -330,7 +345,18 @@ flatpak_builtin_run (int argc, char **argv, GCancellable *cancellable, GError ** + app_fd = FLATPAK_RUN_APP_DEPLOY_APP_ORIGINAL; + } + +- if (opt_usr_path != NULL) ++ if (opt_usr_fd >= 0 && opt_usr_path != NULL) ++ { ++ flatpak_fail_error (error, FLATPAK_ERROR, ++ _("usr-fd and usr-path cannot both be used")); ++ return FALSE; ++ } ++ ++ if (opt_usr_fd >= 0) ++ { ++ usr_fd = opt_usr_fd; ++ } ++ else if (opt_usr_path != NULL) + { + usr_fd = open (opt_usr_path, O_PATH | O_CLOEXEC | O_NOFOLLOW); + +-- +2.54.0 + diff --git a/CVE-2026-34078-7-run-add-ro-bind-fds-to-flatpak-run-app.patch b/CVE-2026-34078-7-run-add-ro-bind-fds-to-flatpak-run-app.patch new file mode 100644 index 0000000..15fd271 --- /dev/null +++ b/CVE-2026-34078-7-run-add-ro-bind-fds-to-flatpak-run-app.patch @@ -0,0 +1,116 @@ +From 4f1e7f8d1705488e501925255fbe061ac71405fc Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Fri, 6 Feb 2026 21:02:47 +0100 +Subject: [PATCH] run: Add (ro-)bind fds to flatpak_run_app + +The flatpak portal allows apps to expose files and folders from within +the sandbox to a side-sandbox using flatpak-spawn. So far it has used +the --filesystem option to mount those files and folders, but it takes a +path. Paths are inherently racy and they allow the app to swap out any +component of the path with a symlink after handing it off. If they win +the race, flatpak will mount a completely different directory. + +This adds a new way to mount files and directories based on O_PATH +file descriptor that needs to provided when execing the flatpak binary. +--- + app/flatpak-builtins-run.c | 2 ++ + common/flatpak-installation.c | 1 + + common/flatpak-run-private.h | 2 ++ + common/flatpak-run.c | 36 +++++++++++++++++++++++++++++++++++ + 4 files changed, 41 insertions(+) + +diff --git a/app/flatpak-builtins-run.c b/app/flatpak-builtins-run.c +index a2644f89..b14bf056 100644 +--- a/app/flatpak-builtins-run.c ++++ b/app/flatpak-builtins-run.c +@@ -385,6 +385,8 @@ flatpak_builtin_run (int argc, char **argv, GCancellable *cancellable, GError ** + opt_instance_id_fd, + (const char * const *) run_environ, + NULL, ++ NULL, ++ NULL, + cancellable, + error)) + return FALSE; +diff --git a/common/flatpak-installation.c b/common/flatpak-installation.c +index 29250b90..fe5e5757 100644 +--- a/common/flatpak-installation.c ++++ b/common/flatpak-installation.c +@@ -714,6 +714,7 @@ flatpak_installation_launch_full (FlatpakInstallation *self, + NULL, 0, -1, + (const char * const *) run_environ, + &instance_dir, ++ NULL, NULL, + cancellable, error)) + return FALSE; + +diff --git a/common/flatpak-run-private.h b/common/flatpak-run-private.h +index 0adbb8af..8c6cc049 100644 +--- a/common/flatpak-run-private.h ++++ b/common/flatpak-run-private.h +@@ -124,6 +124,8 @@ gboolean flatpak_run_app (FlatpakDecomposed *app_ref, + int instance_id_fd, + const char * const *run_environ, + char **instance_dir_out, ++ GArray *bind_fds, ++ GArray *ro_bind_fds, + GCancellable *cancellable, + GError **error); + +diff --git a/common/flatpak-run.c b/common/flatpak-run.c +index 2813d150..055c721d 100644 +--- a/common/flatpak-run.c ++++ b/common/flatpak-run.c +@@ -2979,6 +2979,8 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + int instance_id_fd, + const char * const *run_environ, + char **instance_dir_out, ++ GArray *bind_fds, ++ GArray *ro_bind_fds, + GCancellable *cancellable, + GError **error) + { +@@ -3583,6 +3585,40 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + flatpak_bwrap_add_arg_printf (bwrap, "/run/user/%d", getuid ()); + } + ++ for (i = 0; bind_fds && i < bind_fds->len; i++) ++ { ++ int fd = g_array_index (bind_fds, int, i); ++ g_autofree char *path = NULL; ++ ++ /* We get the path the fd refers to, to determine to mount point ++ * destination inside the sandbox */ ++ path = get_path_for_fd (fd, error); ++ if (!path) ++ return FALSE; ++ ++ if (!flatpak_bwrap_add_args_data_fd_dup (bwrap, ++ "--bind-fd", fd, path, ++ error)) ++ return FALSE; ++ } ++ ++ for (i = 0; ro_bind_fds && i < ro_bind_fds->len; i++) ++ { ++ int fd = g_array_index (ro_bind_fds, int, i); ++ g_autofree char *path = NULL; ++ ++ /* We get the path the fd refers to, to determine to mount point ++ * destination inside the sandbox */ ++ path = get_path_for_fd (fd, error); ++ if (!path) ++ return FALSE; ++ ++ if (!flatpak_bwrap_add_args_data_fd_dup (bwrap, ++ "--ro-bind-fd", fd, path, ++ error)) ++ return FALSE; ++ } ++ + if (!flatpak_run_add_dconf_args (bwrap, app_id, metakey, error)) + return FALSE; + +-- +2.54.0 + diff --git a/CVE-2026-34078-8-run-add-ro-bind-fd-options.patch b/CVE-2026-34078-8-run-add-ro-bind-fd-options.patch new file mode 100644 index 0000000..d9a76e2 --- /dev/null +++ b/CVE-2026-34078-8-run-add-ro-bind-fd-options.patch @@ -0,0 +1,95 @@ +From a971671f9546a20e2134afde31b317ff140e5bd9 Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Fri, 6 Feb 2026 21:03:34 +0100 +Subject: [PATCH] run: Add --(ro-)bind-fd options + +Exposes the functionality added to flatpak_run_app in the previous +commit with two new options. +--- + app/flatpak-builtins-run.c | 49 ++++++++++++++++++++++++++++++++++++-- + 1 file changed, 47 insertions(+), 2 deletions(-) + +diff --git a/app/flatpak-builtins-run.c b/app/flatpak-builtins-run.c +index b14bf056..0bfc311d 100644 +--- a/app/flatpak-builtins-run.c ++++ b/app/flatpak-builtins-run.c +@@ -63,6 +63,46 @@ static char *opt_app_path; + static int opt_app_fd = -1; + static char *opt_usr_path; + static int opt_usr_fd = -1; ++static GArray *opt_bind_fds = NULL; ++static GArray *opt_ro_bind_fds = NULL; ++ ++static gboolean ++option_bind_fd_cb (const char *option_name, ++ const char *value, ++ gpointer data, ++ GError **error) ++{ ++ glnx_autofd int fd = -1; ++ ++ fd = flatpak_parse_fd (value, error); ++ if (fd < 0) ++ return FALSE; ++ ++ if (fd < 3) ++ return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); ++ ++ g_array_append_val (opt_bind_fds, fd); ++ return TRUE; ++} ++ ++static gboolean ++option_ro_bind_fd_cb (const char *option_name, ++ const char *value, ++ gpointer data, ++ GError **error) ++{ ++ glnx_autofd int fd = -1; ++ ++ fd = flatpak_parse_fd (value, error); ++ if (fd < 0) ++ return FALSE; ++ ++ if (fd < 3) ++ return glnx_throw (error, "File descriptors 0, 1, 2 are reserved"); ++ ++ g_array_append_val (opt_ro_bind_fds, fd); ++ return TRUE; ++} + + static GOptionEntry options[] = { + { "arch", 0, 0, G_OPTION_ARG_STRING, &opt_arch, N_("Arch to use"), N_("ARCH") }, +@@ -93,6 +133,8 @@ static GOptionEntry options[] = { + { "app-fd", 0, 0, G_OPTION_ARG_INT, &opt_app_fd, N_("Use FD instead of the app's /app"), N_("FD") }, + { "usr-path", 0, 0, G_OPTION_ARG_FILENAME, &opt_usr_path, N_("Use PATH instead of the runtime's /usr"), N_("PATH") }, + { "usr-fd", 0, 0, G_OPTION_ARG_INT, &opt_usr_fd, N_("Use FD instead of the runtime's /usr"), N_("FD") }, ++ { "bind-fd", 0, 0, G_OPTION_ARG_CALLBACK | G_OPTION_FLAG_HIDDEN, &option_bind_fd_cb, N_("Bind mount the file or directory referred to by FD to its canonicalized path"), N_("FD") }, ++ { "ro-bind-fd", 0, 0, G_OPTION_ARG_CALLBACK | G_OPTION_FLAG_HIDDEN, &option_ro_bind_fd_cb, N_("Bind mount the file or directory referred to by FD read-only to its canonicalized path"), N_("FD") }, + { NULL } + }; + +@@ -120,6 +162,9 @@ flatpak_builtin_run (int argc, char **argv, GCancellable *cancellable, GError ** + + run_environ = g_get_environ (); + ++ opt_bind_fds = g_array_new (FALSE, FALSE, sizeof (int)); ++ opt_ro_bind_fds = g_array_new (FALSE, FALSE, sizeof (int)); ++ + context = g_option_context_new (_("APP [ARGUMENT…] - Run an app")); + g_option_context_set_translation_domain (context, GETTEXT_PACKAGE); + +@@ -385,8 +430,8 @@ flatpak_builtin_run (int argc, char **argv, GCancellable *cancellable, GError ** + opt_instance_id_fd, + (const char * const *) run_environ, + NULL, +- NULL, +- NULL, ++ opt_bind_fds, ++ opt_ro_bind_fds, + cancellable, + error)) + return FALSE; +-- +2.54.0 + diff --git a/CVE-2026-34078-9-portal-use-bind-fd-app-fd-and-usr-fd-options-to-avoid-races.patch b/CVE-2026-34078-9-portal-use-bind-fd-app-fd-and-usr-fd-options-to-avoid-races.patch new file mode 100644 index 0000000..132f2bd --- /dev/null +++ b/CVE-2026-34078-9-portal-use-bind-fd-app-fd-and-usr-fd-options-to-avoid-races.patch @@ -0,0 +1,602 @@ +From c82a3d7716bd02961d8c68b1d18256dfea19efc5 Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Fri, 6 Feb 2026 21:03:58 +0100 +Subject: [PATCH] portal: Use --bind-fd, --app-fd and --usr-fd options to avoid + races + +Now that flatpak_run_app accepts fds for app and runtime deploy, as well +as bind and ro-bind fds, and flatpak-run exposes the functionality, we +can finally hook this all up to the flatpak portal! +--- + portal/flatpak-portal.c | 438 +++++++++++++++------------------------- + 1 file changed, 162 insertions(+), 276 deletions(-) + +diff --git a/portal/flatpak-portal.c b/portal/flatpak-portal.c +index 12f2729a..5c0935a1 100644 +--- a/portal/flatpak-portal.c ++++ b/portal/flatpak-portal.c +@@ -552,195 +552,60 @@ child_setup_func (gpointer user_data) + } + + static gboolean +-is_valid_expose (const char *expose, +- GError **error) ++validate_opath_fd (int fd, ++ gboolean needs_writable, ++ GError **error) + { +- /* No subdirs or absolute paths */ +- if (expose[0] == '/') +- { +- g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, +- "Invalid sandbox expose: absolute paths not allowed"); +- return FALSE; +- } +- else if (strchr (expose, '/')) +- { +- g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, +- "Invalid sandbox expose: subdirectories not allowed"); +- return FALSE; +- } +- +- return TRUE; +-} +- +-static char * +-filesystem_arg (const char *path, +- gboolean readonly) +-{ +- g_autoptr(GString) s = g_string_new ("--filesystem="); +- const char *p; +- +- for (p = path; *p != 0; p++) +- { +- if (*p == ':') +- g_string_append (s, "\\:"); +- else +- g_string_append_c (s, *p); +- } +- +- if (readonly) +- g_string_append (s, ":ro"); +- +- return g_string_free (g_steal_pointer (&s), FALSE); +-} +- +- +-static char * +-filesystem_sandbox_arg (const char *path, +- const char *sandbox, +- gboolean readonly) +-{ +- g_autoptr(GString) s = g_string_new ("--filesystem="); +- const char *p; +- +- for (p = path; *p != 0; p++) +- { +- if (*p == ':') +- g_string_append (s, "\\:"); +- else +- g_string_append_c (s, *p); +- } +- +- g_string_append (s, "/sandbox/"); +- +- for (p = sandbox; *p != 0; p++) +- { +- if (*p == ':') +- g_string_append (s, "\\:"); +- else +- g_string_append_c (s, *p); +- } +- +- if (readonly) +- g_string_append (s, ":ro"); +- +- return g_string_free (g_steal_pointer (&s), FALSE); +-} +- +-static char * +-bubblewrap_remap_path (const char *path) +-{ +- if (g_str_has_prefix (path, "/newroot/")) +- path = path + strlen ("/newroot"); +- return g_strdup (path); +-} +- +-static char * +-verify_proc_self_fd (const char *proc_path, +- GError **error) +-{ +- char path_buffer[PATH_MAX + 1]; +- ssize_t symlink_size; +- +- symlink_size = readlink (proc_path, path_buffer, PATH_MAX); +- if (symlink_size < 0) +- return glnx_null_throw_errno_prefix (error, "readlink"); +- +- path_buffer[symlink_size] = 0; +- +- /* All normal paths start with /, but some weird things +- don't, such as socket:[27345] or anon_inode:[eventfd]. +- We don't support any of these */ +- if (path_buffer[0] != '/') +- return glnx_null_throw (error, "%s resolves to non-absolute path %s", +- proc_path, path_buffer); +- +- /* File descriptors to actually deleted files have " (deleted)" +- appended to them. This also happens to some fake fd types +- like shmem which are "/ (deleted)". All such +- files are considered invalid. Unfortunatelly this also +- matches files with filenames that actually end in " (deleted)", +- but there is not much to do about this. */ +- if (g_str_has_suffix (path_buffer, " (deleted)")) +- return glnx_null_throw (error, "%s resolves to deleted path %s", +- proc_path, path_buffer); +- +- /* remap from sandbox to host if needed */ +- return bubblewrap_remap_path (path_buffer); +-} +- +-static char * +-get_path_for_fd (int fd, +- gboolean *writable_out, +- GError **error) +-{ +- g_autofree char *proc_path = NULL; + int fd_flags; + struct stat st_buf; +- struct stat real_st_buf; +- g_autofree char *path = NULL; +- gboolean writable = FALSE; +- int read_access_mode; ++ int access_mode; + + /* Must be able to get fd flags */ + fd_flags = fcntl (fd, F_GETFL); +- if (fd_flags == -1) +- return glnx_null_throw_errno_prefix (error, "fcntl F_GETFL"); ++ if (fd_flags < 0) ++ return glnx_throw_errno_prefix (error, "Failed to get fd flags"); + + /* Must be O_PATH */ + if ((fd_flags & O_PATH) != O_PATH) +- return glnx_null_throw (error, "not opened with O_PATH"); +- +- /* We don't want to allow exposing symlinks, because if they are +- * under the callers control they could be changed between now and +- * starting the child allowing it to point anywhere, so enforce NOFOLLOW. +- * and verify that stat is not a link. +- */ +- if ((fd_flags & O_NOFOLLOW) != O_NOFOLLOW) +- return glnx_null_throw (error, "not opened with O_NOFOLLOW"); ++ { ++ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, ++ "File descriptor is not O_PATH"); ++ return FALSE; ++ } + + /* Must be able to fstat */ + if (fstat (fd, &st_buf) < 0) +- return glnx_null_throw_errno_prefix (error, "fstat"); +- +- /* As per above, no symlinks */ +- if (S_ISLNK (st_buf.st_mode)) +- return glnx_null_throw (error, "is a symbolic link"); ++ return glnx_throw_errno_prefix (error, "Failed to fstat"); + +- proc_path = g_strdup_printf ("/proc/self/fd/%d", fd); +- +- /* Must be able to read valid path from /proc/self/fd */ +- /* This is an absolute and (at least at open time) symlink-expanded path */ +- path = verify_proc_self_fd (proc_path, error); +- if (path == NULL) +- return NULL; ++ access_mode = R_OK; ++ if (S_ISDIR (st_buf.st_mode)) ++ access_mode |= X_OK; + +- /* Verify that this is the same file as the app opened */ +- if (stat (path, &real_st_buf) < 0 || +- st_buf.st_dev != real_st_buf.st_dev || +- st_buf.st_ino != real_st_buf.st_ino) +- { +- /* Different files on the inside and the outside, reject the request */ +- return glnx_null_throw (error, +- "different file inside and outside sandbox"); +- } ++ if (needs_writable) ++ access_mode |= W_OK; + +- read_access_mode = R_OK; +- if (S_ISDIR (st_buf.st_mode)) +- read_access_mode |= X_OK; ++ /* Must be able to access readable and potentially writable */ ++ if (faccessat (fd, "", access_mode, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW) != 0) ++ return glnx_throw_errno_prefix (error, "Bad access mode"); + +- /* Must be able to access the path via the sandbox supplied O_PATH fd, +- which applies the sandbox side mount options (like readonly). */ +- if (access (proc_path, read_access_mode) != 0) +- return glnx_null_throw (error, "not %s in sandbox", +- read_access_mode & X_OK ? "accessible" : "readable"); ++ return TRUE; ++} + +- if (access (proc_path, W_OK) == 0) +- writable = TRUE; ++static int ++fd_map_remap_fd (GArray *fd_map, ++ int *max_fd_in_out, ++ int fd) ++{ ++ FdMapEntry fd_map_entry; + +- if (writable_out != NULL) +- *writable_out = writable; ++ /* Use a fd that hasn't been used yet. We might have to reshuffle ++ * fd_map_entry.to, a bit later. */ ++ fd_map_entry.from = fd; ++ fd_map_entry.to = ++(*max_fd_in_out); ++ fd_map_entry.final = fd_map_entry.to; ++ g_array_append_val (fd_map, fd_map_entry); + +- return g_steal_pointer (&path); ++ return fd_map_entry.final; + } + + static gboolean +@@ -798,10 +663,13 @@ handle_spawn (PortalFlatpak *object, + gboolean devel; + gboolean empty_app; + g_autoptr(GString) env_string = g_string_new (""); +- glnx_autofd int env_fd = -1; + const char *flatpak; + gboolean testing = FALSE; + g_autofree char *app_id_prefix = NULL; ++ g_autoptr(GArray) owned_fds = NULL; ++ g_autoptr(GArray) expose_fds = NULL; ++ g_autoptr(GArray) expose_fds_ro = NULL; ++ glnx_autofd int instance_sandbox_fd = -1; + + child_setup_data.instance_id_fd = -1; + child_setup_data.env_fd = -1; +@@ -925,29 +793,6 @@ handle_spawn (PortalFlatpak *object, + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + +- for (i = 0; sandbox_expose != NULL && sandbox_expose[i] != NULL; i++) +- { +- const char *expose = sandbox_expose[i]; +- +- g_info ("exposing %s", expose); +- if (!is_valid_expose (expose, &error)) +- { +- g_dbus_method_invocation_return_gerror (invocation, error); +- return G_DBUS_METHOD_INVOCATION_HANDLED; +- } +- } +- +- for (i = 0; sandbox_expose_ro != NULL && sandbox_expose_ro[i] != NULL; i++) +- { +- const char *expose = sandbox_expose_ro[i]; +- g_info ("exposing %s", expose); +- if (!is_valid_expose (expose, &error)) +- { +- g_dbus_method_invocation_return_gerror (invocation, error); +- return G_DBUS_METHOD_INVOCATION_HANDLED; +- } +- } +- + app_id_prefix = g_strdup_printf ("%s.", app_id); + for (i = 0; sandbox_a11y_own_names != NULL && sandbox_a11y_own_names[i] != NULL; i++) + { +@@ -1197,10 +1042,14 @@ handle_spawn (PortalFlatpak *object, + g_string_append_c (env_string, '\0'); + } + ++ owned_fds = g_array_new (FALSE, FALSE, sizeof (int)); ++ g_array_set_clear_func (owned_fds, (GDestroyNotify) glnx_close_fd); ++ + if (env_string->len > 0) + { +- FdMapEntry fd_map_entry; + g_auto(GLnxTmpfile) env_tmpf = { 0, }; ++ int env_fd = -1; ++ int remapped_fd; + + if (!flatpak_buffer_to_sealed_memfd_or_tmpfile (&env_tmpf, "environ", + env_string->str, +@@ -1211,16 +1060,12 @@ handle_spawn (PortalFlatpak *object, + } + + env_fd = g_steal_fd (&env_tmpf.fd); ++ g_array_append_val (owned_fds, env_fd); + +- /* Use a fd that hasn't been used yet. We might have to reshuffle +- * fd_map_entry.to, a bit later. */ +- fd_map_entry.from = env_fd; +- fd_map_entry.to = ++max_fd; +- fd_map_entry.final = fd_map_entry.to; +- g_array_append_val (fd_map, fd_map_entry); ++ remapped_fd = fd_map_remap_fd (fd_map, &max_fd, env_fd); + + g_ptr_array_add (flatpak_argv, +- g_strdup_printf ("--env-fd=%d", fd_map_entry.final)); ++ g_strdup_printf ("--env-fd=%d", remapped_fd)); + } + + for (i = 0; unset_env != NULL && unset_env[i] != NULL; i++) +@@ -1318,54 +1163,100 @@ handle_spawn (PortalFlatpak *object, + else + g_ptr_array_add (flatpak_argv, g_strdup ("--unshare=network")); + ++ expose_fds = g_array_new (FALSE, FALSE, sizeof (int)); ++ expose_fds_ro = g_array_new (FALSE, FALSE, sizeof (int)); ++ ++ if (instance_path != NULL) ++ { ++ glnx_autofd int instance_fd = -1; ++ ++ instance_fd = glnx_chaseat (AT_FDCWD, instance_path, ++ GLNX_CHASE_DEFAULT, ++ &error); ++ if (instance_fd < 0) ++ { ++ g_dbus_method_invocation_return_gerror (invocation, error); ++ return G_DBUS_METHOD_INVOCATION_HANDLED; ++ } ++ ++ if (!glnx_ensure_dir (instance_fd, "sandbox", 0700, &error)) ++ { ++ g_warning ("Unable to create %s/sandbox: %s", instance_path, error->message); ++ g_clear_error (&error); ++ } ++ ++ instance_sandbox_fd = glnx_chaseat (instance_fd, "sandbox", ++ GLNX_CHASE_RESOLVE_NO_SYMLINKS, ++ &error); ++ if (instance_sandbox_fd < 0) ++ { ++ g_dbus_method_invocation_return_gerror (invocation, error); ++ return G_DBUS_METHOD_INVOCATION_HANDLED; ++ } ++ } + +- if (instance_path) ++ for (i = 0; sandbox_expose != NULL && sandbox_expose[i] != NULL; i++) + { +- for (i = 0; sandbox_expose != NULL && sandbox_expose[i] != NULL; i++) +- g_ptr_array_add (flatpak_argv, +- filesystem_sandbox_arg (instance_path, sandbox_expose[i], FALSE)); +- for (i = 0; sandbox_expose_ro != NULL && sandbox_expose_ro[i] != NULL; i++) +- g_ptr_array_add (flatpak_argv, +- filesystem_sandbox_arg (instance_path, sandbox_expose_ro[i], TRUE)); ++ int expose_fd; ++ ++ g_assert (instance_sandbox_fd >= 0); ++ ++ expose_fd = glnx_chaseat (instance_sandbox_fd, sandbox_expose[i], ++ GLNX_CHASE_RESOLVE_NO_SYMLINKS | ++ GLNX_CHASE_RESOLVE_BENEATH, ++ &error); ++ if (expose_fd < 0) ++ { ++ g_dbus_method_invocation_return_gerror (invocation, error); ++ return G_DBUS_METHOD_INVOCATION_HANDLED; ++ } ++ ++ g_array_append_val (expose_fds, expose_fd); ++ /* transfers ownership, can't g_steal_fd with g_array_append_val */ ++ g_array_append_val (owned_fds, expose_fd); + } + + for (i = 0; sandbox_expose_ro != NULL && sandbox_expose_ro[i] != NULL; i++) + { +- const char *expose = sandbox_expose_ro[i]; +- g_info ("exposing %s", expose); ++ int expose_fd; ++ ++ g_assert (instance_sandbox_fd >= 0); ++ ++ expose_fd = glnx_chaseat (instance_sandbox_fd, sandbox_expose_ro[i], ++ GLNX_CHASE_RESOLVE_NO_SYMLINKS | ++ GLNX_CHASE_RESOLVE_BENEATH, ++ &error); ++ if (expose_fd < 0) ++ { ++ g_dbus_method_invocation_return_gerror (invocation, error); ++ return G_DBUS_METHOD_INVOCATION_HANDLED; ++ } ++ ++ g_array_append_val (expose_fds_ro, expose_fd); ++ /* transfers ownership, can't g_steal_fd with g_array_append_val */ ++ g_array_append_val (owned_fds, expose_fd); + } + + if (sandbox_expose_fd != NULL) + { + gsize len = g_variant_n_children (sandbox_expose_fd); ++ + for (i = 0; i < len; i++) + { + gint32 handle; ++ + g_variant_get_child (sandbox_expose_fd, i, "h", &handle); +- if (handle >= 0 && handle < fds_len) ++ if (handle >= 0 && handle < fds_len && ++ validate_opath_fd (fds[handle], TRUE, &error)) + { +- int handle_fd = fds[handle]; +- g_autofree char *path = NULL; +- gboolean writable = FALSE; +- +- path = get_path_for_fd (handle_fd, &writable, &error); +- +- if (path) +- { +- g_ptr_array_add (flatpak_argv, filesystem_arg (path, !writable)); +- } +- else +- { +- g_info ("unable to get path for sandbox-exposed fd %d, ignoring: %s", +- handle_fd, error->message); +- g_clear_error (&error); +- } ++ g_array_append_val (expose_fds, fds[handle]); + } + else + { ++ g_debug ("Invalid sandbox expose fd: %s", error->message); + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, +- "No file descriptor for handle %d", ++ "No valid file descriptor for handle %d", + handle); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } +@@ -1375,31 +1266,20 @@ handle_spawn (PortalFlatpak *object, + if (sandbox_expose_fd_ro != NULL) + { + gsize len = g_variant_n_children (sandbox_expose_fd_ro); ++ + for (i = 0; i < len; i++) + { + gint32 handle; ++ + g_variant_get_child (sandbox_expose_fd_ro, i, "h", &handle); +- if (handle >= 0 && handle < fds_len) ++ if (handle >= 0 && handle < fds_len && ++ validate_opath_fd (fds[handle], FALSE, &error)) + { +- int handle_fd = fds[handle]; +- g_autofree char *path = NULL; +- gboolean writable = FALSE; +- +- path = get_path_for_fd (handle_fd, &writable, &error); +- +- if (path) +- { +- g_ptr_array_add (flatpak_argv, filesystem_arg (path, TRUE)); +- } +- else +- { +- g_info ("unable to get path for sandbox-exposed fd %d, ignoring: %s", +- handle_fd, error->message); +- g_clear_error (&error); +- } ++ g_array_append_val (expose_fds_ro, fds[handle]); + } + else + { ++ g_debug ("Invalid sandbox expose ro fd: %s", error->message); + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "No file descriptor for handle %d", +@@ -1409,20 +1289,40 @@ handle_spawn (PortalFlatpak *object, + } + } + ++ for (i = 0; i < expose_fds->len; i++) ++ { ++ int remapped_fd; ++ ++ remapped_fd = fd_map_remap_fd (fd_map, &max_fd, expose_fds->data[i]); ++ ++ g_ptr_array_add (flatpak_argv, g_strdup_printf ("--bind-fd=%d", ++ remapped_fd)); ++ } ++ ++ for (i = 0; i < expose_fds_ro->len; i++) ++ { ++ int remapped_fd; ++ ++ remapped_fd = fd_map_remap_fd (fd_map, &max_fd, expose_fds_ro->data[i]); ++ ++ g_ptr_array_add (flatpak_argv, g_strdup_printf ("--ro-bind-fd=%d", ++ remapped_fd)); ++ } ++ + empty_app = (arg_flags & FLATPAK_SPAWN_FLAGS_EMPTY_APP) != 0; + ++ if (empty_app && app_fd != NULL) ++ { ++ g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, ++ G_DBUS_ERROR_INVALID_ARGS, ++ "app-fd and EMPTY_APP cannot both be used"); ++ return G_DBUS_METHOD_INVOCATION_HANDLED; ++ } ++ + if (app_fd != NULL) + { ++ int remapped_fd; + gint32 handle = g_variant_get_handle (app_fd); +- g_autofree char *path = NULL; +- +- if (empty_app) +- { +- g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, +- G_DBUS_ERROR_INVALID_ARGS, +- "app-fd and EMPTY_APP cannot both be used"); +- return G_DBUS_METHOD_INVOCATION_HANDLED; +- } + + if (handle >= fds_len || handle < 0) + { +@@ -1434,18 +1334,11 @@ handle_spawn (PortalFlatpak *object, + } + + g_assert (fds != NULL); /* otherwise fds_len would be 0 */ +- path = get_path_for_fd (fds[handle], NULL, &error); + +- if (path == NULL) +- { +- g_prefix_error (&error, "Unable to convert /app fd %d into path: ", +- fds[handle]); +- g_dbus_method_invocation_return_gerror (invocation, error); +- return G_DBUS_METHOD_INVOCATION_HANDLED; +- } ++ remapped_fd = fd_map_remap_fd (fd_map, &max_fd, fds[handle]); + +- g_info ("Using %s as /app instead of app", path); +- g_ptr_array_add (flatpak_argv, g_strdup_printf ("--app-path=%s", path)); ++ g_ptr_array_add (flatpak_argv, g_strdup_printf ("--app-fd=%d", ++ remapped_fd)); + } + else if (empty_app) + { +@@ -1454,8 +1347,8 @@ handle_spawn (PortalFlatpak *object, + + if (usr_fd != NULL) + { ++ int remapped_fd; + gint32 handle = g_variant_get_handle (usr_fd); +- g_autofree char *path = NULL; + + if (handle >= fds_len || handle < 0) + { +@@ -1467,18 +1360,11 @@ handle_spawn (PortalFlatpak *object, + } + + g_assert (fds != NULL); /* otherwise fds_len would be 0 */ +- path = get_path_for_fd (fds[handle], NULL, &error); + +- if (path == NULL) +- { +- g_prefix_error (&error, "Unable to convert /usr fd %d into path: ", +- fds[handle]); +- g_dbus_method_invocation_return_gerror (invocation, error); +- return G_DBUS_METHOD_INVOCATION_HANDLED; +- } ++ remapped_fd = fd_map_remap_fd (fd_map, &max_fd, fds[handle]); + +- g_info ("Using %s as /usr instead of runtime", path); +- g_ptr_array_add (flatpak_argv, g_strdup_printf ("--usr-path=%s", path)); ++ g_ptr_array_add (flatpak_argv, g_strdup_printf ("--usr-fd=%d", ++ remapped_fd)); + } + + g_ptr_array_add (flatpak_argv, g_strdup_printf ("--runtime=%s", runtime_parts[1])); +-- +2.54.0 + diff --git a/CVE-2026-34079-1-utils-only-remove-cached-files-in-the-cache-directory.patch b/CVE-2026-34079-1-utils-only-remove-cached-files-in-the-cache-directory.patch new file mode 100644 index 0000000..fa9fc41 --- /dev/null +++ b/CVE-2026-34079-1-utils-only-remove-cached-files-in-the-cache-directory.patch @@ -0,0 +1,84 @@ +From c97905c8188ddaad01ee146b57bba6c3fa294113 Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Fri, 9 Jan 2026 19:24:44 +0100 +Subject: utils: Only remove cached files in the cache directory + +The function flatpak_switch_symlink_and_remove is used to implement a +cache for ld.so (regenerate_ld_cache). If the active symlink changes to +a new cache file, the old cache file is supposed to get removed. + +The symlink still points to the old cache file, so we would remove the +file that it points to and then point at the new file. + +Because the symlink is under the app's control, the symlink can point +anywhere, and the removal happens in the host context, which allows an +app to remove arbitrary files on the host. + +The filename of the cache files are checksums, which means that we can +ensure that the link is a file in the same directory of the link by +checking that it only contains the chars a-zA-Z0-9. + +(cherry picked from commit FIXME) + +diff --git a/common/flatpak-utils.c b/common/flatpak-utils.c +index 7a30969c..dbc46951 100644 +--- a/common/flatpak-utils.c ++++ b/common/flatpak-utils.c +@@ -922,6 +922,22 @@ out: + return ret; + } + ++static gboolean ++flatpak_str_is_alphanumeric (const char *arg) ++{ ++ while (*arg != '\0') ++ { ++ char c = *arg; ++ ++ if (!g_ascii_isalnum (c)) ++ return FALSE; ++ ++ arg++; ++ } ++ ++ return TRUE; ++} ++ + /* This atomically replaces a symlink with a new value, removing the + * existing symlink target, if it exstis and is different from + * @target. This is atomic in the sense that we're guaranteed to +@@ -931,6 +947,9 @@ out: + * symlink for some reason, ending up with neither the old or the new + * target. That is fine if the reason for the symlink is keeping a + * cache though. ++ * The target shall only be a file in the same directory as the symlink, and ++ * shall only contain the characters a-zA-Z0-9. This is so that the target of ++ * the symlink that gets removed is in the same directory as the link. + */ + gboolean + flatpak_switch_symlink_and_remove (const char *symlink_path, +@@ -974,10 +993,21 @@ flatpak_switch_symlink_and_remove (const char *symlink_path, + g_autofree char *old_target = flatpak_readlink (tmp_path, error); + if (old_target == NULL) + return FALSE; +- if (strcmp (old_target, target) != 0) /* Don't remove old file if its the same as the new one */ ++ ++ /* Don't remove old file if its the same as the new one */ ++ if (strcmp (old_target, target) != 0) + { +- g_autofree char *old_target_path = g_build_filename (symlink_dir, old_target, NULL); +- unlink (old_target_path); ++ if (flatpak_str_is_alphanumeric (old_target)) ++ { ++ g_autofree char *old_target_path = NULL; ++ ++ old_target_path = g_build_filename (symlink_dir, old_target, NULL); ++ unlink (old_target_path); ++ } ++ else ++ { ++ g_warning ("Refusing to delete old link target %s", old_target); ++ } + } + } + else if (errno != ENOENT) diff --git a/CVE-2026-34079-2-utils-do-not-follow-symlinks-in-local-open-file.patch b/CVE-2026-34079-2-utils-do-not-follow-symlinks-in-local-open-file.patch new file mode 100644 index 0000000..d440318 --- /dev/null +++ b/CVE-2026-34079-2-utils-do-not-follow-symlinks-in-local-open-file.patch @@ -0,0 +1,50 @@ +From 4a678f463b455c585d38ac4cf4d994e7ce710f8e Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Mon, 12 Jan 2026 17:38:02 +0100 +Subject: [PATCH] utils: Do not follow symlinks in local_open_file + +We use local_open_file in the context of the system helper to open +files written by a user. This means that we want to prevent DOS and +exposing files which only the system helper has access to. + +To prevent DOS and avoid side-effects, the file is opened with +O_NONBLOCK and O_NOCTTY. + +To prevent leaking files, the file is supposed to not open symlinks. +This part, we failed at. We check if the opened file is a regular file, +but what we actually checked is, if the file a symlink might point at is +a regular file. + +Fix this by also specifying O_NOFOLLOW in openat. + +(cherry picked from commit FIXME) +--- + common/flatpak-oci-registry.c | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + +diff --git a/common/flatpak-oci-registry.c b/common/flatpak-oci-registry.c +index f45cec5c..942bb47b 100644 +--- a/common/flatpak-oci-registry.c ++++ b/common/flatpak-oci-registry.c +@@ -275,6 +275,9 @@ flatpak_oci_registry_new (const char *uri, + return oci_registry; + } + ++/* Carefully opens a file from a base directory and subpath, ++ * making sure that its not a symlink, pipe, etc. ++ */ + static int + local_open_file (int dfd, + const char *subpath, +@@ -286,7 +289,7 @@ local_open_file (int dfd, + struct stat tmp_st_buf; + + do +- fd = openat (dfd, subpath, O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY); ++ fd = openat (dfd, subpath, O_NOFOLLOW | O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY); + while (G_UNLIKELY (fd == -1 && errno == EINTR)); + if (fd == -1) + { +-- +2.54.0 + diff --git a/CVE-2026-34079-3-system-helper-only-remove-an-ongoing-pull-if-users-match.patch b/CVE-2026-34079-3-system-helper-only-remove-an-ongoing-pull-if-users-match.patch new file mode 100644 index 0000000..67a6888 --- /dev/null +++ b/CVE-2026-34079-3-system-helper-only-remove-an-ongoing-pull-if-users-match.patch @@ -0,0 +1,150 @@ +From a27ec46e8c0ab0ae162f2aa3142dccb6b79d9211 Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Sat, 7 Feb 2026 21:57:30 +0100 +Subject: [PATCH] system-helper: Only remove an ongoing pull if users match + +The code would always remove a pull from the hashtable, and then check if the +users match and abort if they don't. Either way, the pull gets dropped. + +Fix this by only removing the pull if the dir and the user match. +--- + system-helper/flatpak-system-helper.c | 85 ++++++++++++--------------- + 1 file changed, 36 insertions(+), 49 deletions(-) + +diff --git a/system-helper/flatpak-system-helper.c b/system-helper/flatpak-system-helper.c +index 778b3c59..f709f948 100644 +--- a/system-helper/flatpak-system-helper.c ++++ b/system-helper/flatpak-system-helper.c +@@ -358,23 +358,31 @@ get_connection_uid (GDBusMethodInvocation *invocation, uid_t *out_uid, GError ** + } + + static OngoingPull * +-take_ongoing_pull_by_dir (const gchar *src_dir) ++take_ongoing_pull_by_dir (const char *src_dir, ++ uid_t uid) + { + OngoingPull *pull = NULL; +- gpointer key, value; ++ char *cache_dir_name = NULL; + + G_LOCK (cache_dirs_in_use); +- /* Keep src_dir key inside hashtable but remove its OngoingPull +- * value and set it to NULL. This way src_dir is still marked +- * as in-use (as Deploy or CancelPull might be executing on it, +- * whereas OngoingPull ownership is transferred to respective +- * callers. */ +- if (g_hash_table_steal_extended (cache_dirs_in_use, src_dir, &key, &value)) +- { +- if (value) ++ if (g_hash_table_steal_extended (cache_dirs_in_use, src_dir, ++ (gpointer) &cache_dir_name, ++ (gpointer) &pull)) ++ { ++ if (pull && pull->uid == uid) + { +- g_hash_table_insert (cache_dirs_in_use, key, NULL); +- pull = value; ++ /* Keep src_dir key inside hashtable but remove its OngoingPull ++ * value and set it to NULL. This way src_dir is still marked ++ * as in-use (as Deploy or CancelPull might be executing on it, ++ * whereas OngoingPull ownership is transferred to respective ++ * callers. */ ++ g_hash_table_insert (cache_dirs_in_use, cache_dir_name, NULL); ++ } ++ else ++ { ++ /* Otherwise, re-insert what is currently there and return NULL */ ++ g_hash_table_insert (cache_dirs_in_use, cache_dir_name, pull); ++ pull = NULL; + } + } + G_UNLOCK (cache_dirs_in_use); +@@ -426,6 +434,9 @@ handle_deploy (FlatpakSystemHelper *object, + + if (strlen (arg_repo_path) > 0) + { ++ g_autoptr(GError) local_error = NULL; ++ uid_t uid; ++ + if (!g_file_query_exists (repo_file, NULL)) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, +@@ -433,30 +444,17 @@ handle_deploy (FlatpakSystemHelper *object, + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + ++ /* Ensure that pull's uid is same as the caller's uid */ ++ if (!get_connection_uid (invocation, &uid, &local_error)) ++ { ++ g_dbus_method_invocation_return_gerror (invocation, local_error); ++ return G_DBUS_METHOD_INVOCATION_HANDLED; ++ } ++ + src_dir = g_path_get_dirname (arg_repo_path); +- ongoing_pull = take_ongoing_pull_by_dir (src_dir); ++ ongoing_pull = take_ongoing_pull_by_dir (src_dir, uid); + if (ongoing_pull != NULL) + { +- g_autoptr(GError) local_error = NULL; +- uid_t uid; +- +- /* Ensure that pull's uid is same as the caller's uid */ +- if (!get_connection_uid (invocation, &uid, &local_error)) +- { +- g_dbus_method_invocation_return_gerror (invocation, local_error); +- return G_DBUS_METHOD_INVOCATION_HANDLED; +- } +- else +- { +- if (ongoing_pull->uid != uid) +- { +- g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, +- "Ongoing pull's uid(%d) does not match with peer uid(%d)", +- ongoing_pull->uid, uid); +- return G_DBUS_METHOD_INVOCATION_HANDLED; +- } +- } +- + terminate_revokefs_backend (ongoing_pull); + + if (!flatpak_canonicalize_permissions (AT_FDCWD, +@@ -738,31 +736,20 @@ handle_cancel_pull (FlatpakSystemHelper *object, + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + +- ongoing_pull = take_ongoing_pull_by_dir (arg_src_dir); +- if (ongoing_pull == NULL) ++ if (!get_connection_uid (invocation, &uid, &error)) + { +- g_set_error (&error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, +- "Cannot find ongoing pull to cancel at %s", arg_src_dir); + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + +- /* Ensure that pull's uid is same as the caller's uid */ +- if (!get_connection_uid (invocation, &uid, &error)) ++ ongoing_pull = take_ongoing_pull_by_dir (arg_src_dir, uid); ++ if (ongoing_pull == NULL) + { ++ g_set_error (&error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, ++ "Cannot find ongoing pull to cancel at %s", arg_src_dir); + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } +- else +- { +- if (ongoing_pull->uid != uid) +- { +- g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, +- "Ongoing pull's uid(%d) does not match with peer uid(%d)", +- ongoing_pull->uid, uid); +- return G_DBUS_METHOD_INVOCATION_HANDLED; +- } +- } + + ongoing_pull->preserve_pull = (arg_flags & FLATPAK_HELPER_CANCEL_PULL_FLAGS_PRESERVE_PULL) != 0; + ongoing_pull_free (ongoing_pull); +-- +2.54.0 + diff --git a/flatpak-run-Enable-FIPS-crypto-policy-if-it-is-enabled-on-th.patch b/flatpak-run-Enable-FIPS-crypto-policy-if-it-is-enabled-on-th.patch index afdf34e..e7222da 100644 --- a/flatpak-run-Enable-FIPS-crypto-policy-if-it-is-enabled-on-th.patch +++ b/flatpak-run-Enable-FIPS-crypto-policy-if-it-is-enabled-on-th.patch @@ -1,4 +1,4 @@ -From 5f5aeea8d8be071468fb8e9640554518fb65885e Mon Sep 17 00:00:00 2001 +From 8c8d6faa9f0fc35581e95a9e771490feb94dd9ac Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Tue, 16 Dec 2025 17:15:32 +0100 Subject: [PATCH] run: Enable FIPS crypto policy if it is enabled on the host @@ -6,27 +6,30 @@ Subject: [PATCH] run: Enable FIPS crypto policy if it is enabled on the host This is a close copy of what podman/containers does to support FIPS. Any other crypto policy is ignored for now. --- - common/flatpak-run.c | 87 ++++++++++++++++++++++++++++++++++++++++++++ - 1 file changed, 87 insertions(+) + common/flatpak-run.c | 94 ++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 94 insertions(+) -diff --git ./common/flatpak-run.c ../common/flatpak-run.c -index 6c319231..b51cc637 100644 ---- ./common/flatpak-run.c -+++ ../common/flatpak-run.c -@@ -2215,6 +2215,91 @@ flatpak_run_setup_usr_links (FlatpakBwrap *bwrap, +diff --git a/common/flatpak-run.c b/common/flatpak-run.c +index dfcef03..121bef3 100644 +--- a/common/flatpak-run.c ++++ b/common/flatpak-run.c +@@ -2214,6 +2214,98 @@ flatpak_run_setup_usr_links (FlatpakBwrap *bwrap, } } +static void +flatpak_run_setup_fips (FlatpakBwrap *bwrap, -+ GFile *runtime_files) ++ int runtime_fd) +{ -+ g_autoptr(GFile) runtime_crypto_policies = NULL; -+ g_autoptr(GFile) runtime_fips_backend = NULL; -+ g_autoptr(GFile) runtime_fips_config = NULL; ++ glnx_autofd int crypto_policies_fd = -1; ++ glnx_autofd int fips_backend_fd = -1; ++ glnx_autofd int fips_config_fd = -1; + g_autofree char *fips_enabled = NULL; + g_autoptr(GError) error = NULL; + ++ if (runtime_fd < 0) ++ return; ++ + if (!g_file_get_contents ("/proc/sys/crypto/fips_enabled", + &fips_enabled, + NULL, &error)) @@ -48,35 +51,39 @@ index 6c319231..b51cc637 100644 + return; + } + -+ runtime_crypto_policies = -+ g_file_resolve_relative_path (runtime_files, "etc/crypto-policies"); ++ crypto_policies_fd = glnx_chaseat (runtime_fd, "etc/crypto-policies", ++ GLNX_CHASE_RESOLVE_BENEATH, ++ NULL); + -+ if (!g_file_query_exists (runtime_crypto_policies, NULL)) ++ if (crypto_policies_fd < 0) + { + g_info ("FIPS is enabled, but runtime does not support it"); + return; + } + -+ runtime_fips_backend = -+ g_file_resolve_relative_path (runtime_files, -+ "share/crypto-policies/back-ends/FIPS"); ++ fips_backend_fd = glnx_chaseat (runtime_fd, ++ "share/crypto-policies/back-ends/FIPS", ++ GLNX_CHASE_RESOLVE_BENEATH, ++ NULL); + -+ if (!g_file_query_exists (runtime_fips_backend, NULL)) ++ if (fips_backend_fd < 0) + { + g_info ("FIPS is enabled, but runtime does not support it"); + return; + } + -+ runtime_fips_config = -+ g_file_resolve_relative_path (runtime_files, -+ "share/crypto-policies/default-fips-config"); ++ fips_config_fd = glnx_chaseat (runtime_fd, ++ "share/crypto-policies/default-fips-config", ++ GLNX_CHASE_RESOLVE_BENEATH | ++ GLNX_CHASE_MUST_BE_REGULAR, ++ NULL); + -+ if (g_file_query_exists (runtime_fips_config, NULL)) ++ if (fips_config_fd >= 0) + { -+ flatpak_bwrap_add_args (bwrap, "--ro-bind", -+ flatpak_file_get_path_cached (runtime_fips_config), -+ "/etc/crypto-policies/config", -+ NULL); ++ flatpak_bwrap_add_args_data_fd (bwrap, ++ "--ro-bind-fd", ++ g_steal_fd (&fips_config_fd), ++ "/etc/crypto-policies/config"); + } + else + { @@ -94,10 +101,10 @@ index 6c319231..b51cc637 100644 + } + } + -+ flatpak_bwrap_add_args (bwrap, "--ro-bind", -+ flatpak_file_get_path_cached (runtime_fips_backend), -+ "/etc/crypto-policies/back-ends", -+ NULL); ++ flatpak_bwrap_add_args_data_fd (bwrap, ++ "--ro-bind-fd", ++ g_steal_fd (&fips_backend_fd), ++ "/etc/crypto-policies/back-ends"); + + g_info ("Enabled FIPS configuration"); +} @@ -105,15 +112,15 @@ index 6c319231..b51cc637 100644 /* Directories in /sys to share with the sandbox if accessible. */ static const char *const sysfs_dirs[] = { -@@ -2405,6 +2490,8 @@ flatpak_run_setup_base_argv (FlatpakBwrap *bwrap, +@@ -2426,6 +2518,8 @@ flatpak_run_setup_base_argv (FlatpakBwrap *bwrap, } } -+ flatpak_run_setup_fips (bwrap, runtime_files); ++ flatpak_run_setup_fips (bwrap, runtime_fd); + if (app_id_dir != NULL) { g_autoptr(GFile) app_cache_dir = g_file_get_child (app_id_dir, "cache"); -- -2.51.0 +2.54.0 diff --git a/flatpak.spec b/flatpak.spec index 0b89681..5c1a3cc 100644 --- a/flatpak.spec +++ b/flatpak.spec @@ -12,7 +12,7 @@ Name: flatpak Version: 1.16.0 -Release: 9%{?dist} +Release: 9%{?dist}.1 Summary: Application deployment framework for desktop apps License: LGPL-2.1-or-later @@ -52,6 +52,48 @@ Patch7: flatpak-run-Enable-FIPS-crypto-policy-if-it-is-enabled-on-th.pat # Stop killing the session when stopping background apps Patch9: flatpak-kill-Do-not-kill-pid-0-and-embrace-races.patch +# CVE-2026-34078 +Patch100: CVE-2026-34078-1-update-subtree-libglnx-2026-04-07.patch +Patch101: CVE-2026-34078-2-flatpak-bwrap-add-dup-ing-variant-flatpak-bwrap-add-args-data-fd-dup.patch +Patch102: CVE-2026-34078-3-utils-add-flatpak-parse-fd.patch +Patch103: CVE-2026-34078-4-flatpak-bwrap-use-glnx-close-fd-as-clear-func.patch +Patch104: CVE-2026-34078-5-run-use-o-path-fds-for-the-runtime-and-app-deploy-directories.patch +Patch105: CVE-2026-34078-6-run-add-usr-fd-and-app-fd-options.patch +Patch106: CVE-2026-34078-7-run-add-ro-bind-fds-to-flatpak-run-app.patch +Patch107: CVE-2026-34078-8-run-add-ro-bind-fd-options.patch +Patch108: CVE-2026-34078-9-portal-use-bind-fd-app-fd-and-usr-fd-options-to-avoid-races.patch +Patch109: CVE-2026-34078-10-run-fix-checking-wrong-variable-in-runtime-fd-selection.patch +Patch110: CVE-2026-34078-11-run-mount-original-app-on-run-parent-app-when-using-app-path.patch +Patch111: CVE-2026-34078-12-portal-update-max-fd-after-creating-the-instance-id-pipe.patch +Patch112: CVE-2026-34078-13-run-fix-fd-tracking-in-flatpak-run-add-app-info-args.patch +Patch113: CVE-2026-34078-14-utils-improve-error-message-when-passing-an-fd-numer-which-is-not-a-fd.patch +Patch114: CVE-2026-34078-15-run-do-not-close-bind-ro-bind.patch +Patch115: CVE-2026-34078-16-run-use-the-same-fd-validation-for-all-fd-options.patch +Patch116: CVE-2026-34078-17-run-add-bind-fd-and-ro-bind-fd-binds-after-all-other-binds.patch +Patch117: CVE-2026-34078-18-portal-use-g-array-index-to-read-from-expose-fds-expose-fds-ro.patch +Patch118: CVE-2026-34078-19-run-fix-backport-mistake.patch +Patch119: CVE-2026-34078-20-tests-test-run-custom-test-usr-path-usr-fd-app-path-app-fd.patch +Patch120: CVE-2026-34078-21-tests-test-run-custom-test-bind-fd-and-ro-bind-fd.patch +Patch121: CVE-2026-34078-22-run-cope-with-an-empty-runtime.patch +Patch122: CVE-2026-34078-23-dir-in-apply-extra-data-don-t-assume-there-is-always-a-runtime.patch +Patch123: CVE-2026-34078-24-tests-add-test-extra-data-sh-to-test-extra-data-installation.patch +Patch124: CVE-2026-34078-25-tests-add-test-for-noruntime-extra-data-app.patch +Patch125: CVE-2026-34078-26-utils-add-flatpak-set-cloexec.patch +Patch126: CVE-2026-34078-27-run-context-mark-fd-arguments-as-close-on-exec.patch +Patch127: CVE-2026-34078-28-utils-move-flatpak-get-path-for-fd-to-here.patch +Patch128: CVE-2026-34078-29-portal-avoid-crash-if-sandbox-expose-ro-fd-is-out-of-range.patch +Patch129: CVE-2026-34078-30-portal-log-and-ignore-unusable-sandbox-expose-fds-instead-of-erroring.patch +Patch130: CVE-2026-34078-31-portal-reinstate-flatpak-get-path-for-fd-checks.patch +Patch131: CVE-2026-34078-32-libtest-allow-adding-a-new-ref-to-an-existing-temporary-ostree-repo.patch +Patch132: CVE-2026-34078-33-tests-check-that-flatpak-run-fd-arguments-do-not-leak-to-the-command.patch +Patch133: CVE-2026-34078-34-app-context-never-close-fds-0-1-or-2.patch +Patch134: CVE-2026-34078-35-app-context-factor-out-flatpak-accept-fd-argument.patch + +# CVE-2026-34079 +Patch135: CVE-2026-34079-1-utils-only-remove-cached-files-in-the-cache-directory.patch +Patch136: CVE-2026-34079-2-utils-do-not-follow-symlinks-in-local-open-file.patch +Patch137: CVE-2026-34079-3-system-helper-only-remove-an-ongoing-pull-if-users-match.patch + # ostree not on i686 for RHEL 10 # https://github.com/containers/composefs/pull/229#issuecomment-1838735764 %if 0%{?rhel} >= 10 @@ -320,6 +362,12 @@ fi %changelog +* Mon May 18 2026 Jan Grulich - 1.16.0-9.1 +- Fix arbitrary code execution via crafted symlinks in sandbox-expose options + Resolves: RHEL-165630 +- Fix arbitrary file deletion on host via improper cache file path validation + Resolves: RHEL-170157 + * Tue Jan 13 2026 Sebastian Wick - 1.16.0-9 - kill: Do not kill pid 0 and embrace races Resolves: RHEL-140924