diff --git a/SOURCES/CVE-2026-34078-1-flatpak-bwrap-add-dup-ing-variant-flatpak-bwrap-add-args-data-fd-dup.patch b/SOURCES/CVE-2026-34078-1-flatpak-bwrap-add-dup-ing-variant-flatpak-bwrap-add-args-data-fd-dup.patch new file mode 100644 index 0000000..2506bff --- /dev/null +++ b/SOURCES/CVE-2026-34078-1-flatpak-bwrap-add-dup-ing-variant-flatpak-bwrap-add-args-data-fd-dup.patch @@ -0,0 +1,61 @@ +From fbe7a80a9e49ed364af2d7caf5902e3088f54587 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 207d23a0..64bd26b6 100644 +--- a/common/flatpak-bwrap-private.h ++++ b/common/flatpak-bwrap-private.h +@@ -62,6 +62,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 cda0dbfb..1694ffd4 100644 +--- a/common/flatpak-bwrap.c ++++ b/common/flatpak-bwrap.c +@@ -141,6 +141,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/SOURCES/CVE-2026-34078-10-run-mount-original-app-on-run-parent-app-when-using-app-path.patch b/SOURCES/CVE-2026-34078-10-run-mount-original-app-on-run-parent-app-when-using-app-path.patch new file mode 100644 index 0000000..c60b763 --- /dev/null +++ b/SOURCES/CVE-2026-34078-10-run-mount-original-app-on-run-parent-app-when-using-app-path.patch @@ -0,0 +1,34 @@ +From 5096a974bcba5dfeddaea197b2ac63f558100858 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 4ffc5fa3..a80d0050 100644 +--- a/common/flatpak-run.c ++++ b/common/flatpak-run.c +@@ -4476,6 +4476,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/SOURCES/CVE-2026-34078-11-portal-update-max-fd-after-creating-the-instance-id-pipe.patch b/SOURCES/CVE-2026-34078-11-portal-update-max-fd-after-creating-the-instance-id-pipe.patch new file mode 100644 index 0000000..1b7f573 --- /dev/null +++ b/SOURCES/CVE-2026-34078-11-portal-update-max-fd-after-creating-the-instance-id-pipe.patch @@ -0,0 +1,31 @@ +From d05aeb399ba56e46ab3b7b4eed3669ab98a0df56 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 a4378f77..3110071c 100644 +--- a/portal/flatpak-portal.c ++++ b/portal/flatpak-portal.c +@@ -1075,6 +1075,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/SOURCES/CVE-2026-34078-12-run-fix-fd-tracking-in-flatpak-run-add-app-info-args.patch b/SOURCES/CVE-2026-34078-12-run-fix-fd-tracking-in-flatpak-run-add-app-info-args.patch new file mode 100644 index 0000000..263a706 --- /dev/null +++ b/SOURCES/CVE-2026-34078-12-run-fix-fd-tracking-in-flatpak-run-add-app-info-args.patch @@ -0,0 +1,122 @@ +From 559c415b517c2533cf6b7294fb3f4cf99ed8e2ae 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 | 34 +++++++++++++++++----------------- + 1 file changed, 17 insertions(+), 17 deletions(-) + +diff --git a/common/flatpak-run.c b/common/flatpak-run.c +index a80d0050..a11e26d6 100644 +--- a/common/flatpak-run.c ++++ b/common/flatpak-run.c +@@ -2602,13 +2602,17 @@ 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, + GError **error) + { + g_autofree char *info_path = NULL; + g_autofree char *bwrapinfo_path = NULL; +- int fd, fd2, fd3; ++ glnx_autofd int fd1 = -1; ++ glnx_autofd int fd2 = -1; ++ glnx_autofd int fd3 = -1; ++ int info_fd; ++ glnx_autofd int instance_id_fd = instance_id_fd_arg; + g_autoptr(GKeyFile) keyfile = NULL; + g_autofree char *runtime_path = NULL; + const char *group; +@@ -2754,8 +2758,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), +@@ -2766,7 +2770,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)); +@@ -2774,9 +2777,9 @@ flatpak_run_add_app_info_args (FlatpakBwrap *bwrap, + } + + 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, +@@ -2793,8 +2796,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)); +@@ -2817,10 +2818,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; +@@ -2830,13 +2827,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); +@@ -4697,7 +4695,9 @@ 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, &instance_id_host_dir, ++ &app_info_path, ++ g_steal_fd (&instance_id_fd), ++ &instance_id_host_dir, + error)) + return FALSE; + +-- +2.54.0 + diff --git a/SOURCES/CVE-2026-34078-13-utils-improve-error-message-when-passing-an-fd-numer-which-is-not-a-fd.patch b/SOURCES/CVE-2026-34078-13-utils-improve-error-message-when-passing-an-fd-numer-which-is-not-a-fd.patch new file mode 100644 index 0000000..0675637 --- /dev/null +++ b/SOURCES/CVE-2026-34078-13-utils-improve-error-message-when-passing-an-fd-numer-which-is-not-a-fd.patch @@ -0,0 +1,28 @@ +From 9462b0f980aa0718a41601aaa87125f545c424eb 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 c2015941..938d8131 100644 +--- a/common/flatpak-utils.c ++++ b/common/flatpak-utils.c +@@ -9205,8 +9205,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/SOURCES/CVE-2026-34078-14-run-do-not-close-bind-ro-bind.patch b/SOURCES/CVE-2026-34078-14-run-do-not-close-bind-ro-bind.patch new file mode 100644 index 0000000..20450f5 --- /dev/null +++ b/SOURCES/CVE-2026-34078-14-run-do-not-close-bind-ro-bind.patch @@ -0,0 +1,32 @@ +From 9e145899fe0a28ce79d905b5efabc253b5c8050b 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 7edd7326..701e3454 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/SOURCES/CVE-2026-34078-15-run-use-the-same-fd-validation-for-all-fd-options.patch b/SOURCES/CVE-2026-34078-15-run-use-the-same-fd-validation-for-all-fd-options.patch new file mode 100644 index 0000000..04bb976 --- /dev/null +++ b/SOURCES/CVE-2026-34078-15-run-use-the-same-fd-validation-for-all-fd-options.patch @@ -0,0 +1,95 @@ +From 8708ab052d884cc84e6e71ea012e8d11eba41982 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 701e3454..d97dc0bf 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/SOURCES/CVE-2026-34078-16-run-add-bind-fd-and-ro-bind-fd-binds-after-all-other-binds.patch b/SOURCES/CVE-2026-34078-16-run-add-bind-fd-and-ro-bind-fd-binds-after-all-other-binds.patch new file mode 100644 index 0000000..1354124 --- /dev/null +++ b/SOURCES/CVE-2026-34078-16-run-add-bind-fd-and-ro-bind-fd-binds-after-all-other-binds.patch @@ -0,0 +1,101 @@ +From 3469414dbb068f770b594e6a3f47cd143aff6172 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 | 68 ++++++++++++++++++++++---------------------- + 1 file changed, 34 insertions(+), 34 deletions(-) + +diff --git a/common/flatpak-run.c b/common/flatpak-run.c +index a11e26d6..58b96dc1 100644 +--- a/common/flatpak-run.c ++++ b/common/flatpak-run.c +@@ -4720,40 +4720,6 @@ 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; + +@@ -4789,6 +4755,40 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + "--symlink", "/usr/lib/debug/source", "/run/build-runtime", + NULL); + ++ for (i = 0; bind_fds && i < bind_fds->len; i++) ++ { ++ int fd = g_array_index (bind_fds, int, i); ++ g_autofree char *path = NULL; ++ ++ /* We get the path the fd refers to, to determine to mount point ++ * destination inside the sandbox */ ++ path = 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 (cwd) + flatpak_bwrap_add_args (bwrap, "--chdir", cwd, NULL); + +-- +2.54.0 + diff --git a/SOURCES/CVE-2026-34078-17-portal-use-g-array-index-to-read-from-expose-fds-expose-fds-ro.patch b/SOURCES/CVE-2026-34078-17-portal-use-g-array-index-to-read-from-expose-fds-expose-fds-ro.patch new file mode 100644 index 0000000..5db42ee --- /dev/null +++ b/SOURCES/CVE-2026-34078-17-portal-use-g-array-index-to-read-from-expose-fds-expose-fds-ro.patch @@ -0,0 +1,38 @@ +From dd0a1c25299907b69085aae0fab32becbfca884e 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 3110071c..59a17b2b 100644 +--- a/portal/flatpak-portal.c ++++ b/portal/flatpak-portal.c +@@ -1219,7 +1219,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)); +@@ -1229,7 +1229,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/SOURCES/CVE-2026-34078-18-run-fix-backport-mistake.patch b/SOURCES/CVE-2026-34078-18-run-fix-backport-mistake.patch new file mode 100644 index 0000000..eb62858 --- /dev/null +++ b/SOURCES/CVE-2026-34078-18-run-fix-backport-mistake.patch @@ -0,0 +1,28 @@ +From 37a73712d0e5b20d31fd1a1bd4df1b66e53b32ee 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 d97dc0bf..e174848e 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/SOURCES/CVE-2026-34078-19-run-cope-with-an-empty-runtime.patch b/SOURCES/CVE-2026-34078-19-run-cope-with-an-empty-runtime.patch new file mode 100644 index 0000000..b9e8966 --- /dev/null +++ b/SOURCES/CVE-2026-34078-19-run-cope-with-an-empty-runtime.patch @@ -0,0 +1,100 @@ +From dc40c215b8678a5eabdb3b3ff8556d29c67f8fde 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, 25 insertions(+), 5 deletions(-) + +diff --git a/common/flatpak-run.c b/common/flatpak-run.c +index 58b96dc1..bd6536ac 100644 +--- a/common/flatpak-run.c ++++ b/common/flatpak-run.c +@@ -2842,6 +2842,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) +@@ -2853,14 +2857,19 @@ add_tzdata_args (FlatpakBwrap *bwrap, + g_autofree char *runtime_zoneinfo = NULL; + 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); + +- 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); ++ } + + runtime_zoneinfo = g_strconcat ("share/zoneinfo/", raw_timezone, NULL); + +@@ -3370,6 +3379,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, +@@ -3423,6 +3436,10 @@ flatpak_run_setup_usr_links (FlatpakBwrap *bwrap, + } + } + ++/* ++ * @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, +@@ -3439,6 +3456,8 @@ flatpak_run_setup_base_argv (FlatpakBwrap *bwrap, + gulong pers; + gid_t gid = getgid (); + ++ g_return_val_if_fail (runtime_fd >= -1, FALSE); ++ + run_dir = g_strdup_printf ("/run/user/%d", getuid ()); + + passwd_contents = g_strdup_printf ("%s:x:%d:%d:%s:%s:%s\n" +@@ -3512,7 +3531,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/SOURCES/CVE-2026-34078-2-utils-add-flatpak-parse-fd.patch b/SOURCES/CVE-2026-34078-2-utils-add-flatpak-parse-fd.patch new file mode 100644 index 0000000..2a198d4 --- /dev/null +++ b/SOURCES/CVE-2026-34078-2-utils-add-flatpak-parse-fd.patch @@ -0,0 +1,94 @@ +From 8ce576a3b950d6ab735a5e049342d91b36685f94 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 53b79807..992243e3 100644 +--- a/common/flatpak-context.c ++++ b/common/flatpak-context.c +@@ -1304,21 +1304,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 f79b22c8..754fe412 100644 +--- a/common/flatpak-utils-private.h ++++ b/common/flatpak-utils-private.h +@@ -927,6 +927,9 @@ void flatpak_print_escaped_string (const char *s, + gboolean flatpak_validate_path_characters (const char *path, + GError **error); + ++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 0ab84064..c2015941 100644 +--- a/common/flatpak-utils.c ++++ b/common/flatpak-utils.c +@@ -9188,3 +9188,25 @@ flatpak_validate_path_characters (const char *path, + + return TRUE; + } ++ ++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/SOURCES/CVE-2026-34078-20-dir-in-apply-extra-data-don-t-assume-there-is-always-a-runtime.patch b/SOURCES/CVE-2026-34078-20-dir-in-apply-extra-data-don-t-assume-there-is-always-a-runtime.patch new file mode 100644 index 0000000..19d98e9 --- /dev/null +++ b/SOURCES/CVE-2026-34078-20-dir-in-apply-extra-data-don-t-assume-there-is-always-a-runtime.patch @@ -0,0 +1,44 @@ +From 630fcba57765ac64a0cebc31257e63a5bc530961 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 94d2d4f2..adc9bf62 100644 +--- a/common/flatpak-dir.c ++++ b/common/flatpak-dir.c +@@ -8015,10 +8015,14 @@ apply_extra_data (FlatpakDir *self, + NULL); + + 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, + /* Might need multiarch in apply_extra (see e.g. #3742). Should be pretty safe in this limited context */ +-- +2.54.0 + diff --git a/SOURCES/CVE-2026-34078-21-utils-add-flatpak-set-cloexec.patch b/SOURCES/CVE-2026-34078-21-utils-add-flatpak-set-cloexec.patch new file mode 100644 index 0000000..6f8f57c --- /dev/null +++ b/SOURCES/CVE-2026-34078-21-utils-add-flatpak-set-cloexec.patch @@ -0,0 +1,52 @@ +From 0ac58a6734a3d5f6a0445cfe3310596e382c092b 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 754fe412..fd819c71 100644 +--- a/common/flatpak-utils-private.h ++++ b/common/flatpak-utils-private.h +@@ -932,4 +932,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 938d8131..446b3325 100644 +--- a/common/flatpak-utils.c ++++ b/common/flatpak-utils.c +@@ -9210,3 +9210,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/SOURCES/CVE-2026-34078-22-run-context-mark-fd-arguments-as-close-on-exec.patch b/SOURCES/CVE-2026-34078-22-run-context-mark-fd-arguments-as-close-on-exec.patch new file mode 100644 index 0000000..3c6b4d1 --- /dev/null +++ b/SOURCES/CVE-2026-34078-22-run-context-mark-fd-arguments-as-close-on-exec.patch @@ -0,0 +1,102 @@ +From 5807aa3d7712d2fb5c294056dd90431cd863fded 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 e174848e..cc69423e 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 992243e3..3ff4ac16 100644 +--- a/common/flatpak-context.c ++++ b/common/flatpak-context.c +@@ -1313,6 +1313,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/SOURCES/CVE-2026-34078-23-utils-move-flatpak-get-path-for-fd-to-here.patch b/SOURCES/CVE-2026-34078-23-utils-move-flatpak-get-path-for-fd-to-here.patch new file mode 100644 index 0000000..b8cdc3f --- /dev/null +++ b/SOURCES/CVE-2026-34078-23-utils-move-flatpak-get-path-for-fd-to-here.patch @@ -0,0 +1,179 @@ +From 5bd5827412a440b074f9f79ae34073e2554ad76c 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 bd6536ac..8ce6978c 100644 +--- a/common/flatpak-run.c ++++ b/common/flatpak-run.c +@@ -4108,42 +4108,6 @@ check_sudo (GError **error) + return TRUE; + } + +-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, +@@ -4354,7 +4318,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, +@@ -4476,7 +4440,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"); + +@@ -4782,7 +4746,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; + +@@ -4799,7 +4763,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 fd819c71..efe8eb2b 100644 +--- a/common/flatpak-utils-private.h ++++ b/common/flatpak-utils-private.h +@@ -930,6 +930,9 @@ gboolean flatpak_validate_path_characters (const char *path, + 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 446b3325..6f09fa09 100644 +--- a/common/flatpak-utils.c ++++ b/common/flatpak-utils.c +@@ -9227,3 +9227,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/SOURCES/CVE-2026-34078-24-portal-avoid-crash-if-sandbox-expose-ro-fd-is-out-of-range.patch b/SOURCES/CVE-2026-34078-24-portal-avoid-crash-if-sandbox-expose-ro-fd-is-out-of-range.patch new file mode 100644 index 0000000..662a296 --- /dev/null +++ b/SOURCES/CVE-2026-34078-24-portal-avoid-crash-if-sandbox-expose-ro-fd-is-out-of-range.patch @@ -0,0 +1,66 @@ +From 0663652213e742df38528f6c929789189831d40a 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 59a17b2b..c7310f89 100644 +--- a/portal/flatpak-portal.c ++++ b/portal/flatpak-portal.c +@@ -1172,8 +1172,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]); + } +@@ -1198,8 +1207,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/SOURCES/CVE-2026-34078-25-portal-log-and-ignore-unusable-sandbox-expose-fds-instead-of-erroring.patch b/SOURCES/CVE-2026-34078-25-portal-log-and-ignore-unusable-sandbox-expose-fds-instead-of-erroring.patch new file mode 100644 index 0000000..d665d2d --- /dev/null +++ b/SOURCES/CVE-2026-34078-25-portal-log-and-ignore-unusable-sandbox-expose-fds-instead-of-erroring.patch @@ -0,0 +1,65 @@ +From db8a36948a0b9d7bc0b7572a969c148f4a704ed4 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 c7310f89..9f8c7918 100644 +--- a/portal/flatpak-portal.c ++++ b/portal/flatpak-portal.c +@@ -1188,12 +1188,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); + } + } + } +@@ -1223,12 +1220,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/SOURCES/CVE-2026-34078-26-portal-reinstate-flatpak-get-path-for-fd-checks.patch b/SOURCES/CVE-2026-34078-26-portal-reinstate-flatpak-get-path-for-fd-checks.patch new file mode 100644 index 0000000..eab86dc --- /dev/null +++ b/SOURCES/CVE-2026-34078-26-portal-reinstate-flatpak-get-path-for-fd-checks.patch @@ -0,0 +1,69 @@ +From df7cfbbed0a0576c35f236ab6ba3620cbfeb900f 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 9f8c7918..185ed676 100644 +--- a/portal/flatpak-portal.c ++++ b/portal/flatpak-portal.c +@@ -556,7 +556,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); +@@ -575,6 +577,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/SOURCES/CVE-2026-34078-27-libtest-allow-adding-a-new-ref-to-an-existing-temporary-ostree-repo.patch b/SOURCES/CVE-2026-34078-27-libtest-allow-adding-a-new-ref-to-an-existing-temporary-ostree-repo.patch new file mode 100644 index 0000000..dabf173 --- /dev/null +++ b/SOURCES/CVE-2026-34078-27-libtest-allow-adding-a-new-ref-to-an-existing-temporary-ostree-repo.patch @@ -0,0 +1,37 @@ +From 313ceb0cac8d28f941421fabb24e7371b2f3cc8d 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 4a9fd481..5dc4011e 100644 +--- a/tests/libtest.sh ++++ b/tests/libtest.sh +@@ -302,7 +302,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/SOURCES/CVE-2026-34078-28-app-context-never-close-fds-0-1-or-2.patch b/SOURCES/CVE-2026-34078-28-app-context-never-close-fds-0-1-or-2.patch new file mode 100644 index 0000000..20030db --- /dev/null +++ b/SOURCES/CVE-2026-34078-28-app-context-never-close-fds-0-1-or-2.patch @@ -0,0 +1,103 @@ +From 263497107d68e5aa44856b6d1d375fd1adf77879 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 cc69423e..82725119 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 3ff4ac16..3dc8f533 100644 +--- a/common/flatpak-context.c ++++ b/common/flatpak-context.c +@@ -1311,7 +1311,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/SOURCES/CVE-2026-34078-29-app-context-factor-out-flatpak-accept-fd-argument.patch b/SOURCES/CVE-2026-34078-29-app-context-factor-out-flatpak-accept-fd-argument.patch new file mode 100644 index 0000000..b5fb081 --- /dev/null +++ b/SOURCES/CVE-2026-34078-29-app-context-factor-out-flatpak-accept-fd-argument.patch @@ -0,0 +1,235 @@ +From 9c357f7f9d4d781bf04dafab9559343e9ec44f55 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 82725119..c9e7e071 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 3dc8f533..2165631d 100644 +--- a/common/flatpak-context.c ++++ b/common/flatpak-context.c +@@ -1306,25 +1306,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 efe8eb2b..215b3c38 100644 +--- a/common/flatpak-utils-private.h ++++ b/common/flatpak-utils-private.h +@@ -937,4 +937,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 6f09fa09..63a2e325 100644 +--- a/common/flatpak-utils.c ++++ b/common/flatpak-utils.c +@@ -9228,6 +9228,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/SOURCES/CVE-2026-34078-3-flatpak-bwrap-use-glnx-close-fd-as-clear-func.patch b/SOURCES/CVE-2026-34078-3-flatpak-bwrap-use-glnx-close-fd-as-clear-func.patch new file mode 100644 index 0000000..94dcd45 --- /dev/null +++ b/SOURCES/CVE-2026-34078-3-flatpak-bwrap-use-glnx-close-fd-as-clear-func.patch @@ -0,0 +1,48 @@ +From ba43f074af594705b0aea9f53261d3b61fc3c866 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 1694ffd4..693a1981 100644 +--- a/common/flatpak-bwrap.c ++++ b/common/flatpak-bwrap.c +@@ -41,15 +41,6 @@ + #include "flatpak-utils-private.h" + #include "flatpak-utils-base-private.h" + +-static void +-clear_fd (gpointer data) +-{ +- int *fd_p = data; +- +- if (fd_p != NULL && *fd_p != -1) +- close (*fd_p); +-} +- + char *flatpak_bwrap_empty_env[] = { NULL }; + + FlatpakBwrap * +@@ -59,9 +50,9 @@ 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/SOURCES/CVE-2026-34078-4-run-use-o-path-fds-for-the-runtime-and-app-deploy-directories.patch b/SOURCES/CVE-2026-34078-4-run-use-o-path-fds-for-the-runtime-and-app-deploy-directories.patch new file mode 100644 index 0000000..9cf7c40 --- /dev/null +++ b/SOURCES/CVE-2026-34078-4-run-use-o-path-fds-for-the-runtime-and-app-deploy-directories.patch @@ -0,0 +1,912 @@ +From f41f12c98896aced62830bf5f3e21c40ee24a6d7 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/Makefile.am.inc | 2 +- + common/flatpak-dir.c | 8 +- + common/flatpak-installation.c | 3 +- + common/flatpak-run-private.h | 11 +- + common/flatpak-run.c | 452 +++++++++++++++++++++++----------- + 7 files changed, 372 insertions(+), 149 deletions(-) + +diff --git a/app/flatpak-builtins-build.c b/app/flatpak-builtins-build.c +index 4a1e762..09b469a 100644 +--- a/app/flatpak-builtins-build.c ++++ b/app/flatpak-builtins-build.c +@@ -457,7 +457,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 ba2ae67..9403d8a 100644 +--- a/app/flatpak-builtins-run.c ++++ b/app/flatpak-builtins-run.c +@@ -110,6 +110,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; + + context = g_option_context_new (_("APP [ARGUMENT…] - Run an app")); + g_option_context_set_translation_domain (context, GETTEXT_PACKAGE); +@@ -305,14 +307,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/Makefile.am.inc b/common/Makefile.am.inc +index 892ee4c..3147cc7 100644 +--- a/common/Makefile.am.inc ++++ b/common/Makefile.am.inc +@@ -205,6 +205,7 @@ libflatpak_common_la_CFLAGS = \ + $(NULL) + libflatpak_common_la_LIBADD = \ + $(AM_LIBADD) \ ++ libglnx.la \ + $(ARCHIVE_LIBS) \ + $(ZSTD_LIBS) \ + $(BASE_LIBS) \ +@@ -249,7 +250,6 @@ libflatpak_la_LIBADD = \ + $(AM_LIBADD) \ + libflatpak-common.la \ + libflatpak-common-base.la \ +- libglnx.la \ + $(BASE_LIBS) \ + $(OSTREE_LIBS) \ + $(SOUP_LIBS) \ +diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c +index 978df03..c18fd44 100644 +--- a/common/flatpak-dir.c ++++ b/common/flatpak-dir.c +@@ -8014,7 +8014,13 @@ apply_extra_data (FlatpakDir *self, + "--cap-drop", "ALL", + NULL); + +- 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, + /* Might need multiarch in apply_extra (see e.g. #3742). Should be pretty safe in this limited context */ + FLATPAK_RUN_FLAG_MULTIARCH | + FLATPAK_RUN_FLAG_NO_SESSION_HELPER | FLATPAK_RUN_FLAG_NO_PROC, +diff --git a/common/flatpak-installation.c b/common/flatpak-installation.c +index 3c5a12a..9254252 100644 +--- a/common/flatpak-installation.c ++++ b/common/flatpak-installation.c +@@ -700,9 +700,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 eac25cc..5fc1f29 100644 +--- a/common/flatpak-run-private.h ++++ b/common/flatpak-run-private.h +@@ -28,6 +28,11 @@ + #include "flatpak-utils-private.h" + #include "flatpak-exports-private.h" + ++#define FLATPAK_RUN_APP_DEPLOY_APP_ORIGINAL (-2) ++#define FLATPAK_RUN_APP_DEPLOY_APP_EMPTY (-3) ++ ++#define FLATPAK_RUN_APP_DEPLOY_USR_ORIGINAL (-2) ++ + gboolean flatpak_run_in_transient_unit (const char *app_id, + GError **error); + +@@ -151,7 +156,7 @@ 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, +@@ -181,12 +186,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_runtime_fd, + int parent_pid, + FlatpakRunFlags flags, + const char *cwd, +diff --git a/common/flatpak-run.c b/common/flatpak-run.c +index 4fdb56f..0e1d16f 100644 +--- a/common/flatpak-run.c ++++ b/common/flatpak-run.c +@@ -2846,19 +2846,37 @@ 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; ++ glnx_autofd int zoneinfo_fd = -1; ++ g_autofree char *runtime_zoneinfo = NULL; ++ g_autoptr(GError) error = NULL; ++ ++ raw_timezone = flatpak_get_timezone (); ++ timezone_content = g_strdup_printf ("%s\n", raw_timezone); ++ localtime_content = g_strconcat ("../usr/share/zoneinfo/", raw_timezone, NULL); ++ ++ zoneinfo_fd = glnx_chaseat (runtime_fd, "share/zoneinfo", ++ GLNX_CHASE_RESOLVE_BENEATH | ++ GLNX_CHASE_MUST_BE_DIRECTORY, ++ NULL); + +- if (runtime_files) +- runtime_zoneinfo = g_file_resolve_relative_path (runtime_files, "share/zoneinfo"); ++ runtime_zoneinfo = g_strconcat ("share/zoneinfo/", raw_timezone, NULL); + + /* Check for runtime /usr/share/zoneinfo */ +- if (runtime_zoneinfo != NULL && g_file_query_exists (runtime_zoneinfo, NULL)) ++ if (zoneinfo_fd >= 0) + { ++ glnx_autofd int runtime_zoneinfo_fd = -1; ++ ++ /* 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); ++ + /* Check for host /usr/share/zoneinfo */ + if (g_file_test ("/usr/share/zoneinfo", G_FILE_TEST_IS_DIR)) + { +@@ -2868,15 +2886,12 @@ add_tzdata_args (FlatpakBwrap *bwrap, + "--symlink", localtime_content, "/etc/localtime", + NULL); + } +- else ++ /* Check if host timezone file exist in the runtime tzdata */ ++ else if (runtime_zoneinfo_fd >= 0) + { +- 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 (bwrap, ++ "--symlink", localtime_content, "/etc/localtime", ++ NULL); + } + } + +@@ -3359,24 +3374,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_debug ("%s does not exist in runtime", subdir); ++ } ++ else + { + g_autofree char *link = g_strconcat ("usr", subdir, NULL); + g_autofree char *create = NULL; +@@ -3390,17 +3422,12 @@ flatpak_run_setup_usr_links (FlatpakBwrap *bwrap, + "--symlink", link, create, + NULL); + } +- else +- { +- g_debug ("%s does not exist", +- flatpak_file_get_path_cached (runtime_subdir)); +- } + } + } + + gboolean + flatpak_run_setup_base_argv (FlatpakBwrap *bwrap, +- GFile *runtime_files, ++ int runtime_fd, + GFile *app_id_dir, + const char *arch, + FlatpakRunFlags flags, +@@ -3413,7 +3440,6 @@ flatpak_run_setup_base_argv (FlatpakBwrap *bwrap, + struct group *g; + gulong pers; + gid_t gid = getgid (); +- g_autoptr(GFile) etc = NULL; + + run_dir = g_strdup_printf ("/run/user/%d", getuid ()); + +@@ -3488,22 +3514,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; +@@ -3520,9 +3549,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; + +@@ -3533,9 +3572,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); + } + } + } +@@ -3556,9 +3598,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; + +@@ -3785,7 +3827,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) +@@ -3825,7 +3867,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) + { +@@ -4048,15 +4090,51 @@ check_sudo (GError **error) + return TRUE; + } + ++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, +@@ -4071,11 +4149,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; +@@ -4107,18 +4180,39 @@ 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_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); ++ + if (!check_sudo (error)) + return FALSE; + +@@ -4231,38 +4325,53 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + flatpak_context_merge (app_context, extra_context); + + original_runtime_files = flatpak_deploy_get_files (runtime_deploy); ++ original_runtime_fd = open (flatpak_file_get_path_cached (original_runtime_files), ++ O_PATH | O_CLOEXEC); ++ if (original_runtime_fd < 0) ++ return glnx_throw_errno_prefix (error, "Failed to open original runtime"); + +- if (custom_usr_path != NULL) ++ if (custom_runtime_fd >= 0) + { +- 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); +@@ -4343,19 +4452,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; + +- /* Mount the original app below here */ +- app_target_path = "/run/parent/app"; ++ 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 (); + } +- else if (original_app_files != NULL) ++ ++ /* 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) + { +- app_files = g_object_ref (original_app_files); ++ use_ld_so_cache = FALSE; ++ } ++ else ++ { ++ glnx_autofd int ldconfig_fd = -1; ++ ++ ldconfig_fd = glnx_chaseat (runtime_fd, "bin/ldconfig", ++ GLNX_CHASE_RESOLVE_BENEATH | ++ GLNX_CHASE_MUST_BE_REGULAR, ++ &my_error); ++ if (ldconfig_fd < 0) ++ { ++ use_ld_so_cache = FALSE; ++ g_debug ("bin/ldconfig not found in runtime: %s", my_error->message); ++ } ++ ++ g_clear_error (&my_error); + } + + flatpak_run_apply_env_default (bwrap, use_ld_so_cache); +@@ -4368,75 +4518,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 + { +@@ -4445,7 +4606,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, +@@ -4458,26 +4619,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) */ +@@ -4489,7 +4661,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) +@@ -4499,7 +4671,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/SOURCES/CVE-2026-34078-5-run-add-usr-fd-and-app-fd-options.patch b/SOURCES/CVE-2026-34078-5-run-add-usr-fd-and-app-fd-options.patch new file mode 100644 index 0000000..84d18d6 --- /dev/null +++ b/SOURCES/CVE-2026-34078-5-run-add-usr-fd-and-app-fd-options.patch @@ -0,0 +1,78 @@ +From 7140fee959d82d1c5913167398e6d559f3a13ee3 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 9403d8ae..a2d71420 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 } + }; + +@@ -307,7 +311,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) + { +@@ -326,7 +341,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/SOURCES/CVE-2026-34078-6-run-add-ro-bind-fds-to-flatpak-run-app.patch b/SOURCES/CVE-2026-34078-6-run-add-ro-bind-fds-to-flatpak-run-app.patch new file mode 100644 index 0000000..3f3e175 --- /dev/null +++ b/SOURCES/CVE-2026-34078-6-run-add-ro-bind-fds-to-flatpak-run-app.patch @@ -0,0 +1,116 @@ +From 3a55408e13066f39f78158d3026ea3581f0b28c6 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 a2d71420..7190f1f7 100644 +--- a/app/flatpak-builtins-run.c ++++ b/app/flatpak-builtins-run.c +@@ -380,6 +380,8 @@ flatpak_builtin_run (int argc, char **argv, GCancellable *cancellable, GError ** + rest_argc - 1, + opt_instance_id_fd, + NULL, ++ NULL, ++ NULL, + cancellable, + error)) + return FALSE; +diff --git a/common/flatpak-installation.c b/common/flatpak-installation.c +index 9254252d..6eb16459 100644 +--- a/common/flatpak-installation.c ++++ b/common/flatpak-installation.c +@@ -710,6 +710,7 @@ flatpak_installation_launch_full (FlatpakInstallation *self, + NULL, + NULL, 0, -1, + &instance_dir, ++ NULL, NULL, + cancellable, error)) + return FALSE; + +diff --git a/common/flatpak-run-private.h b/common/flatpak-run-private.h +index 5fc1f296..23477bd8 100644 +--- a/common/flatpak-run-private.h ++++ b/common/flatpak-run-private.h +@@ -200,6 +200,8 @@ gboolean flatpak_run_app (FlatpakDecomposed *app_ref, + int n_args, + int instance_id_fd, + 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 0e1d16f9..9087be23 100644 +--- a/common/flatpak-run.c ++++ b/common/flatpak-run.c +@@ -4143,6 +4143,8 @@ flatpak_run_app (FlatpakDecomposed *app_ref, + int n_args, + int instance_id_fd, + char **instance_dir_out, ++ GArray *bind_fds, ++ GArray *ro_bind_fds, + GCancellable *cancellable, + GError **error) + { +@@ -4717,6 +4719,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/SOURCES/CVE-2026-34078-7-run-add-ro-bind-fd-options.patch b/SOURCES/CVE-2026-34078-7-run-add-ro-bind-fd-options.patch new file mode 100644 index 0000000..8b4e5c2 --- /dev/null +++ b/SOURCES/CVE-2026-34078-7-run-add-ro-bind-fd-options.patch @@ -0,0 +1,95 @@ +From 9ca818511d96dadbdb7b8deee1f8ef99f10914e6 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 7190f1f7..7edd7326 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 } + }; + +@@ -117,6 +159,9 @@ flatpak_builtin_run (int argc, char **argv, GCancellable *cancellable, GError ** + glnx_autofd int app_fd = -1; + glnx_autofd int usr_fd = -1; + ++ opt_bind_fds = g_array_new (FALSE, FALSE, sizeof (int)); ++ opt_ro_bind_fds = g_array_new (FALSE, FALSE, sizeof (int)); ++ + context = g_option_context_new (_("APP [ARGUMENT…] - Run an app")); + g_option_context_set_translation_domain (context, GETTEXT_PACKAGE); + +@@ -380,8 +425,8 @@ flatpak_builtin_run (int argc, char **argv, GCancellable *cancellable, GError ** + rest_argc - 1, + opt_instance_id_fd, + NULL, +- NULL, +- NULL, ++ opt_bind_fds, ++ opt_ro_bind_fds, + cancellable, + error)) + return FALSE; +-- +2.54.0 + diff --git a/SOURCES/CVE-2026-34078-8-portal-use-bind-fd-app-fd-and-usr-fd-options-to-avoid-races.patch b/SOURCES/CVE-2026-34078-8-portal-use-bind-fd-app-fd-and-usr-fd-options-to-avoid-races.patch new file mode 100644 index 0000000..9652eac --- /dev/null +++ b/SOURCES/CVE-2026-34078-8-portal-use-bind-fd-app-fd-and-usr-fd-options-to-avoid-races.patch @@ -0,0 +1,601 @@ +From 9958b547bea1e9335cb91af61a239d1a7039387e 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 e5701895..a4378f77 100644 +--- a/portal/flatpak-portal.c ++++ b/portal/flatpak-portal.c +@@ -550,195 +550,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 +@@ -793,9 +658,12 @@ 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_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; +@@ -918,29 +786,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_debug ("exposing %s", expose); +- if (!is_valid_expose (expose, &error)) +- { +- g_dbus_method_invocation_return_gerror (invocation, error); +- return G_DBUS_METHOD_INVOCATION_HANDLED; +- } +- } +- +- for (i = 0; sandbox_expose_ro != NULL && sandbox_expose_ro[i] != NULL; i++) +- { +- const char *expose = sandbox_expose_ro[i]; +- g_debug ("exposing %s", expose); +- if (!is_valid_expose (expose, &error)) +- { +- g_dbus_method_invocation_return_gerror (invocation, error); +- return G_DBUS_METHOD_INVOCATION_HANDLED; +- } +- } +- + g_debug ("Running spawn command %s", arg_argv[0]); + + n_fds = 0; +@@ -1112,10 +957,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, +@@ -1126,16 +975,12 @@ handle_spawn (PortalFlatpak *object, + } + + env_fd = glnx_steal_fd (&env_tmpf.fd); ++ g_array_append_val (owned_fds, env_fd); + +- /* Use a fd that hasn't been used yet. We might have to reshuffle +- * fd_map_entry.to, a bit later. */ +- fd_map_entry.from = env_fd; +- fd_map_entry.to = ++max_fd; +- fd_map_entry.final = fd_map_entry.to; +- g_array_append_val (fd_map, fd_map_entry); ++ remapped_fd = fd_map_remap_fd (fd_map, &max_fd, env_fd); + + g_ptr_array_add (flatpak_argv, +- g_strdup_printf ("--env-fd=%d", fd_map_entry.final)); ++ g_strdup_printf ("--env-fd=%d", remapped_fd)); + } + + for (i = 0; unset_env != NULL && unset_env[i] != NULL; i++) +@@ -1243,54 +1088,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_debug ("exposing %s", expose); ++ int expose_fd; ++ ++ g_assert (instance_sandbox_fd >= 0); ++ ++ expose_fd = glnx_chaseat (instance_sandbox_fd, sandbox_expose_ro[i], ++ GLNX_CHASE_RESOLVE_NO_SYMLINKS | ++ GLNX_CHASE_RESOLVE_BENEATH, ++ &error); ++ if (expose_fd < 0) ++ { ++ g_dbus_method_invocation_return_gerror (invocation, error); ++ return G_DBUS_METHOD_INVOCATION_HANDLED; ++ } ++ ++ g_array_append_val (expose_fds_ro, expose_fd); ++ /* transfers ownership, can't g_steal_fd with g_array_append_val */ ++ g_array_append_val (owned_fds, expose_fd); + } + + if (sandbox_expose_fd != NULL) + { + gsize len = g_variant_n_children (sandbox_expose_fd); ++ + for (i = 0; i < len; i++) + { + gint32 handle; ++ + g_variant_get_child (sandbox_expose_fd, i, "h", &handle); +- if (handle >= 0 && handle < fds_len) ++ 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_debug ("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; + } +@@ -1300,31 +1191,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_debug ("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", +@@ -1334,20 +1214,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) + { +@@ -1359,18 +1259,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_debug ("Using %s as /app instead of app", path); +- g_ptr_array_add (flatpak_argv, g_strdup_printf ("--app-path=%s", path)); ++ g_ptr_array_add (flatpak_argv, g_strdup_printf ("--app-fd=%d", ++ remapped_fd)); + } + else if (empty_app) + { +@@ -1379,8 +1272,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) + { +@@ -1392,18 +1285,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_debug ("Using %s as /usr instead of runtime", path); +- g_ptr_array_add (flatpak_argv, g_strdup_printf ("--usr-path=%s", path)); ++ g_ptr_array_add (flatpak_argv, g_strdup_printf ("--usr-fd=%d", ++ remapped_fd)); + } + + g_ptr_array_add (flatpak_argv, g_strdup_printf ("--runtime=%s", runtime_parts[1])); +-- +2.54.0 + diff --git a/SOURCES/CVE-2026-34078-9-run-fix-checking-wrong-variable-in-runtime-fd-selection.patch b/SOURCES/CVE-2026-34078-9-run-fix-checking-wrong-variable-in-runtime-fd-selection.patch new file mode 100644 index 0000000..f65826b --- /dev/null +++ b/SOURCES/CVE-2026-34078-9-run-fix-checking-wrong-variable-in-runtime-fd-selection.patch @@ -0,0 +1,38 @@ +From 8d1e978c6a582b1603dccb7fe9340e15787f6297 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 9087be23..4ffc5fa3 100644 +--- a/common/flatpak-run.c ++++ b/common/flatpak-run.c +@@ -4350,7 +4350,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/SOURCES/CVE-2026-34079-1-utils-only-remove-cached-files-in-the-cache-directory.patch b/SOURCES/CVE-2026-34079-1-utils-only-remove-cached-files-in-the-cache-directory.patch new file mode 100644 index 0000000..92b2d0c --- /dev/null +++ b/SOURCES/CVE-2026-34079-1-utils-only-remove-cached-files-in-the-cache-directory.patch @@ -0,0 +1,90 @@ +From a13a7415517cbf1d5073bde05e5b9c3c95de6b16 Mon Sep 17 00:00:00 2001 +From: Sebastian Wick +Date: Fri, 9 Jan 2026 19:24:44 +0100 +Subject: [PATCH] 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 c97905c8188ddaad01ee146b57bba6c3fa294113): +--- + common/flatpak-utils.c | 36 +++++++++++++++++++++++++++++++++--- + 1 file changed, 33 insertions(+), 3 deletions(-) + +diff --git a/common/flatpak-utils.c b/common/flatpak-utils.c +index 999d88fd..0ab84064 100644 +--- a/common/flatpak-utils.c ++++ b/common/flatpak-utils.c +@@ -1344,6 +1344,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 +@@ -1353,6 +1369,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, +@@ -1396,10 +1415,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) +-- +2.54.0 + diff --git a/SOURCES/CVE-2026-34079-2-utils-do-not-follow-symlinks-in-local-open-file.patch b/SOURCES/CVE-2026-34079-2-utils-do-not-follow-symlinks-in-local-open-file.patch new file mode 100644 index 0000000..df208c8 --- /dev/null +++ b/SOURCES/CVE-2026-34079-2-utils-do-not-follow-symlinks-in-local-open-file.patch @@ -0,0 +1,50 @@ +From 6a85e0591b6fbc01d9afe6d81bf6748ce2f3e8ac 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 4a678f463b455c585d38ac4cf4d994e7ce710f8e): +--- + 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 2dbd46b5..56853142 100644 +--- a/common/flatpak-oci-registry.c ++++ b/common/flatpak-oci-registry.c +@@ -251,6 +251,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, +@@ -262,7 +265,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/SOURCES/CVE-2026-34079-3-system-helper-only-remove-an-ongoing-pull-if-users-match.patch b/SOURCES/CVE-2026-34079-3-system-helper-only-remove-an-ongoing-pull-if-users-match.patch new file mode 100644 index 0000000..44a933b --- /dev/null +++ b/SOURCES/CVE-2026-34079-3-system-helper-only-remove-an-ongoing-pull-if-users-match.patch @@ -0,0 +1,152 @@ +From 640c9c2725f67220dd25a5148d6f2bb6fc7ddcf5 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. + +(cherry picked from commit a27ec46e8c0ab0ae162f2aa3142dccb6b79d9211): +--- + 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 03410e84..5aa3e2ab 100644 +--- a/system-helper/flatpak-system-helper.c ++++ b/system-helper/flatpak-system-helper.c +@@ -355,23 +355,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); +@@ -423,6 +431,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, +@@ -430,30 +441,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, +@@ -735,31 +733,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/SOURCES/flatpak-1.12.x-update-libglnx-for-glnx-chaseseat.patch b/SOURCES/flatpak-1.12.x-update-libglnx-for-glnx-chaseseat.patch new file mode 100644 index 0000000..77f8b0e --- /dev/null +++ b/SOURCES/flatpak-1.12.x-update-libglnx-for-glnx-chaseseat.patch @@ -0,0 +1,6134 @@ +From 80fc0bcefe2b667f337745dfac1508ee04d4455c Mon Sep 17 00:00:00 2001 +From: Jan Grulich +Date: Wed, 20 May 2026 14:09:05 +0200 +Subject: [PATCH] Update libglnx for glnx_chaseat + +--- + subprojects/libglnx/Makefile-libglnx.am | 92 ++ + .../libglnx/glnx-backport-autocleanups.h | 1 + + subprojects/libglnx/glnx-backport-autoptr.h | 1 + + subprojects/libglnx/glnx-backport-testutils.c | 162 ++++ + subprojects/libglnx/glnx-backport-testutils.h | 209 +++++ + subprojects/libglnx/glnx-backports.c | 519 +++++++++++- + subprojects/libglnx/glnx-backports.h | 141 +++- + subprojects/libglnx/glnx-chase.c | 789 ++++++++++++++++++ + subprojects/libglnx/glnx-chase.h | 51 ++ + subprojects/libglnx/glnx-console.c | 7 +- + subprojects/libglnx/glnx-console.h | 1 + + subprojects/libglnx/glnx-dirfd.c | 25 +- + subprojects/libglnx/glnx-dirfd.h | 2 + + subprojects/libglnx/glnx-errors.c | 3 +- + subprojects/libglnx/glnx-errors.h | 15 +- + subprojects/libglnx/glnx-fdio.c | 131 ++- + subprojects/libglnx/glnx-fdio.h | 46 +- + subprojects/libglnx/glnx-local-alloc.c | 3 +- + subprojects/libglnx/glnx-local-alloc.h | 17 +- + subprojects/libglnx/glnx-lockfile.c | 7 +- + subprojects/libglnx/glnx-lockfile.h | 3 +- + subprojects/libglnx/glnx-macros.h | 5 +- + subprojects/libglnx/glnx-missing-syscall.h | 437 +++++++++- + subprojects/libglnx/glnx-missing.h | 9 + + subprojects/libglnx/glnx-shutil.c | 7 +- + subprojects/libglnx/glnx-shutil.h | 1 + + subprojects/libglnx/glnx-xattrs.c | 13 +- + subprojects/libglnx/glnx-xattrs.h | 1 + + subprojects/libglnx/libglnx.doap | 35 + + subprojects/libglnx/libglnx.h | 3 + + subprojects/libglnx/libglnx.m4 | 8 + + subprojects/libglnx/meson.build | 115 +++ + subprojects/libglnx/meson_options.txt | 9 + + subprojects/libglnx/tests/libglnx-testlib.c | 6 +- + subprojects/libglnx/tests/libglnx-testlib.h | 1 + + subprojects/libglnx/tests/meson.build | 59 ++ + .../libglnx/tests/test-libglnx-backports.c | 299 +++++++ + .../libglnx/tests/test-libglnx-chase.c | 609 ++++++++++++++ + .../libglnx/tests/test-libglnx-errors.c | 3 +- + subprojects/libglnx/tests/test-libglnx-fdio.c | 207 ++++- + .../libglnx/tests/test-libglnx-macros.c | 11 +- + .../libglnx/tests/test-libglnx-shutil.c | 45 +- + .../libglnx/tests/test-libglnx-testing.c | 371 ++++++++ + .../libglnx/tests/test-libglnx-xattrs.c | 7 +- + subprojects/libglnx/tests/testing-helper.c | 342 ++++++++ + .../tests/use-as-subproject/.gitignore | 5 + + .../libglnx/tests/use-as-subproject/README | 8 + + .../libglnx/tests/use-as-subproject/config.h | 6 + + .../tests/use-as-subproject/dummy-config.h.in | 6 + + .../tests/use-as-subproject/meson.build | 48 ++ + .../libglnx/tests/use-as-subproject/trivial.c | 15 + + .../tests/use-as-subproject/use-libglnx.c | 16 + + .../tests/use-as-subproject/use-testlib.c | 17 + + 53 files changed, 4862 insertions(+), 87 deletions(-) + create mode 100644 subprojects/libglnx/Makefile-libglnx.am + create mode 100644 subprojects/libglnx/glnx-backport-testutils.c + create mode 100644 subprojects/libglnx/glnx-backport-testutils.h + create mode 100644 subprojects/libglnx/glnx-chase.c + create mode 100644 subprojects/libglnx/glnx-chase.h + create mode 100644 subprojects/libglnx/libglnx.doap + create mode 100644 subprojects/libglnx/meson.build + create mode 100644 subprojects/libglnx/meson_options.txt + create mode 100644 subprojects/libglnx/tests/meson.build + create mode 100644 subprojects/libglnx/tests/test-libglnx-backports.c + create mode 100644 subprojects/libglnx/tests/test-libglnx-chase.c + create mode 100644 subprojects/libglnx/tests/test-libglnx-testing.c + create mode 100644 subprojects/libglnx/tests/testing-helper.c + create mode 100644 subprojects/libglnx/tests/use-as-subproject/.gitignore + create mode 100644 subprojects/libglnx/tests/use-as-subproject/README + create mode 100644 subprojects/libglnx/tests/use-as-subproject/config.h + create mode 100644 subprojects/libglnx/tests/use-as-subproject/dummy-config.h.in + create mode 100644 subprojects/libglnx/tests/use-as-subproject/meson.build + create mode 100644 subprojects/libglnx/tests/use-as-subproject/trivial.c + create mode 100644 subprojects/libglnx/tests/use-as-subproject/use-libglnx.c + create mode 100644 subprojects/libglnx/tests/use-as-subproject/use-testlib.c + +diff --git a/subprojects/libglnx/Makefile-libglnx.am b/subprojects/libglnx/Makefile-libglnx.am +new file mode 100644 +index 0000000..2c6ad44 +--- /dev/null ++++ b/subprojects/libglnx/Makefile-libglnx.am +@@ -0,0 +1,92 @@ ++# Copyright (C) 2015 Colin Walters ++# SPDX-License-Identifier: LGPL-2.0-or-later ++# ++# 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. ++ ++EXTRA_DIST += \ ++ $(libglnx_srcpath)/README.md \ ++ $(libglnx_srcpath)/COPYING \ ++ $(libglnx_srcpath)/LICENSES/LGPL-2.0-or-later.txt \ ++ $(libglnx_srcpath)/LICENSES/LGPL-2.1-or-later.txt \ ++ $(libglnx_srcpath)/libglnx.m4 \ ++ $(NULL) ++ ++BUILT_SOURCES += $(top_builddir)/libglnx-config.h ++CLEANFILES += $(top_builddir)/libglnx-config.h ++$(top_builddir)/libglnx-config.h: Makefile.am ++ echo '#include "config.h"' > $@ ++ ++libglnx_la_SOURCES = \ ++ $(libglnx_srcpath)/glnx-macros.h \ ++ $(libglnx_srcpath)/glnx-backport-autocleanups.h \ ++ $(libglnx_srcpath)/glnx-backport-autoptr.h \ ++ $(libglnx_srcpath)/glnx-backport-testutils.h \ ++ $(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 \ ++ $(libglnx_srcpath)/glnx-errors.c \ ++ $(libglnx_srcpath)/glnx-console.h \ ++ $(libglnx_srcpath)/glnx-console.c \ ++ $(libglnx_srcpath)/glnx-dirfd.h \ ++ $(libglnx_srcpath)/glnx-dirfd.c \ ++ $(libglnx_srcpath)/glnx-fdio.h \ ++ $(libglnx_srcpath)/glnx-fdio.c \ ++ $(libglnx_srcpath)/glnx-lockfile.h \ ++ $(libglnx_srcpath)/glnx-lockfile.c \ ++ $(libglnx_srcpath)/glnx-missing-syscall.h \ ++ $(libglnx_srcpath)/glnx-missing.h \ ++ $(libglnx_srcpath)/glnx-xattrs.h \ ++ $(libglnx_srcpath)/glnx-xattrs.c \ ++ $(libglnx_srcpath)/glnx-shutil.h \ ++ $(libglnx_srcpath)/glnx-shutil.c \ ++ $(libglnx_srcpath)/libglnx.h \ ++ $(libglnx_srcpath)/tests/libglnx-testlib.h \ ++ $(NULL) ++ ++libglnx_la_CFLAGS = $(AM_CFLAGS) $(libglnx_cflags) -I$(builddir) ++libglnx_la_LDFLAGS = -avoid-version -Bsymbolic-functions -export-symbols-regex "^glnx_" -no-undefined -export-dynamic ++libglnx_la_LIBADD = $(libglnx_libs) ++ ++libglnx_tests = test-libglnx-xattrs test-libglnx-fdio test-libglnx-errors test-libglnx-macros test-libglnx-shutil ++TESTS += $(libglnx_tests) ++ ++libglnx_testlib_sources = $(libglnx_srcpath)/tests/libglnx-testlib.c ++ ++check_PROGRAMS += $(libglnx_tests) ++test_libglnx_xattrs_SOURCES = $(libglnx_testlib_sources) $(libglnx_srcpath)/tests/test-libglnx-xattrs.c ++test_libglnx_xattrs_CFLAGS = $(AM_CFLAGS) $(libglnx_cflags) ++test_libglnx_xattrs_LDADD = $(libglnx_libs) libglnx.la ++ ++test_libglnx_fdio_SOURCES = $(libglnx_testlib_sources) $(libglnx_srcpath)/tests/test-libglnx-fdio.c ++test_libglnx_fdio_CFLAGS = $(AM_CFLAGS) $(libglnx_cflags) ++test_libglnx_fdio_LDADD = $(libglnx_libs) libglnx.la ++ ++test_libglnx_errors_SOURCES = $(libglnx_testlib_sources) $(libglnx_srcpath)/tests/test-libglnx-errors.c ++test_libglnx_errors_CFLAGS = $(AM_CFLAGS) $(libglnx_cflags) ++test_libglnx_errors_LDADD = $(libglnx_libs) libglnx.la ++ ++test_libglnx_macros_SOURCES = $(libglnx_testlib_sources) $(libglnx_srcpath)/tests/test-libglnx-macros.c ++test_libglnx_macros_CFLAGS = $(AM_CFLAGS) $(libglnx_cflags) ++test_libglnx_macros_LDADD = $(libglnx_libs) libglnx.la ++ ++test_libglnx_shutil_SOURCES = $(libglnx_testlib_sources) $(libglnx_srcpath)/tests/test-libglnx-shutil.c ++test_libglnx_shutil_CFLAGS = $(AM_CFLAGS) $(libglnx_cflags) ++test_libglnx_shutil_LDADD = $(libglnx_libs) libglnx.la +diff --git a/subprojects/libglnx/glnx-backport-autocleanups.h b/subprojects/libglnx/glnx-backport-autocleanups.h +index 50f469f..9b15ee4 100644 +--- a/subprojects/libglnx/glnx-backport-autocleanups.h ++++ b/subprojects/libglnx/glnx-backport-autocleanups.h +@@ -1,5 +1,6 @@ + /* + * Copyright © 2015 Canonical Limited ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +diff --git a/subprojects/libglnx/glnx-backport-autoptr.h b/subprojects/libglnx/glnx-backport-autoptr.h +index b36919d..8eb91b9 100644 +--- a/subprojects/libglnx/glnx-backport-autoptr.h ++++ b/subprojects/libglnx/glnx-backport-autoptr.h +@@ -1,6 +1,7 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2015 Colin Walters ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * GLIB - Library of useful routines for C programming + * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald +diff --git a/subprojects/libglnx/glnx-backport-testutils.c b/subprojects/libglnx/glnx-backport-testutils.c +new file mode 100644 +index 0000000..3440872 +--- /dev/null ++++ b/subprojects/libglnx/glnx-backport-testutils.c +@@ -0,0 +1,162 @@ ++/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- ++ * ++ * Copyright 2015 Colin Walters ++ * Copyright 2020 Niels De Graef ++ * Copyright 2021-2022 Collabora Ltd. ++ * SPDX-License-Identifier: LGPL-2.1-or-later ++ * ++ * This program 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.1 of the licence 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. ++ */ ++ ++#include "libglnx-config.h" ++ ++#include ++#include ++ ++#include ++ ++#include "glnx-backport-autocleanups.h" ++#include "glnx-backport-autoptr.h" ++#include "glnx-backport-testutils.h" ++#include "glnx-backports.h" ++ ++#include ++#include ++ ++#if !GLIB_CHECK_VERSION (2, 68, 0) ++/* Backport of g_assertion_message_cmpstrv() */ ++void ++_glnx_assertion_message_cmpstrv (const char *domain, ++ const char *file, ++ int line, ++ const char *func, ++ const char *expr, ++ const char * const *arg1, ++ const char * const *arg2, ++ gsize first_wrong_idx) ++{ ++ const char *s1 = arg1[first_wrong_idx], *s2 = arg2[first_wrong_idx]; ++ char *a1, *a2, *s, *t1 = NULL, *t2 = NULL; ++ ++ a1 = g_strconcat ("\"", t1 = g_strescape (s1, NULL), "\"", NULL); ++ a2 = g_strconcat ("\"", t2 = g_strescape (s2, NULL), "\"", NULL); ++ g_free (t1); ++ g_free (t2); ++ s = g_strdup_printf ("assertion failed (%s): first differing element at index %" G_GSIZE_FORMAT ": %s does not equal %s", ++ expr, first_wrong_idx, a1, a2); ++ g_free (a1); ++ g_free (a2); ++ g_assertion_message (domain, file, line, func, s); ++ g_free (s); ++} ++#endif ++ ++#if !GLIB_CHECK_VERSION(2, 70, 0) ++/* ++ * Same as g_test_message(), but split messages with newlines into ++ * multiple separate messages to avoid corrupting stdout, even in older ++ * GLib versions that didn't do this ++ */ ++void ++_glnx_test_message_safe (const char *format, ++ ...) ++{ ++ g_autofree char *message = NULL; ++ va_list ap; ++ char *line; ++ char *saveptr = NULL; ++ ++ va_start (ap, format); ++ g_vasprintf (&message, format, ap); ++ va_end (ap); ++ ++ for (line = strtok_r (message, "\n", &saveptr); ++ line != NULL; ++ line = strtok_r (NULL, "\n", &saveptr)) ++ (g_test_message) ("%s", line); ++} ++ ++/* Backport of g_test_fail_printf() */ ++void ++_glnx_test_fail_printf (const char *format, ++ ...) ++{ ++ g_autofree char *message = NULL; ++ va_list ap; ++ ++ va_start (ap, format); ++ g_vasprintf (&message, format, ap); ++ va_end (ap); ++ ++ /* This is the closest we can do in older GLib */ ++ g_test_message ("Bail out! %s", message); ++ g_test_fail (); ++} ++ ++/* Backport of g_test_skip_printf() */ ++void ++_glnx_test_skip_printf (const char *format, ++ ...) ++{ ++ g_autofree char *message = NULL; ++ va_list ap; ++ ++ va_start (ap, format); ++ g_vasprintf (&message, format, ap); ++ va_end (ap); ++ ++ g_test_skip (message); ++} ++ ++/* Backport of g_test_incomplete_printf() */ ++void ++_glnx_test_incomplete_printf (const char *format, ++ ...) ++{ ++ g_autofree char *message = NULL; ++ va_list ap; ++ ++ va_start (ap, format); ++ g_vasprintf (&message, format, ap); ++ va_end (ap); ++ ++#if GLIB_CHECK_VERSION(2, 58, 0) ++ /* Since 2.58, g_test_incomplete() sets the exit status correctly. */ ++ g_test_incomplete (message); ++#elif GLIB_CHECK_VERSION (2, 38, 0) ++ /* Before 2.58, g_test_incomplete() was treated like a failure for the ++ * purposes of setting the exit status, so prefer to use (our wrapper ++ * around) g_test_skip(). */ ++ g_test_skip_printf ("TODO: %s", message); ++#else ++ g_test_message ("TODO: %s", message); ++#endif ++} ++#endif ++ ++#if !GLIB_CHECK_VERSION (2, 78, 0) ++void ++_glnx_test_disable_crash_reporting (void) ++{ ++ struct rlimit limit = { 0, 0 }; ++ ++ (void) setrlimit (RLIMIT_CORE, &limit); ++ ++ /* On Linux, RLIMIT_CORE = 0 is ignored if core dumps are ++ * configured to be written to a pipe, but PR_SET_DUMPABLE is not. */ ++ (void) prctl (PR_SET_DUMPABLE, 0, 0, 0, 0); ++} ++#endif +diff --git a/subprojects/libglnx/glnx-backport-testutils.h b/subprojects/libglnx/glnx-backport-testutils.h +new file mode 100644 +index 0000000..f579c0e +--- /dev/null ++++ b/subprojects/libglnx/glnx-backport-testutils.h +@@ -0,0 +1,209 @@ ++/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- ++ * ++ * Copyright 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald ++ * Copyright 2015 Colin Walters ++ * Copyright 2014 Dan Winship ++ * Copyright 2015 Colin Walters ++ * Copyright 2017 Emmanuele Bassi ++ * Copyright 2018-2019 Endless OS Foundation LLC ++ * Copyright 2020 Niels De Graef ++ * Copyright 2021-2022 Collabora Ltd. ++ * SPDX-License-Identifier: LGPL-2.1-or-later ++ * ++ * 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.1 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. ++ */ ++ ++#pragma once ++ ++#include ++ ++#include "glnx-backports.h" ++ ++G_BEGIN_DECLS ++ ++#ifndef g_assert_true /* added in 2.38 */ ++#define g_assert_true(x) g_assert ((x)) ++#endif ++ ++#ifndef g_assert_false /* added in 2.38 */ ++#define g_assert_false(x) g_assert (!(x)) ++#endif ++ ++#ifndef g_assert_nonnull /* added in 2.40 */ ++#define g_assert_nonnull(x) g_assert (x != NULL) ++#endif ++ ++#ifndef g_assert_null /* added in 2.40 */ ++#define g_assert_null(x) g_assert (x == NULL) ++#endif ++ ++#if !GLIB_CHECK_VERSION (2, 38, 0) ++/* Not exactly equivalent, but close enough */ ++#define g_test_skip(s) g_test_message ("SKIP: %s", s) ++#endif ++ ++#if !GLIB_CHECK_VERSION (2, 58, 0) ++/* Before 2.58, g_test_incomplete() didn't set the exit status correctly */ ++#define g_test_incomplete(s) _glnx_test_incomplete_printf ("%s", s) ++#endif ++ ++#if !GLIB_CHECK_VERSION (2, 46, 0) ++#define g_assert_cmpmem(m1, l1, m2, l2) G_STMT_START {\ ++ gconstpointer __m1 = m1, __m2 = m2; \ ++ int __l1 = l1, __l2 = l2; \ ++ if (__l1 != 0 && __m1 == NULL) \ ++ g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ ++ "assertion failed (" #l1 " == 0 || " #m1 " != NULL)"); \ ++ else if (__l2 != 0 && __m2 == NULL) \ ++ g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ ++ "assertion failed (" #l2 " == 0 || " #m2 " != NULL)"); \ ++ else if (__l1 != __l2) \ ++ g_assertion_message_cmpnum (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ ++ #l1 " (len(" #m1 ")) == " #l2 " (len(" #m2 "))", \ ++ (long double) __l1, "==", (long double) __l2, 'i'); \ ++ else if (__l1 != 0 && __m2 != NULL && memcmp (__m1, __m2, __l1) != 0) \ ++ g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ ++ "assertion failed (" #m1 " == " #m2 ")"); \ ++ } G_STMT_END ++#endif ++ ++#if !GLIB_CHECK_VERSION (2, 58, 0) ++#define g_assert_cmpfloat_with_epsilon(n1,n2,epsilon) \ ++ G_STMT_START { \ ++ double __n1 = (n1), __n2 = (n2), __epsilon = (epsilon); \ ++ if (G_APPROX_VALUE (__n1, __n2, __epsilon)) ; else \ ++ g_assertion_message_cmpnum (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ ++ #n1 " == " #n2 " (+/- " #epsilon ")", __n1, "==", __n2, 'f'); \ ++ } G_STMT_END ++#endif ++ ++#if !GLIB_CHECK_VERSION (2, 60, 0) ++#define g_assert_cmpvariant(v1, v2) \ ++ G_STMT_START \ ++ { \ ++ GVariant *__v1 = (v1), *__v2 = (v2); \ ++ if (!g_variant_equal (__v1, __v2)) \ ++ { \ ++ gchar *__s1, *__s2, *__msg; \ ++ __s1 = g_variant_print (__v1, TRUE); \ ++ __s2 = g_variant_print (__v2, TRUE); \ ++ __msg = g_strdup_printf ("assertion failed (" #v1 " == " #v2 "): %s does not equal %s", __s1, __s2); \ ++ g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, __msg); \ ++ g_free (__s1); \ ++ g_free (__s2); \ ++ g_free (__msg); \ ++ } \ ++ } \ ++ G_STMT_END ++#endif ++ ++#if !GLIB_CHECK_VERSION (2, 62, 0) ++/* Not exactly equivalent, but close enough */ ++#define g_test_summary(s) g_test_message ("SUMMARY: %s", s) ++#endif ++ ++#if !GLIB_CHECK_VERSION (2, 66, 0) ++#define g_assert_no_errno(expr) G_STMT_START { \ ++ int __ret, __errsv; \ ++ errno = 0; \ ++ __ret = expr; \ ++ __errsv = errno; \ ++ if (__ret < 0) \ ++ { \ ++ gchar *__msg; \ ++ __msg = g_strdup_printf ("assertion failed (" #expr " >= 0): errno %i: %s", __errsv, g_strerror (__errsv)); \ ++ g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, __msg); \ ++ g_free (__msg); \ ++ } \ ++ } G_STMT_END ++#endif ++ ++#if !GLIB_CHECK_VERSION (2, 68, 0) ++#define g_assertion_message_cmpstrv _glnx_assertion_message_cmpstrv ++void _glnx_assertion_message_cmpstrv (const char *domain, ++ const char *file, ++ int line, ++ const char *func, ++ const char *expr, ++ const char * const *arg1, ++ const char * const *arg2, ++ gsize first_wrong_idx); ++#define g_assert_cmpstrv(strv1, strv2) \ ++ G_STMT_START \ ++ { \ ++ const char * const *__strv1 = (const char * const *) (strv1); \ ++ const char * const *__strv2 = (const char * const *) (strv2); \ ++ if (!__strv1 || !__strv2) \ ++ { \ ++ if (__strv1) \ ++ { \ ++ g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ ++ "assertion failed (" #strv1 " == " #strv2 "): " #strv2 " is NULL, but " #strv1 " is not"); \ ++ } \ ++ else if (__strv2) \ ++ { \ ++ g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ ++ "assertion failed (" #strv1 " == " #strv2 "): " #strv1 " is NULL, but " #strv2 " is not"); \ ++ } \ ++ } \ ++ else \ ++ { \ ++ guint __l1 = g_strv_length ((char **) __strv1); \ ++ guint __l2 = g_strv_length ((char **) __strv2); \ ++ if (__l1 != __l2) \ ++ { \ ++ char *__msg; \ ++ __msg = g_strdup_printf ("assertion failed (" #strv1 " == " #strv2 "): length %u does not equal length %u", __l1, __l2); \ ++ g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, __msg); \ ++ g_free (__msg); \ ++ } \ ++ else \ ++ { \ ++ guint __i; \ ++ for (__i = 0; __i < __l1; __i++) \ ++ { \ ++ if (g_strcmp0 (__strv1[__i], __strv2[__i]) != 0) \ ++ { \ ++ g_assertion_message_cmpstrv (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ ++ #strv1 " == " #strv2, \ ++ __strv1, __strv2, __i); \ ++ } \ ++ } \ ++ } \ ++ } \ ++ } \ ++ G_STMT_END ++#endif ++ ++#if !GLIB_CHECK_VERSION (2, 70, 0) ++/* Before 2.70, diagnostic messages containing newlines were problematic */ ++#define g_test_message(...) _glnx_test_message_safe (__VA_ARGS__) ++void _glnx_test_message_safe (const char *format, ...) G_GNUC_PRINTF (1, 2); ++ ++#define g_test_fail_printf _glnx_test_fail_printf ++void _glnx_test_fail_printf (const char *format, ...) G_GNUC_PRINTF (1, 2); ++#define g_test_skip_printf _glnx_test_skip_printf ++void _glnx_test_skip_printf (const char *format, ...) G_GNUC_PRINTF (1, 2); ++#define g_test_incomplete_printf _glnx_test_incomplete_printf ++void _glnx_test_incomplete_printf (const char *format, ...) G_GNUC_PRINTF (1, 2); ++#endif ++ ++#if !GLIB_CHECK_VERSION (2, 78, 0) ++#define g_test_disable_crash_reporting _glnx_test_disable_crash_reporting ++void _glnx_test_disable_crash_reporting (void); ++#endif ++ ++G_END_DECLS +diff --git a/subprojects/libglnx/glnx-backports.c b/subprojects/libglnx/glnx-backports.c +index c7bb600..69f9577 100644 +--- a/subprojects/libglnx/glnx-backports.c ++++ b/subprojects/libglnx/glnx-backports.c +@@ -1,10 +1,27 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * ++ * Copyright 2000-2022 Red Hat, Inc. ++ * Copyright 2006-2007 Matthias Clasen ++ * Copyright 2006 Padraig O'Briain ++ * Copyright 2007 Lennart Poettering + * Copyright (C) 2015 Colin Walters ++ * Copyright 2018-2022 Endless OS Foundation, LLC ++ * Copyright 2018 Peter Wu ++ * Copyright 2019 Ting-Wei Lan ++ * Copyright 2019 Sebastian Schwarz ++ * Copyright 2020 Matt Rose ++ * Copyright 2021 Casper Dik ++ * Copyright 2022 Alexander Richardson ++ * Copyright 2022 Ray Strode ++ * Copyright 2022 Thomas Haller ++ * Copyright 2023-2024 Collabora Ltd. ++ * Copyright 2023 Sebastian Wilhelmi ++ * Copyright 2023 CaiJingLong ++ * SPDX-License-Identifier: LGPL-2.1-or-later + * +- * This program is free software: you can redistribute it and/or modify ++ * 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 licence or (at ++ * by the Free Software Foundation; either version 2.1 of the licence or (at + * your option) any later version. + * + * This library is distributed in the hope that it will be useful, +@@ -12,15 +29,23 @@ + * 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. ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with this library; if not, see . + */ + +-#include "config.h" ++#include "libglnx-config.h" + + #include "glnx-backports.h" ++#include "glnx-missing.h" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include + + #if !GLIB_CHECK_VERSION(2, 44, 0) + gboolean +@@ -59,3 +84,483 @@ glnx_set_object (GObject **object_ptr, + return TRUE; + } + #endif ++ ++#if !GLIB_CHECK_VERSION(2, 60, 0) ++gboolean ++_glnx_strv_equal (const gchar * const *strv1, ++ const gchar * const *strv2) ++{ ++ g_return_val_if_fail (strv1 != NULL, FALSE); ++ g_return_val_if_fail (strv2 != NULL, FALSE); ++ ++ if (strv1 == strv2) ++ return TRUE; ++ ++ for (; *strv1 != NULL && *strv2 != NULL; strv1++, strv2++) ++ { ++ if (!g_str_equal (*strv1, *strv2)) ++ return FALSE; ++ } ++ ++ return (*strv1 == NULL && *strv2 == NULL); ++} ++#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)). */ ++static int ++set_cloexec (void *data, gint fd) ++{ ++ if (fd >= GPOINTER_TO_INT (data)) ++ fcntl (fd, F_SETFD, FD_CLOEXEC); ++ ++ return 0; ++} ++ ++/* fdwalk()-compatible callback to close a fd for non-compliant ++ * implementations of fdwalk() that potentially pass already ++ * closed fds. ++ * ++ * It is not an error to pass an invalid fd to this function. ++ * ++ * This function is called between fork() and exec() and hence must be ++ * async-signal-safe (see signal-safety(7)). ++ */ ++G_GNUC_UNUSED static int ++close_func_with_invalid_fds (void *data, int fd) ++{ ++ /* We use close and not g_close here because on some platforms, we ++ * don't know how to close only valid, open file descriptors, so we ++ * have to pass bad fds to close too. g_close warns if given a bad ++ * fd. ++ * ++ * This function returns no error, because there is nothing that the caller ++ * could do with that information. That is even the case for EINTR. See ++ * g_close() about the specialty of EINTR and why that is correct. ++ * If g_close() ever gets extended to handle EINTR specially, then this place ++ * should get updated to do the same handling. ++ */ ++ if (fd >= GPOINTER_TO_INT (data)) ++ close (fd); ++ ++ return 0; ++} ++ ++#ifdef __linux__ ++struct linux_dirent64 ++{ ++ guint64 d_ino; /* 64-bit inode number */ ++ guint64 d_off; /* 64-bit offset to next structure */ ++ unsigned short d_reclen; /* Size of this dirent */ ++ unsigned char d_type; /* File type */ ++ char d_name[]; /* Filename (null-terminated) */ ++}; ++ ++/* This function is called between fork() and exec() and hence must be ++ * async-signal-safe (see signal-safety(7)). */ ++static gint ++filename_to_fd (const char *p) ++{ ++ char c; ++ int fd = 0; ++ const int cutoff = G_MAXINT / 10; ++ const int cutlim = G_MAXINT % 10; ++ ++ if (*p == '\0') ++ return -1; ++ ++ while ((c = *p++) != '\0') ++ { ++ if (c < '0' || c > '9') ++ return -1; ++ c -= '0'; ++ ++ /* Check for overflow. */ ++ if (fd > cutoff || (fd == cutoff && c > cutlim)) ++ return -1; ++ ++ fd = fd * 10 + c; ++ } ++ ++ return fd; ++} ++#endif ++ ++static int safe_fdwalk_with_invalid_fds (int (*cb)(void *data, int fd), void *data); ++ ++/* This function is called between fork() and exec() and hence must be ++ * async-signal-safe (see signal-safety(7)). */ ++static int ++safe_fdwalk (int (*cb)(void *data, int fd), void *data) ++{ ++#if 0 ++ /* Use fdwalk function provided by the system if it is known to be ++ * async-signal safe. ++ * ++ * Currently there are no operating systems known to provide a safe ++ * implementation, so this section is not used for now. ++ */ ++ return fdwalk (cb, data); ++#else ++ /* Fallback implementation of fdwalk. It should be async-signal safe, but it ++ * may fail on non-Linux operating systems. See safe_fdwalk_with_invalid_fds ++ * for a slower alternative. ++ */ ++ ++#ifdef __linux__ ++ gint fd; ++ gint res = 0; ++ ++ /* Avoid use of opendir/closedir since these are not async-signal-safe. */ ++ int dir_fd = open ("/proc/self/fd", O_RDONLY | O_DIRECTORY); ++ if (dir_fd >= 0) ++ { ++ /* buf needs to be aligned correctly to receive linux_dirent64. ++ * C11 has _Alignof for this purpose, but for now a ++ * union serves the same purpose. */ ++ union ++ { ++ char buf[4096]; ++ struct linux_dirent64 alignment; ++ } u; ++ int pos, nread; ++ struct linux_dirent64 *de; ++ ++ while ((nread = syscall (SYS_getdents64, dir_fd, u.buf, sizeof (u.buf))) > 0) ++ { ++ for (pos = 0; pos < nread; pos += de->d_reclen) ++ { ++ de = (struct linux_dirent64 *) (u.buf + pos); ++ ++ fd = filename_to_fd (de->d_name); ++ if (fd < 0 || fd == dir_fd) ++ continue; ++ ++ if ((res = cb (data, fd)) != 0) ++ break; ++ } ++ } ++ ++ close (dir_fd); ++ return res; ++ } ++ ++ /* If /proc is not mounted or not accessible we fail here and rely on ++ * safe_fdwalk_with_invalid_fds to fall back to the old ++ * rlimit trick. */ ++ ++#endif ++ ++#if defined(__sun__) && defined(F_PREVFD) && defined(F_NEXTFD) ++/* ++ * Solaris 11.4 has a signal-safe way which allows ++ * us to find all file descriptors in a process. ++ * ++ * fcntl(fd, F_NEXTFD, maxfd) ++ * - returns the first allocated file descriptor <= maxfd > fd. ++ * ++ * fcntl(fd, F_PREVFD) ++ * - return highest allocated file descriptor < fd. ++ */ ++ gint fd; ++ gint res = 0; ++ ++ open_max = fcntl (INT_MAX, F_PREVFD); /* find the maximum fd */ ++ if (open_max < 0) /* No open files */ ++ return 0; ++ ++ for (fd = -1; (fd = fcntl (fd, F_NEXTFD, open_max)) != -1; ) ++ if ((res = cb (data, fd)) != 0 || fd == open_max) ++ break; ++ ++ return res; ++#endif ++ ++ return safe_fdwalk_with_invalid_fds (cb, data); ++#endif ++} ++ ++/* This function is called between fork() and exec() and hence must be ++ * async-signal-safe (see signal-safety(7)). */ ++static int ++safe_fdwalk_with_invalid_fds (int (*cb)(void *data, int fd), void *data) ++{ ++ /* Fallback implementation of fdwalk. It should be async-signal safe, but it ++ * may be slow, especially on systems allowing very high number of open file ++ * descriptors. ++ */ ++ gint open_max = -1; ++ gint fd; ++ gint res = 0; ++ ++#if 0 && defined(HAVE_SYS_RESOURCE_H) ++ struct rlimit rl; ++ ++ /* Use getrlimit() function provided by the system if it is known to be ++ * async-signal safe. ++ * ++ * Currently there are no operating systems known to provide a safe ++ * implementation, so this section is not used for now. ++ */ ++ if (getrlimit (RLIMIT_NOFILE, &rl) == 0 && rl.rlim_max != RLIM_INFINITY) ++ open_max = rl.rlim_max; ++#endif ++#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__APPLE__) ++ /* Use sysconf() function provided by the system if it is known to be ++ * async-signal safe. ++ * ++ * FreeBSD: sysconf() is included in the list of async-signal safe functions ++ * found in https://man.freebsd.org/sigaction(2). ++ * ++ * OpenBSD: sysconf() is included in the list of async-signal safe functions ++ * found in https://man.openbsd.org/sigaction.2. ++ * ++ * Apple: sysconf() is included in the list of async-signal safe functions ++ * found in https://opensource.apple.com/source/xnu/xnu-517.12.7/bsd/man/man2/sigaction.2 ++ */ ++ if (open_max < 0) ++ open_max = sysconf (_SC_OPEN_MAX); ++#endif ++ /* Hardcoded fallback: the default process hard limit in Linux as of 2020 */ ++ if (open_max < 0) ++ open_max = 4096; ++ ++#if defined(__APPLE__) && defined(HAVE_LIBPROC_H) ++ /* proc_pidinfo isn't documented as async-signal-safe but looking at the implementation ++ * in the darwin tree here: ++ * ++ * https://opensource.apple.com/source/Libc/Libc-498/darwin/libproc.c.auto.html ++ * ++ * It's just a thin wrapper around a syscall, so it's probably okay. ++ */ ++ { ++ char buffer[4096 * PROC_PIDLISTFD_SIZE]; ++ ssize_t buffer_size; ++ ++ buffer_size = proc_pidinfo (getpid (), PROC_PIDLISTFDS, 0, buffer, sizeof (buffer)); ++ ++ if (buffer_size > 0 && ++ sizeof (buffer) >= (size_t) buffer_size && ++ (buffer_size % PROC_PIDLISTFD_SIZE) == 0) ++ { ++ const struct proc_fdinfo *fd_info = (const struct proc_fdinfo *) buffer; ++ size_t number_of_fds = (size_t) buffer_size / PROC_PIDLISTFD_SIZE; ++ ++ for (size_t i = 0; i < number_of_fds; i++) ++ if ((res = cb (data, fd_info[i].proc_fd)) != 0) ++ break; ++ ++ return res; ++ } ++ } ++#endif ++ ++ for (fd = 0; fd < open_max; fd++) ++ if ((res = cb (data, fd)) != 0) ++ break; ++ ++ return res; ++} ++ ++/** ++ * g_fdwalk_set_cloexec: ++ * @lowfd: Minimum fd to act on, which must be non-negative ++ * ++ * Mark every file descriptor equal to or greater than @lowfd to be closed ++ * at the next `execve()` or similar, as if via the `FD_CLOEXEC` flag. ++ * ++ * Typically @lowfd will be 3, to leave standard input, standard output ++ * and standard error open after exec. ++ * ++ * This is the same as Linux `close_range (lowfd, ~0U, CLOSE_RANGE_CLOEXEC)`, ++ * but portable to other OSs and to older versions of Linux. ++ * ++ * This function is async-signal safe, making it safe to call from a ++ * signal handler or a [callback@GLib.SpawnChildSetupFunc], as long as @lowfd is ++ * non-negative. ++ * See [`signal(7)`](man:signal(7)) and ++ * [`signal-safety(7)`](man:signal-safety(7)) for more details. ++ * ++ * Returns: 0 on success, -1 with errno set on error ++ * Since: 2.80 ++ */ ++int ++_glnx_fdwalk_set_cloexec (int lowfd) ++{ ++ int ret; ++ ++ g_return_val_if_fail (lowfd >= 0, (errno = EINVAL, -1)); ++ ++#if defined(HAVE_CLOSE_RANGE) && defined(CLOSE_RANGE_CLOEXEC) ++ /* close_range() is available in Linux since kernel 5.9, and on FreeBSD at ++ * around the same time. It was designed for use in async-signal-safe ++ * situations: https://bugs.python.org/issue38061 ++ * ++ * The `CLOSE_RANGE_CLOEXEC` flag was added in Linux 5.11, and is not yet ++ * present in FreeBSD. ++ * ++ * Handle ENOSYS in case it’s supported in libc but not the kernel; if so, ++ * fall back to safe_fdwalk(). Handle EINVAL in case `CLOSE_RANGE_CLOEXEC` ++ * is not supported. */ ++ ret = close_range (lowfd, G_MAXUINT, CLOSE_RANGE_CLOEXEC); ++ if (ret == 0 || !(errno == ENOSYS || errno == EINVAL)) ++ return ret; ++#endif /* HAVE_CLOSE_RANGE */ ++ ++ ret = safe_fdwalk (set_cloexec, GINT_TO_POINTER (lowfd)); ++ ++ return ret; ++} ++ ++/** ++ * g_closefrom: ++ * @lowfd: Minimum fd to close, which must be non-negative ++ * ++ * Close every file descriptor equal to or greater than @lowfd. ++ * ++ * Typically @lowfd will be 3, to leave standard input, standard output ++ * and standard error open. ++ * ++ * This is the same as Linux `close_range (lowfd, ~0U, 0)`, ++ * but portable to other OSs and to older versions of Linux. ++ * Equivalently, it is the same as BSD `closefrom (lowfd)`, but portable, ++ * and async-signal-safe on all OSs. ++ * ++ * This function is async-signal safe, making it safe to call from a ++ * signal handler or a [callback@GLib.SpawnChildSetupFunc], as long as @lowfd is ++ * non-negative. ++ * See [`signal(7)`](man:signal(7)) and ++ * [`signal-safety(7)`](man:signal-safety(7)) for more details. ++ * ++ * Returns: 0 on success, -1 with errno set on error ++ * Since: 2.80 ++ */ ++int ++_glnx_closefrom (int lowfd) ++{ ++ int ret; ++ ++ g_return_val_if_fail (lowfd >= 0, (errno = EINVAL, -1)); ++ ++#if defined(HAVE_CLOSE_RANGE) ++ /* close_range() is available in Linux since kernel 5.9, and on FreeBSD at ++ * around the same time. It was designed for use in async-signal-safe ++ * situations: https://bugs.python.org/issue38061 ++ * ++ * Handle ENOSYS in case it’s supported in libc but not the kernel; if so, ++ * fall back to safe_fdwalk(). */ ++ ret = close_range (lowfd, G_MAXUINT, 0); ++ if (ret == 0 || errno != ENOSYS) ++ return ret; ++#endif /* HAVE_CLOSE_RANGE */ ++ ++#if defined(__FreeBSD__) || defined(__OpenBSD__) || \ ++ (defined(__sun__) && defined(F_CLOSEFROM)) ++ /* Use closefrom function provided by the system if it is known to be ++ * async-signal safe. ++ * ++ * FreeBSD: closefrom is included in the list of async-signal safe functions ++ * found in https://man.freebsd.org/sigaction(2). ++ * ++ * OpenBSD: closefrom is not included in the list, but a direct system call ++ * should be safe to use. ++ * ++ * In Solaris as of 11.3 SRU 31, closefrom() is also a direct system call. ++ * On such systems, F_CLOSEFROM is defined. ++ */ ++ (void) closefrom (lowfd); ++ return 0; ++#elif defined(__DragonFly__) ++ /* It is unclear whether closefrom function included in DragonFlyBSD libc_r ++ * is safe to use because it calls a lot of library functions. It is also ++ * unclear whether libc_r itself is still being used. Therefore, we do a ++ * direct system call here ourselves to avoid possible issues. ++ */ ++ (void) syscall (SYS_closefrom, lowfd); ++ return 0; ++#elif defined(F_CLOSEM) ++ /* NetBSD and AIX have a special fcntl command which does the same thing as ++ * closefrom. NetBSD also includes closefrom function, which seems to be a ++ * simple wrapper of the fcntl command. ++ */ ++ return fcntl (lowfd, F_CLOSEM); ++#else ++ ret = safe_fdwalk (close_func_with_invalid_fds, GINT_TO_POINTER (lowfd)); ++ ++ return ret; ++#endif ++} ++#endif /* !2.80.0 */ +diff --git a/subprojects/libglnx/glnx-backports.h b/subprojects/libglnx/glnx-backports.h +index 6c39cf2..9ec11e4 100644 +--- a/subprojects/libglnx/glnx-backports.h ++++ b/subprojects/libglnx/glnx-backports.h +@@ -1,6 +1,11 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * ++ * Copyright 1998 Manish Singh ++ * Copyright 1998 Tim Janik + * Copyright (C) 2015 Colin Walters ++ * Copyright (C) 2018 Endless OS Foundation, LLC ++ * Copyright 2017 Emmanuele Bassi ++ * SPDX-License-Identifier: LGPL-2.1-or-later + * + * GLIB - Library of useful routines for C programming + * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald +@@ -8,7 +13,7 @@ + * 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. ++ * version 2.1 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 +@@ -16,13 +21,15 @@ + * 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. ++ * License along with this library; if not, see . + */ + + #pragma once + ++#include ++ ++#include ++#include + #include + + G_BEGIN_DECLS +@@ -47,6 +54,38 @@ 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 ++ + #if !GLIB_CHECK_VERSION(2, 44, 0) + + #define g_strv_contains glnx_strv_contains +@@ -63,16 +102,98 @@ gboolean glnx_set_object (GObject **object_ptr, + + #endif /* !GLIB_CHECK_VERSION(2, 44, 0) */ + +-#ifndef g_assert_nonnull +-#define g_assert_nonnull(x) g_assert (x != NULL) ++#if !GLIB_CHECK_VERSION(2, 38, 0) ++#define G_SPAWN_DEFAULT ((GSpawnFlags) 0) ++#endif ++ ++#if !GLIB_CHECK_VERSION(2, 42, 0) ++#define G_OPTION_FLAG_NONE ((GOptionFlags) 0) ++#endif ++ ++#ifndef G_PID_FORMAT /* added in 2.50 */ ++#define G_PID_FORMAT "i" ++#endif ++ ++#if !GLIB_CHECK_VERSION(2, 60, 0) ++#define g_strv_equal _glnx_strv_equal ++gboolean _glnx_strv_equal (const gchar * const *strv1, ++ const gchar * const *strv2); ++#endif ++ ++#ifndef G_DBUS_METHOD_INVOCATION_HANDLED /* added in 2.68 */ ++#define G_DBUS_METHOD_INVOCATION_HANDLED TRUE ++#endif ++ ++#ifndef G_DBUS_METHOD_INVOCATION_UNHANDLED /* added in 2.68 */ ++#define G_DBUS_METHOD_INVOCATION_UNHANDLED FALSE ++#endif ++ ++#if !GLIB_CHECK_VERSION(2, 68, 0) ++static inline gpointer _glnx_memdup2 (gconstpointer mem, ++ gsize byte_size) G_GNUC_ALLOC_SIZE(2); ++static inline gpointer ++_glnx_memdup2 (gconstpointer mem, ++ gsize byte_size) ++{ ++ gpointer new_mem; ++ ++ if (mem && byte_size != 0) ++ { ++ new_mem = g_malloc (byte_size); ++ memcpy (new_mem, mem, byte_size); ++ } ++ else ++ new_mem = NULL; ++ ++ return new_mem; ++} ++#define g_memdup2 _glnx_memdup2 ++#endif ++ ++#ifndef G_OPTION_ENTRY_NULL /* added in 2.70 */ ++#define G_OPTION_ENTRY_NULL { NULL, 0, 0, 0, NULL, NULL, NULL } ++#endif ++ ++#ifndef G_APPROX_VALUE /* added in 2.58 */ ++#define G_APPROX_VALUE(a, b, epsilon) \ ++ (((a) > (b) ? (a) - (b) : (b) - (a)) < (epsilon)) ++#endif ++ ++static inline int ++_glnx_steal_fd (int *fdp) ++{ ++#if GLIB_CHECK_VERSION(2, 70, 0) ++ /* Allow it to be used without deprecation warnings, even if the target ++ * GLib version is older */ ++ G_GNUC_BEGIN_IGNORE_DEPRECATIONS ++ return g_steal_fd (fdp); ++ G_GNUC_END_IGNORE_DEPRECATIONS ++#else ++ int fd = *fdp; ++ *fdp = -1; ++ return fd; + #endif ++} ++#define g_steal_fd _glnx_steal_fd + +-#ifndef g_assert_null +-#define g_assert_null(x) g_assert (x == NULL) ++#if !GLIB_CHECK_VERSION(2, 74, 0) ++#define G_APPLICATION_DEFAULT_FLAGS ((GApplicationFlags) 0) ++#define G_CONNECT_DEFAULT ((GConnectFlags) 0) ++#define G_IO_FLAG_NONE ((GIOFlags) 0) ++#define G_MARKUP_DEFAULT_FLAGS ((GMarkupParseFlags) 0) ++#define G_REGEX_DEFAULT ((GRegexCompileFlags) 0) ++#define G_REGEX_MATCH_DEFAULT ((GRegexMatchFlags) 0) ++#define G_TEST_SUBPROCESS_DEFAULT ((GTestSubprocessFlags) 0) ++#define G_TEST_TRAP_DEFAULT ((GTestTrapFlags) 0) ++#define G_TLS_CERTIFICATE_NO_FLAGS ((GTlsCertificateFlags) 0) ++#define G_TYPE_FLAG_NONE ((GTypeFlags) 0) + #endif + +-#if !GLIB_CHECK_VERSION (2, 38, 0) +-#define g_test_skip(s) g_test_message ("SKIP: %s", s) ++#if !GLIB_CHECK_VERSION(2, 80, 0) ++#define g_closefrom _glnx_closefrom ++int _glnx_closefrom (int lowfd); ++#define g_fdwalk_set_cloexec _glnx_fdwalk_set_cloexec ++int _glnx_fdwalk_set_cloexec (int lowfd); + #endif + + G_END_DECLS +diff --git a/subprojects/libglnx/glnx-chase.c b/subprojects/libglnx/glnx-chase.c +new file mode 100644 +index 0000000..9ad2fe3 +--- /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 0000000..05dac12 +--- /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-console.c b/subprojects/libglnx/glnx-console.c +index c2fe29d..f6ce0d4 100644 +--- a/subprojects/libglnx/glnx-console.c ++++ b/subprojects/libglnx/glnx-console.c +@@ -1,6 +1,7 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2013,2014,2015 Colin Walters ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published +@@ -18,7 +19,7 @@ + * Boston, MA 02111-1307, USA. + */ + +-#include "config.h" ++#include "libglnx-config.h" + + #include "glnx-console.h" + +@@ -142,7 +143,7 @@ glnx_console_lines (void) + } + + static void +-on_sigwinch (int signum) ++on_sigwinch (G_GNUC_UNUSED int signum) + { + cached_columns = 0; + cached_lines = 0; +@@ -261,7 +262,7 @@ text_percent_internal (const char *text, + const guint textlen = MIN (input_textlen, ncolumns - bar_min); + const guint barlen = MIN (MAX_PROGRESSBAR_COLUMNS, ncolumns - (textlen + 1)); + +- if (textlen > 0) ++ if (text && textlen > 0) + { + fwrite (text, 1, textlen, stdout); + fputc (' ', stdout); +diff --git a/subprojects/libglnx/glnx-console.h b/subprojects/libglnx/glnx-console.h +index d853a80..3d94895 100644 +--- a/subprojects/libglnx/glnx-console.h ++++ b/subprojects/libglnx/glnx-console.h +@@ -1,6 +1,7 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2013,2014,2015 Colin Walters ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published +diff --git a/subprojects/libglnx/glnx-dirfd.c b/subprojects/libglnx/glnx-dirfd.c +index 6d1e2d2..b78e2df 100644 +--- a/subprojects/libglnx/glnx-dirfd.c ++++ b/subprojects/libglnx/glnx-dirfd.c +@@ -1,6 +1,7 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2014,2015 Colin Walters . ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +@@ -18,7 +19,7 @@ + * Boston, MA 02111-1307, USA. + */ + +-#include "config.h" ++#include "libglnx-config.h" + + #include + +@@ -128,7 +129,7 @@ glnx_dirfd_iterator_init_take_fd (int *dfd, + if (!d) + return glnx_throw_errno_prefix (error, "fdopendir"); + +- real_dfd_iter->fd = glnx_steal_fd (dfd); ++ real_dfd_iter->fd = g_steal_fd (dfd); + real_dfd_iter->d = d; + real_dfd_iter->initialized = TRUE; + +@@ -173,6 +174,24 @@ glnx_dirfd_iterator_next_dent (GLnxDirFdIterator *dfd_iter, + return TRUE; + } + ++/** ++ * glnx_dirfd_iterator_rewind: ++ * @dfd_iter: A directory iterator ++ * ++ * Rewind to the beginning of @dfd_iter. The next call to ++ * glnx_dirfd_iterator_next_dent() will provide the first entry in the ++ * directory. ++ */ ++void ++glnx_dirfd_iterator_rewind (GLnxDirFdIterator *dfd_iter) ++{ ++ GLnxRealDirfdIterator *real_dfd_iter = (GLnxRealDirfdIterator*) dfd_iter; ++ ++ g_return_if_fail (dfd_iter->initialized); ++ ++ rewinddir (real_dfd_iter->d); ++} ++ + /** + * glnx_dirfd_iterator_next_dent_ensure_dtype: + * @dfd_iter: A directory iterator +@@ -330,7 +349,7 @@ glnx_mkdtempat (int dfd, const char *tmpl, int mode, + /* Return the initialized directory struct */ + out_tmpdir->initialized = TRUE; + out_tmpdir->src_dfd = dfd; /* referenced; see above docs */ +- out_tmpdir->fd = glnx_steal_fd (&ret_dfd); ++ out_tmpdir->fd = g_steal_fd (&ret_dfd); + out_tmpdir->path = g_steal_pointer (&path); + return TRUE; + } +diff --git a/subprojects/libglnx/glnx-dirfd.h b/subprojects/libglnx/glnx-dirfd.h +index 0046ac8..1960820 100644 +--- a/subprojects/libglnx/glnx-dirfd.h ++++ b/subprojects/libglnx/glnx-dirfd.h +@@ -1,6 +1,7 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2014,2015 Colin Walters . ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +@@ -66,6 +67,7 @@ gboolean glnx_dirfd_iterator_next_dent_ensure_dtype (GLnxDirFdIterator *dfd_ite + struct dirent **out_dent, + GCancellable *cancellable, + GError **error); ++void glnx_dirfd_iterator_rewind (GLnxDirFdIterator *dfd_iter); + void glnx_dirfd_iterator_clear (GLnxDirFdIterator *dfd_iter); + + G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(GLnxDirFdIterator, glnx_dirfd_iterator_clear) +diff --git a/subprojects/libglnx/glnx-errors.c b/subprojects/libglnx/glnx-errors.c +index f350f30..cb0df13 100644 +--- a/subprojects/libglnx/glnx-errors.c ++++ b/subprojects/libglnx/glnx-errors.c +@@ -1,6 +1,7 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2014,2015 Colin Walters . ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +@@ -18,7 +19,7 @@ + * Boston, MA 02111-1307, USA. + */ + +-#include "config.h" ++#include "libglnx-config.h" + + #include + #include +diff --git a/subprojects/libglnx/glnx-errors.h b/subprojects/libglnx/glnx-errors.h +index cbe74a6..5434a6f 100644 +--- a/subprojects/libglnx/glnx-errors.h ++++ b/subprojects/libglnx/glnx-errors.h +@@ -1,6 +1,7 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2014,2015 Colin Walters . ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +@@ -31,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, +@@ -96,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; +@@ -107,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, +@@ -119,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 d4eeb24..5926166 100644 +--- a/subprojects/libglnx/glnx-fdio.c ++++ b/subprojects/libglnx/glnx-fdio.c +@@ -1,6 +1,7 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2014,2015 Colin Walters . ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * Portions derived from systemd: + * Copyright 2010 Lennart Poettering +@@ -21,7 +22,7 @@ + * Boston, MA 02111-1307, USA. + */ + +-#include "config.h" ++#include "libglnx-config.h" + + #include + #include +@@ -217,7 +218,7 @@ open_tmpfile_core (int dfd, const char *subpath, + return glnx_throw_errno_prefix (error, "fchmod"); + out_tmpf->initialized = TRUE; + out_tmpf->src_dfd = dfd; /* Copied; caller must keep open */ +- out_tmpf->fd = glnx_steal_fd (&fd); ++ out_tmpf->fd = g_steal_fd (&fd); + out_tmpf->path = NULL; + return TRUE; + } +@@ -228,7 +229,7 @@ open_tmpfile_core (int dfd, const char *subpath, + const guint count_max = 100; + { g_autofree char *tmp = g_strconcat (subpath, "/tmp.XXXXXX", NULL); + +- for (int count = 0; count < count_max; count++) ++ for (guint count = 0; count < count_max; count++) + { + glnx_gen_temp_name (tmp); + +@@ -244,7 +245,7 @@ open_tmpfile_core (int dfd, const char *subpath, + { + out_tmpf->initialized = TRUE; + out_tmpf->src_dfd = dfd; /* Copied; caller must keep open */ +- out_tmpf->fd = glnx_steal_fd (&fd); ++ out_tmpf->fd = g_steal_fd (&fd); + out_tmpf->path = g_steal_pointer (&tmp); + return TRUE; + } +@@ -320,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). + */ +@@ -366,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) + { +@@ -454,15 +457,15 @@ 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; + } + + glnx_close_fd (&tmpf->fd); +- tmpf->fd = glnx_steal_fd (&rdonly_fd); ++ tmpf->fd = g_steal_fd (&rdonly_fd); + return TRUE; + } + +@@ -659,7 +662,7 @@ glnx_file_get_contents_utf8_at (int dfd, + char * + glnx_readlinkat_malloc (int dfd, + const char *subpath, +- GCancellable *cancellable, ++ G_GNUC_UNUSED GCancellable *cancellable, + GError **error) + { + dfd = glnx_dirfd_canonicalize (dfd); +@@ -793,10 +796,21 @@ glnx_regfile_copy_bytes (int fdf, int fdt, off_t max_bytes) + + /* If we've requested to copy the whole range, try a full-file clone first. + */ +- if (max_bytes == (off_t) -1) ++ if (max_bytes == (off_t) -1 && ++ lseek (fdf, 0, SEEK_CUR) == 0 && ++ lseek (fdt, 0, SEEK_CUR) == 0) + { + if (ioctl (fdt, FICLONE, fdf) == 0) +- return 0; ++ { ++ /* All the other methods advance the fds. Do it here too for consistency. */ ++ if (lseek (fdf, 0, SEEK_END) < 0) ++ return -1; ++ if (lseek (fdt, 0, SEEK_END) < 0) ++ return -1; ++ ++ return 0; ++ } ++ + /* Fall through */ + struct stat stbuf; + +@@ -805,7 +819,9 @@ glnx_regfile_copy_bytes (int fdf, int fdt, off_t max_bytes) + */ + if (fstat (fdf, &stbuf) < 0) + return -1; +- max_bytes = stbuf.st_size; ++ ++ if (stbuf.st_size > 0) ++ max_bytes = stbuf.st_size; + } + + while (TRUE) +@@ -816,7 +832,7 @@ glnx_regfile_copy_bytes (int fdf, int fdt, off_t max_bytes) + * try_copy_file_range() from systemd upstream, which works better since + * we use POSIX errno style. + */ +- if (try_cfr) ++ if (try_cfr && max_bytes != (off_t) -1) + { + n = copy_file_range (fdf, NULL, fdt, NULL, max_bytes, 0u); + if (n < 0) +@@ -829,7 +845,7 @@ glnx_regfile_copy_bytes (int fdf, int fdt, off_t max_bytes) + have_cfr = 0; + try_cfr = false; + } +- else if (G_IN_SET (errno, EXDEV, EOPNOTSUPP)) ++ else if (G_IN_SET (errno, EXDEV, EINVAL, EOPNOTSUPP)) + /* We won't try cfr again for this run, but let's be + * conservative and not mark it as available/unavailable until + * we know for sure. +@@ -855,7 +871,7 @@ glnx_regfile_copy_bytes (int fdf, int fdt, off_t max_bytes) + /* Next try sendfile(); this version is also changed from systemd upstream + * to match the same logic we have for copy_file_range(). + */ +- if (try_sendfile) ++ if (try_sendfile && max_bytes != (off_t) -1) + { + n = sendfile (fdt, fdf, NULL, max_bytes); + if (n < 0) +@@ -1000,8 +1016,11 @@ glnx_file_copy_at (int src_dfd, + if (glnx_regfile_copy_bytes (src_fd, tmp_dest.fd, (off_t) -1) < 0) + return glnx_throw_errno_prefix (error, "regfile copy"); + +- if (fchown (tmp_dest.fd, src_stbuf->st_uid, src_stbuf->st_gid) != 0) +- return glnx_throw_errno_prefix (error, "fchown"); ++ if (!(copyflags & GLNX_FILE_COPY_NOCHOWN)) ++ { ++ if (fchown (tmp_dest.fd, src_stbuf->st_uid, src_stbuf->st_gid) != 0) ++ return glnx_throw_errno_prefix (error, "fchown"); ++ } + + if (!(copyflags & GLNX_FILE_COPY_NOXATTRS)) + { +@@ -1100,7 +1119,7 @@ glnx_file_replace_contents_with_perms_at (int dfd, + uid_t uid, + gid_t gid, + GLnxFileReplaceFlags flags, +- GCancellable *cancellable, ++ G_GNUC_UNUSED GCancellable *cancellable, + GError **error) + { + char *dnbuf = strdupa (subpath); +@@ -1124,7 +1143,7 @@ glnx_file_replace_contents_with_perms_at (int dfd, + &tmpf, error)) + return FALSE; + +- if (len == -1) ++ if (len == (gsize) -1) + len = strlen ((char*)buf); + + if (!glnx_try_fallocate (tmpf.fd, 0, len, error)) +@@ -1188,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 cc1ed4b..acd546d 100644 +--- a/subprojects/libglnx/glnx-fdio.h ++++ b/subprojects/libglnx/glnx-fdio.h +@@ -1,6 +1,7 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2014,2015 Colin Walters . ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +@@ -21,6 +22,7 @@ + #pragma once + + #include ++#include + #include + #include + #include +@@ -43,7 +45,7 @@ G_BEGIN_DECLS + static inline + const char *glnx_basename (const char *path) + { +- gchar *base = strrchr (path, G_DIR_SEPARATOR); ++ const gchar *base = strrchr (path, G_DIR_SEPARATOR); + + if (base) + return base + 1; +@@ -55,7 +57,7 @@ const char *glnx_basename (const char *path) + static inline void + glnx_stdio_file_cleanup (void *filep) + { +- FILE *f = filep; ++ FILE *f = (FILE*)filep; + if (f) + fclose (f); + } +@@ -189,7 +191,8 @@ glnx_regfile_copy_bytes (int fdf, int fdt, off_t max_bytes); + typedef enum { + GLNX_FILE_COPY_OVERWRITE = (1 << 0), + GLNX_FILE_COPY_NOXATTRS = (1 << 1), +- GLNX_FILE_COPY_DATASYNC = (1 << 2) ++ GLNX_FILE_COPY_DATASYNC = (1 << 2), ++ GLNX_FILE_COPY_NOCHOWN = (1 << 3) + } GLnxFileCopyFlags; + + gboolean +@@ -207,6 +210,7 @@ int glnx_renameat2_noreplace (int olddirfd, const char *oldpath, + int glnx_renameat2_exchange (int olddirfd, const char *oldpath, + int newdirfd, const char *newpath); + ++#ifdef _GNU_SOURCE + /** + * glnx_try_fallocate: + * @fd: File descriptor +@@ -238,6 +242,7 @@ glnx_try_fallocate (int fd, + + return TRUE; + } ++#endif + + /** + * glnx_fstat: +@@ -309,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 +@@ -379,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.c b/subprojects/libglnx/glnx-local-alloc.c +index 692f0de..8bd5514 100644 +--- a/subprojects/libglnx/glnx-local-alloc.c ++++ b/subprojects/libglnx/glnx-local-alloc.c +@@ -1,6 +1,7 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2012,2015 Colin Walters ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +@@ -18,7 +19,7 @@ + * Boston, MA 02111-1307, USA. + */ + +-#include "config.h" ++#include "libglnx-config.h" + + #include "glnx-local-alloc.h" + +diff --git a/subprojects/libglnx/glnx-local-alloc.h b/subprojects/libglnx/glnx-local-alloc.h +index 3be1fa4..a6e0e9b 100644 +--- a/subprojects/libglnx/glnx-local-alloc.h ++++ b/subprojects/libglnx/glnx-local-alloc.h +@@ -1,6 +1,7 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2012,2015 Colin Walters . ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +@@ -23,6 +24,8 @@ + #include + #include + ++#include "glnx-backports.h" ++ + G_BEGIN_DECLS + + /** +@@ -40,21 +43,15 @@ glnx_local_obj_unref (void *v) + if (o) + g_object_unref (o); + } +-#define glnx_unref_object __attribute__ ((cleanup(glnx_local_obj_unref))) + +-static inline int +-glnx_steal_fd (int *fdp) +-{ +- int fd = *fdp; +- *fdp = -1; +- return fd; +-} ++/* Backwards-compat with older libglnx */ ++#define glnx_steal_fd g_steal_fd + + /** + * glnx_close_fd: + * @fdp: Pointer to fd + * +- * Effectively `close (glnx_steal_fd (&fd))`. Also ++ * Effectively `close (g_steal_fd (&fd))`. Also + * asserts that `close()` did not raise `EBADF` - encountering + * that error is usually a critical bug in the program. + */ +@@ -65,7 +62,7 @@ glnx_close_fd (int *fdp) + + g_assert (fdp); + +- int fd = glnx_steal_fd (fdp); ++ int fd = g_steal_fd (fdp); + if (fd >= 0) + { + errsv = errno; +diff --git a/subprojects/libglnx/glnx-lockfile.c b/subprojects/libglnx/glnx-lockfile.c +index f1d52de..30e638c 100644 +--- a/subprojects/libglnx/glnx-lockfile.c ++++ b/subprojects/libglnx/glnx-lockfile.c +@@ -7,6 +7,7 @@ + + Copyright 2010 Lennart Poettering + Copyright 2015 Colin Walters ++ SPDX-License-Identifier: LGPL-2.1-or-later + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by +@@ -22,7 +23,7 @@ + along with systemd; If not, see . + ***/ + +-#include "config.h" ++#include "libglnx-config.h" + + #include + #include +@@ -65,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, +@@ -128,7 +131,7 @@ glnx_make_lock_file(int dfd, const char *p, int operation, GLnxLockFile *out_loc + out_lock->initialized = TRUE; + out_lock->dfd = dfd; + out_lock->path = g_steal_pointer (&t); +- out_lock->fd = glnx_steal_fd (&fd); ++ out_lock->fd = g_steal_fd (&fd); + out_lock->operation = operation; + return TRUE; + } +diff --git a/subprojects/libglnx/glnx-lockfile.h b/subprojects/libglnx/glnx-lockfile.h +index b346508..6604d70 100644 +--- a/subprojects/libglnx/glnx-lockfile.h ++++ b/subprojects/libglnx/glnx-lockfile.h +@@ -7,6 +7,7 @@ + + Copyright 2011 Lennart Poettering + Copyright 2015 Colin Walters ++ SPDX-License-Identifier: LGPL-2.1-or-later + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by +@@ -22,8 +23,6 @@ + along with systemd; If not, see . + ***/ + +-#include "config.h" +- + #include "glnx-backport-autoptr.h" + + typedef struct GLnxLockFile { +diff --git a/subprojects/libglnx/glnx-macros.h b/subprojects/libglnx/glnx-macros.h +index 700fc75..b92e9e2 100644 +--- a/subprojects/libglnx/glnx-macros.h ++++ b/subprojects/libglnx/glnx-macros.h +@@ -3,6 +3,7 @@ + * Copyright (C) 2017 Colin Walters + * With original source from systemd: + * Copyright 2010 Lennart Poettering ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +@@ -52,7 +53,7 @@ G_BEGIN_DECLS + unsigned _i_; \ + for (_i_ = 0; _i_ < G_N_ELEMENTS(_appendees_) && _appendees_[_i_]; _i_++) \ + _len_ += strlen(_appendees_[_i_]); \ +- _p_ = _d_ = alloca(_len_ + 1); \ ++ _p_ = _d_ = (char*) alloca(_len_ + 1); \ + for (_i_ = 0; _i_ < G_N_ELEMENTS(_appendees_) && _appendees_[_i_]; _i_++) \ + _p_ = stpcpy(_p_, _appendees_[_i_]); \ + *_p_ = 0; \ +@@ -136,7 +137,7 @@ G_BEGIN_DECLS + guard && ({ g_hash_table_iter_init (&it, ht), TRUE; }); \ + guard = FALSE) \ + for (kt k; guard; guard = FALSE) \ +- for (vt v; g_hash_table_iter_next (&it, (gpointer)&k, (gpointer)&v);) ++ for (vt v; g_hash_table_iter_next (&it, (void**)&k, (void**)&v);) + + + /* Cleaner method to iterate over a GHashTable. I.e. rather than +diff --git a/subprojects/libglnx/glnx-missing-syscall.h b/subprojects/libglnx/glnx-missing-syscall.h +index 4876ca3..f3bdb3b 100644 +--- a/subprojects/libglnx/glnx-missing-syscall.h ++++ b/subprojects/libglnx/glnx-missing-syscall.h +@@ -3,6 +3,7 @@ + + Copyright 2010 Lennart Poettering + Copyright 2016 Zbigniew Jędrzejewski-Szmek ++ SPDX-License-Identifier: LGPL-2.1-or-later + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by +@@ -30,7 +31,9 @@ + Add abstraction model for BPF programs + */ + +-#include "config.h" ++#include "libglnx-config.h" ++#include ++#include + + #if !HAVE_DECL_RENAMEAT2 + # ifndef __NR_renameat2 +@@ -154,3 +157,435 @@ static inline ssize_t missing_copy_file_range(int fd_in, loff_t *off_in, + + # define copy_file_range missing_copy_file_range + #endif ++ ++#ifndef __IGNORE_close_range ++# if defined(__aarch64__) ++# define systemd_NR_close_range 436 ++# elif defined(__alpha__) ++# define systemd_NR_close_range 546 ++# elif defined(__arc__) || defined(__tilegx__) ++# define systemd_NR_close_range 436 ++# elif defined(__arm__) ++# define systemd_NR_close_range 436 ++# elif defined(__i386__) ++# define systemd_NR_close_range 436 ++# elif defined(__ia64__) ++# define systemd_NR_close_range 1460 ++# elif defined(__loongarch_lp64) ++# define systemd_NR_close_range 436 ++# elif defined(__m68k__) ++# define systemd_NR_close_range 436 ++# elif defined(_MIPS_SIM) ++# if _MIPS_SIM == _MIPS_SIM_ABI32 ++# define systemd_NR_close_range 4436 ++# elif _MIPS_SIM == _MIPS_SIM_NABI32 ++# define systemd_NR_close_range 6436 ++# elif _MIPS_SIM == _MIPS_SIM_ABI64 ++# define systemd_NR_close_range 5436 ++# else ++# error "Unknown MIPS ABI" ++# endif ++# elif defined(__hppa__) ++# define systemd_NR_close_range 436 ++# elif defined(__powerpc__) ++# define systemd_NR_close_range 436 ++# elif defined(__riscv) ++# if __riscv_xlen == 32 ++# define systemd_NR_close_range 436 ++# elif __riscv_xlen == 64 ++# define systemd_NR_close_range 436 ++# else ++# error "Unknown RISC-V ABI" ++# endif ++# elif defined(__s390__) ++# define systemd_NR_close_range 436 ++# elif defined(__sparc__) ++# define systemd_NR_close_range 436 ++# elif defined(__x86_64__) ++# if defined(__ILP32__) ++# define systemd_NR_close_range (436 | /* __X32_SYSCALL_BIT */ 0x40000000) ++# else ++# define systemd_NR_close_range 436 ++# endif ++# elif !defined(missing_arch_template) ++# warning "close_range() syscall number is unknown for your architecture" ++# endif ++ ++/* may be an (invalid) negative number due to libseccomp, see PR 13319 */ ++# if defined __NR_close_range && __NR_close_range >= 0 ++# if defined systemd_NR_close_range ++G_STATIC_ASSERT(__NR_close_range == systemd_NR_close_range); ++# endif ++# else ++# if defined __NR_close_range ++# undef __NR_close_range ++# endif ++# if defined systemd_NR_close_range && systemd_NR_close_range >= 0 ++# define __NR_close_range systemd_NR_close_range ++# endif ++# endif ++#endif ++ ++#if !defined(HAVE_CLOSE_RANGE) && defined(__NR_close_range) ++static inline int ++inline_close_range (unsigned int low, ++ unsigned int high, ++ int flags) ++{ ++ return syscall (__NR_close_range, low, high, flags); ++} ++#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/glnx-missing.h b/subprojects/libglnx/glnx-missing.h +index 9d35c40..fa724b8 100644 +--- a/subprojects/libglnx/glnx-missing.h ++++ b/subprojects/libglnx/glnx-missing.h +@@ -4,6 +4,7 @@ + This file was originally part of systemd. + + Copyright 2010 Lennart Poettering ++ SPDX-License-Identifier: LGPL-2.1-or-later + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by +@@ -91,4 +92,12 @@ + #define MFD_CLOEXEC 0x0001U + #endif + ++#ifndef CLOSE_RANGE_UNSHARE ++#define CLOSE_RANGE_UNSHARE (1U << 1) ++#endif ++ ++#ifndef CLOSE_RANGE_CLOEXEC ++#define CLOSE_RANGE_CLOEXEC (1U << 2) ++#endif ++ + #include "glnx-missing-syscall.h" +diff --git a/subprojects/libglnx/glnx-shutil.c b/subprojects/libglnx/glnx-shutil.c +index 78042fe..5a6fd7c 100644 +--- a/subprojects/libglnx/glnx-shutil.c ++++ b/subprojects/libglnx/glnx-shutil.c +@@ -1,6 +1,7 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2014,2015 Colin Walters . ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +@@ -18,7 +19,7 @@ + * Boston, MA 02111-1307, USA. + */ + +-#include "config.h" ++#include "libglnx-config.h" + + #include + +@@ -148,12 +149,10 @@ mkdir_p_at_internal (int dfd, + again: + if (mkdirat (dfd, path, mode) == -1) + { +- if (errno == ENOENT) ++ if (errno == ENOENT && !did_recurse) + { + char *lastslash; + +- g_assert (!did_recurse); +- + lastslash = strrchr (path, '/'); + if (lastslash == NULL) + { +diff --git a/subprojects/libglnx/glnx-shutil.h b/subprojects/libglnx/glnx-shutil.h +index 56a99fa..6a00312 100644 +--- a/subprojects/libglnx/glnx-shutil.h ++++ b/subprojects/libglnx/glnx-shutil.h +@@ -1,6 +1,7 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2014,2015 Colin Walters . ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +diff --git a/subprojects/libglnx/glnx-xattrs.c b/subprojects/libglnx/glnx-xattrs.c +index 892d534..84fd609 100644 +--- a/subprojects/libglnx/glnx-xattrs.c ++++ b/subprojects/libglnx/glnx-xattrs.c +@@ -1,6 +1,7 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2014,2015 Colin Walters . ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +@@ -18,7 +19,7 @@ + * Boston, MA 02111-1307, USA. + */ + +-#include "config.h" ++#include "libglnx-config.h" + + #include + #include +@@ -140,7 +141,7 @@ static gboolean + get_xattrs_impl (const char *path, + int fd, + GVariant **out_xattrs, +- GCancellable *cancellable, ++ G_GNUC_UNUSED GCancellable *cancellable, + GError **error) + { + gboolean ret = FALSE; +@@ -181,7 +182,7 @@ get_xattrs_impl (const char *path, + { + if (errno == ERANGE) + { +- g_free (xattr_names); ++ g_free (g_steal_pointer (&xattr_names)); + goto again; + } + glnx_set_prefix_error_from_errno (error, "%s", "llistxattr"); +@@ -268,7 +269,7 @@ glnx_dfd_name_get_all_xattrs (int dfd, + static gboolean + set_all_xattrs_for_path (const char *path, + GVariant *xattrs, +- GCancellable *cancellable, ++ G_GNUC_UNUSED GCancellable *cancellable, + GError **error) + { + const guint n = g_variant_n_children (xattrs); +@@ -336,7 +337,7 @@ glnx_dfd_name_set_all_xattrs (int dfd, + gboolean + glnx_fd_set_all_xattrs (int fd, + GVariant *xattrs, +- GCancellable *cancellable, ++ G_GNUC_UNUSED GCancellable *cancellable, + GError **error) + { + const guint n = g_variant_n_children (xattrs); +@@ -436,7 +437,7 @@ glnx_lsetxattrat (int dfd, + char pathbuf[PATH_MAX]; + snprintf (pathbuf, sizeof (pathbuf), "/proc/self/fd/%d/%s", dfd, subpath); + +- if (TEMP_FAILURE_RETRY (lsetxattr (subpath, attribute, value, len, flags)) < 0) ++ if (TEMP_FAILURE_RETRY (lsetxattr (pathbuf, attribute, value, len, flags)) < 0) + return glnx_throw_errno_prefix (error, "lsetxattr(%s)", attribute); + + return TRUE; +diff --git a/subprojects/libglnx/glnx-xattrs.h b/subprojects/libglnx/glnx-xattrs.h +index a566a22..b028850 100644 +--- a/subprojects/libglnx/glnx-xattrs.h ++++ b/subprojects/libglnx/glnx-xattrs.h +@@ -1,6 +1,7 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2014,2015 Colin Walters . ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +diff --git a/subprojects/libglnx/libglnx.doap b/subprojects/libglnx/libglnx.doap +new file mode 100644 +index 0000000..6752e88 +--- /dev/null ++++ b/subprojects/libglnx/libglnx.doap +@@ -0,0 +1,35 @@ ++ ++ ++ ++ ++ libglnx ++ libglnx ++ ++ "Copylib" for system service modules using GLib with Linux ++ ++ This module is intended for use by ++ infrastructure code using GLib that is also Linux specific, such as ++ ostree, NetworkManager, and others. ++ ++ ++ ++ ++ ++ C ++ ++ ++ ++ Colin Walters ++ ++ walters ++ ++ ++ ++ +diff --git a/subprojects/libglnx/libglnx.h b/subprojects/libglnx/libglnx.h +index 411d4fa..2b5e07f 100644 +--- a/subprojects/libglnx/libglnx.h ++++ b/subprojects/libglnx/libglnx.h +@@ -1,6 +1,7 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2012,2013,2015 Colin Walters . ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +@@ -28,7 +29,9 @@ G_BEGIN_DECLS + #include + #include + #include ++#include + #include ++#include + #include + #include + #include +diff --git a/subprojects/libglnx/libglnx.m4 b/subprojects/libglnx/libglnx.m4 +index 5a72e98..e0beac9 100644 +--- a/subprojects/libglnx/libglnx.m4 ++++ b/subprojects/libglnx/libglnx.m4 +@@ -1,5 +1,10 @@ ++# Copyright 2016 Colin Walters ++# Copyright 2020 Endless OS Foundation LLC ++# SPDX-License-Identifier: LGPL-2.1-or-later ++ + AC_DEFUN([LIBGLNX_CONFIGURE], + [ ++dnl This defines HAVE_DECL_FOO to 1 if found or 0 if not + AC_CHECK_DECLS([ + renameat2, + memfd_create, +@@ -15,6 +20,9 @@ AC_CHECK_DECLS([ + #include + #include + ]]) ++dnl This defines HAVE_FOO to 1 if found, or leaves it undefined if not: ++dnl not the same! ++AC_CHECK_FUNCS([close_range]) + + AC_ARG_ENABLE(otmpfile, + [AS_HELP_STRING([--disable-otmpfile], +diff --git a/subprojects/libglnx/meson.build b/subprojects/libglnx/meson.build +new file mode 100644 +index 0000000..0fff39d +--- /dev/null ++++ b/subprojects/libglnx/meson.build +@@ -0,0 +1,115 @@ ++# Copyright 2019 Endless OS Foundation LLC ++# Copyright 2019 Collabora Ltd. ++# SPDX-License-Identifier: LGPL-2.1-or-later ++ ++project( ++ 'libglnx', ++ 'c', ++ default_options : [ ++ 'c_std=gnu99', ++ 'warning_level=2', ++ ], ++) ++ ++add_project_arguments('-D_GNU_SOURCE', language: 'c') ++add_project_arguments('-Wno-unused-local-typedefs', language: 'c') ++ ++# We are intentionally using non-ISO features in this (sub)project, ++# even if a parent project wants to use pedantic warnings ++add_project_arguments('-Wno-pedantic', language: 'c') ++add_project_arguments('-Wno-variadic-macros', language: 'c') ++ ++cc = meson.get_compiler('c') ++ ++ ++check_functions = [ ++ 'renameat2', ++ 'memfd_create', ++ 'copy_file_range', ++] ++conf = configuration_data() ++foreach check_function : check_functions ++ have_it = cc.compiles(''' ++ #include ++ #include ++ #include ++ #include ++ #include ++ #include ++ #include ++ #include ++ #include ++ ++ void func (void) { ++ (void) ''' + check_function + '''; ++ } ++ ''', ++ args : '-D_GNU_SOURCE', ++ name : check_function + '() is declared', ++ ) ++ conf.set10('HAVE_DECL_' + check_function.underscorify().to_upper(), have_it) ++endforeach ++ ++check_functions = [ ++ 'close_range', ++] ++foreach check_function : check_functions ++ if cc.has_function(check_function) ++ conf.set('HAVE_' + check_function.underscorify().to_upper(), 1) ++ endif ++endforeach ++ ++config_h = configure_file( ++ output : 'libglnx-config.h', ++ configuration : conf, ++) ++ ++libglnx_deps = [ ++ dependency('gio-2.0'), ++ dependency('gio-unix-2.0'), ++] ++libglnx_inc = include_directories('.') ++libglnx_sources = [ ++ 'glnx-backport-autocleanups.h', ++ 'glnx-backport-autoptr.h', ++ 'glnx-backport-testutils.c', ++ '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', ++ 'glnx-dirfd.h', ++ 'glnx-errors.c', ++ 'glnx-errors.h', ++ 'glnx-fdio.c', ++ 'glnx-fdio.h', ++ 'glnx-local-alloc.c', ++ 'glnx-local-alloc.h', ++ 'glnx-lockfile.c', ++ 'glnx-lockfile.h', ++ 'glnx-macros.h', ++ 'glnx-missing.h', ++ 'glnx-missing-syscall.h', ++ 'glnx-shutil.c', ++ 'glnx-shutil.h', ++ 'glnx-xattrs.c', ++ 'glnx-xattrs.h', ++ 'libglnx.h', ++] ++ ++libglnx = static_library('glnx', ++ libglnx_sources, ++ dependencies : libglnx_deps, ++ gnu_symbol_visibility : 'hidden', ++ include_directories : libglnx_inc, ++ install : false) ++libglnx_dep = declare_dependency( ++ dependencies : libglnx_deps, ++ include_directories : libglnx_inc, ++ link_with : libglnx) ++ ++subdir('tests') ++ +diff --git a/subprojects/libglnx/meson_options.txt b/subprojects/libglnx/meson_options.txt +new file mode 100644 +index 0000000..e900111 +--- /dev/null ++++ b/subprojects/libglnx/meson_options.txt +@@ -0,0 +1,9 @@ ++# Copyright 2022 Collabora Ltd. ++# SPDX-License-Identifier: LGPL-2.0-or-later ++ ++option( ++ 'tests', ++ type : 'boolean', ++ description : 'build and run unit tests', ++ value : true, ++) +diff --git a/subprojects/libglnx/tests/libglnx-testlib.c b/subprojects/libglnx/tests/libglnx-testlib.c +index 869d144..3eb2ba1 100644 +--- a/subprojects/libglnx/tests/libglnx-testlib.c ++++ b/subprojects/libglnx/tests/libglnx-testlib.c +@@ -1,6 +1,7 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright 2019 Collabora Ltd. ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +@@ -18,7 +19,7 @@ + * Boston, MA 02111-1307, USA. + */ + +-#include "config.h" ++#include "libglnx-config.h" + #include "libglnx-testlib.h" + + #include +@@ -66,8 +67,7 @@ _glnx_test_auto_temp_dir_leave (_GLnxTestAutoTempDir *dir) + glnx_tmpdir_delete (&dir->temp_dir, NULL, &error); + g_assert_no_error (error); + +- g_close (dir->old_cwd_fd, &error); +- g_assert_no_error (error); ++ glnx_close_fd (&dir->old_cwd_fd); + + g_free (dir->old_cwd); + g_free (dir); +diff --git a/subprojects/libglnx/tests/libglnx-testlib.h b/subprojects/libglnx/tests/libglnx-testlib.h +index d45ba86..dccc7e5 100644 +--- a/subprojects/libglnx/tests/libglnx-testlib.h ++++ b/subprojects/libglnx/tests/libglnx-testlib.h +@@ -2,6 +2,7 @@ + * + * Copyright (C) 2017 Red Hat, Inc. + * Copyright 2019 Collabora Ltd. ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +diff --git a/subprojects/libglnx/tests/meson.build b/subprojects/libglnx/tests/meson.build +new file mode 100644 +index 0000000..5b773e8 +--- /dev/null ++++ b/subprojects/libglnx/tests/meson.build +@@ -0,0 +1,59 @@ ++# Copyright 2019 Endless OS Foundation LLC ++# Copyright 2019 Collabora Ltd. ++# SPDX-License-Identifier: LGPL-2.1-or-later ++ ++libglnx_testlib = static_library( ++ 'glnx-testlib', ++ 'libglnx-testlib.c', ++ 'libglnx-testlib.h', ++ dependencies : [ ++ libglnx_dep, ++ libglnx_deps, ++ ], ++ install : false, ++) ++libglnx_testlib_dep = declare_dependency( ++ dependencies : [ ++ libglnx_dep, ++ libglnx_deps, ++ ], ++ include_directories : include_directories('.'), ++ link_with : libglnx_testlib, ++) ++ ++if get_option('tests') ++ testing_helper = executable( ++ 'testing-helper', ++ 'testing-helper.c', ++ dependencies : [ ++ libglnx_dep, ++ libglnx_deps, ++ ], ++ install : false, ++ ) ++ ++ test_names = [ ++ 'backports', ++ 'chase', ++ 'errors', ++ 'fdio', ++ 'macros', ++ 'shutil', ++ 'testing', ++ 'xattrs', ++ ] ++ ++ foreach test_name : test_names ++ exe = executable(test_name, ++ [ ++ 'test-libglnx-' + test_name + '.c', ++ ], ++ dependencies: [ ++ libglnx_dep, ++ libglnx_deps, ++ libglnx_testlib_dep, ++ ], ++ ) ++ test(test_name, exe, depends: testing_helper) ++ endforeach ++endif +diff --git a/subprojects/libglnx/tests/test-libglnx-backports.c b/subprojects/libglnx/tests/test-libglnx-backports.c +new file mode 100644 +index 0000000..89b1b3f +--- /dev/null ++++ b/subprojects/libglnx/tests/test-libglnx-backports.c +@@ -0,0 +1,299 @@ ++/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- ++ * ++ * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald ++ * Copyright (C) 2011 Red Hat, Inc. ++ * Copyright (C) 2018 Endless OS Foundation, LLC ++ * Copyright 2019 Emmanuel Fleury ++ * Copyright 2021-2024 Collabora Ltd. ++ * SPDX-License-Identifier: LGPL-2.1-or-later AND LicenseRef-old-glib-tests ++ */ ++ ++#include "libglnx-config.h" ++#include "libglnx.h" ++ ++#include ++#include ++ ++#include ++#include ++ ++static void ++async_signal_safe_message (const char *message) ++{ ++ if (write (2, message, strlen (message)) < 0 || ++ write (2, "\n", 1) < 0) ++ { ++ /* ignore: not much we can do */ ++ } ++} ++ ++static void test_closefrom_subprocess_einval (void); ++ ++static void ++test_closefrom (void) ++{ ++ /* Enough file descriptors to be confident that we're operating on ++ * all of them */ ++ const int N_FDS = 20; ++ int *fds; ++ int fd; ++ int i; ++ pid_t child; ++ int wait_status; ++ ++ /* The loop that populates @fds with pipes assumes this */ ++ g_assert (N_FDS % 2 == 0); ++ ++ for (fd = 0; fd <= 2; fd++) ++ { ++ int flags; ++ ++ g_assert_no_errno ((flags = fcntl (fd, F_GETFD))); ++ g_assert_no_errno (fcntl (fd, F_SETFD, flags & ~FD_CLOEXEC)); ++ } ++ ++ fds = g_new0 (int, N_FDS); ++ ++ for (i = 0; i < N_FDS; i += 2) ++ { ++ GError *error = NULL; ++ int pipefd[2]; ++ int res; ++ ++ /* Intentionally neither O_CLOEXEC nor FD_CLOEXEC */ ++ res = g_unix_open_pipe (pipefd, 0, &error); ++ g_assert (res); ++ g_assert_no_error (error); ++ g_clear_error (&error); ++ fds[i] = pipefd[0]; ++ fds[i + 1] = pipefd[1]; ++ } ++ ++ child = fork (); ++ ++ /* Child process exits with status = 100 + the first wrong fd, ++ * or 0 if all were correct */ ++ if (child == 0) ++ { ++ for (i = 0; i < N_FDS; i++) ++ { ++ int flags = fcntl (fds[i], F_GETFD); ++ ++ if (flags == -1) ++ { ++ async_signal_safe_message ("fd should not have been closed"); ++ _exit (100 + fds[i]); ++ } ++ ++ if (flags & FD_CLOEXEC) ++ { ++ async_signal_safe_message ("fd should not have been close-on-exec yet"); ++ _exit (100 + fds[i]); ++ } ++ } ++ ++ g_fdwalk_set_cloexec (3); ++ ++ for (i = 0; i < N_FDS; i++) ++ { ++ int flags = fcntl (fds[i], F_GETFD); ++ ++ if (flags == -1) ++ { ++ async_signal_safe_message ("fd should not have been closed"); ++ _exit (100 + fds[i]); ++ } ++ ++ if (!(flags & FD_CLOEXEC)) ++ { ++ async_signal_safe_message ("fd should have been close-on-exec"); ++ _exit (100 + fds[i]); ++ } ++ } ++ ++ g_closefrom (3); ++ ++ for (fd = 0; fd <= 2; fd++) ++ { ++ int flags = fcntl (fd, F_GETFD); ++ ++ if (flags == -1) ++ { ++ async_signal_safe_message ("fd should not have been closed"); ++ _exit (100 + fd); ++ } ++ ++ if (flags & FD_CLOEXEC) ++ { ++ async_signal_safe_message ("fd should not have been close-on-exec"); ++ _exit (100 + fd); ++ } ++ } ++ ++ for (i = 0; i < N_FDS; i++) ++ { ++ if (fcntl (fds[i], F_GETFD) != -1 || errno != EBADF) ++ { ++ async_signal_safe_message ("fd should have been closed"); ++ _exit (100 + fds[i]); ++ } ++ } ++ ++ _exit (0); ++ } ++ ++ g_assert_no_errno (waitpid (child, &wait_status, 0)); ++ ++ if (WIFEXITED (wait_status)) ++ { ++ int exit_status = WEXITSTATUS (wait_status); ++ ++ if (exit_status != 0) ++ g_test_fail_printf ("File descriptor %d in incorrect state", exit_status - 100); ++ } ++ else ++ { ++ g_test_fail_printf ("Unexpected wait status %d", wait_status); ++ } ++ ++ for (i = 0; i < N_FDS; i++) ++ g_assert_no_errno (close (fds[i])); ++ ++ g_free (fds); ++ ++ if (g_test_undefined ()) ++ { ++#if GLIB_CHECK_VERSION (2, 38, 0) ++ g_test_trap_subprocess ("/glib-unix/closefrom/subprocess/einval", ++ 0, G_TEST_SUBPROCESS_DEFAULT); ++#else ++ if (g_test_trap_fork (0, 0)) ++ { ++ test_closefrom_subprocess_einval (); ++ exit (0); ++ } ++ ++#endif ++ g_test_trap_assert_passed (); ++ } ++} ++ ++static void ++test_closefrom_subprocess_einval (void) ++{ ++ int res; ++ int errsv; ++ ++ g_log_set_always_fatal (G_LOG_FATAL_MASK); ++ g_log_set_fatal_mask ("GLib", G_LOG_FATAL_MASK); ++ ++ errno = 0; ++ res = g_closefrom (-1); ++ errsv = errno; ++ g_assert_cmpint (res, ==, -1); ++ g_assert_cmpint (errsv, ==, EINVAL); ++ ++ errno = 0; ++ res = g_fdwalk_set_cloexec (-42); ++ errsv = errno; ++ g_assert_cmpint (res, ==, -1); ++ g_assert_cmpint (errsv, ==, EINVAL); ++} ++ ++/* Testing g_memdup2() function with various positive and negative cases */ ++static void ++test_memdup2 (void) ++{ ++ gchar *str_dup = NULL; ++ const gchar *str = "The quick brown fox jumps over the lazy dog"; ++ ++ /* Testing negative cases */ ++ g_assert_null (g_memdup2 (NULL, 1024)); ++ g_assert_null (g_memdup2 (str, 0)); ++ g_assert_null (g_memdup2 (NULL, 0)); ++ ++ /* Testing normal usage cases */ ++ str_dup = g_memdup2 (str, strlen (str) + 1); ++ g_assert_nonnull (str_dup); ++ g_assert_cmpstr (str, ==, str_dup); ++ ++ g_free (str_dup); ++} ++ ++static void ++test_steal_fd (void) ++{ ++ GError *error = NULL; ++ gchar *tmpfile = NULL; ++ int fd = -42; ++ int borrowed; ++ int stolen; ++ ++ g_assert_cmpint (g_steal_fd (&fd), ==, -42); ++ g_assert_cmpint (fd, ==, -1); ++ g_assert_cmpint (g_steal_fd (&fd), ==, -1); ++ g_assert_cmpint (fd, ==, -1); ++ ++ fd = g_file_open_tmp (NULL, &tmpfile, &error); ++ g_assert_cmpint (fd, >=, 0); ++ g_assert_no_error (error); ++ borrowed = fd; ++ stolen = g_steal_fd (&fd); ++ g_assert_cmpint (fd, ==, -1); ++ g_assert_cmpint (borrowed, ==, stolen); ++ ++ g_assert_no_errno (close (g_steal_fd (&stolen))); ++ g_assert_cmpint (stolen, ==, -1); ++ ++ g_assert_no_errno (remove (tmpfile)); ++ g_free (tmpfile); ++ ++ /* Backwards compatibility with older libglnx: glnx_steal_fd is the same ++ * as g_steal_fd */ ++ fd = -23; ++ g_assert_cmpint (glnx_steal_fd (&fd), ==, -23); ++ g_assert_cmpint (fd, ==, -1); ++} ++ ++/* Test g_strv_equal() works for various inputs. */ ++static void ++test_strv_equal (void) ++{ ++ const gchar *strv_empty[] = { NULL }; ++ const gchar *strv_empty2[] = { NULL }; ++ const gchar *strv_simple[] = { "hello", "you", NULL }; ++ const gchar *strv_simple2[] = { "hello", "you", NULL }; ++ const gchar *strv_simple_reordered[] = { "you", "hello", NULL }; ++ const gchar *strv_simple_superset[] = { "hello", "you", "again", NULL }; ++ const gchar *strv_another[] = { "not", "a", "coded", "message", NULL }; ++ ++ g_assert_true (g_strv_equal (strv_empty, strv_empty)); ++ g_assert_true (g_strv_equal (strv_empty, strv_empty2)); ++ g_assert_true (g_strv_equal (strv_empty2, strv_empty)); ++ g_assert_false (g_strv_equal (strv_empty, strv_simple)); ++ g_assert_false (g_strv_equal (strv_simple, strv_empty)); ++ g_assert_true (g_strv_equal (strv_simple, strv_simple)); ++ g_assert_true (g_strv_equal (strv_simple, strv_simple2)); ++ g_assert_true (g_strv_equal (strv_simple2, strv_simple)); ++ g_assert_false (g_strv_equal (strv_simple, strv_simple_reordered)); ++ g_assert_false (g_strv_equal (strv_simple_reordered, strv_simple)); ++ g_assert_false (g_strv_equal (strv_simple, strv_simple_superset)); ++ g_assert_false (g_strv_equal (strv_simple_superset, strv_simple)); ++ g_assert_false (g_strv_equal (strv_simple, strv_another)); ++ g_assert_false (g_strv_equal (strv_another, strv_simple)); ++} ++ ++int main (int argc, char **argv) ++{ ++ g_test_init (&argc, &argv, NULL); ++ ++ g_test_add_func ("/glib-unix/closefrom", test_closefrom); ++#if GLIB_CHECK_VERSION (2, 38, 0) ++ g_test_add_func ("/glib-unix/closefrom/subprocess/einval", ++ test_closefrom_subprocess_einval); ++#endif ++ g_test_add_func ("/mainloop/steal-fd", test_steal_fd); ++ g_test_add_func ("/strfuncs/memdup2", test_memdup2); ++ g_test_add_func ("/strfuncs/strv-equal", test_strv_equal); ++ return g_test_run(); ++} +diff --git a/subprojects/libglnx/tests/test-libglnx-chase.c b/subprojects/libglnx/tests/test-libglnx-chase.c +new file mode 100644 +index 0000000..b0ce1b4 +--- /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-errors.c b/subprojects/libglnx/tests/test-libglnx-errors.c +index 4e91e02..3b2a485 100644 +--- a/subprojects/libglnx/tests/test-libglnx-errors.c ++++ b/subprojects/libglnx/tests/test-libglnx-errors.c +@@ -1,6 +1,7 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2017 Red Hat, Inc. ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +@@ -18,7 +19,7 @@ + * Boston, MA 02111-1307, USA. + */ + +-#include "config.h" ++#include "libglnx-config.h" + #include "libglnx.h" + #include + #include +diff --git a/subprojects/libglnx/tests/test-libglnx-fdio.c b/subprojects/libglnx/tests/test-libglnx-fdio.c +index 84ebb14..cfa5b64 100644 +--- a/subprojects/libglnx/tests/test-libglnx-fdio.c ++++ b/subprojects/libglnx/tests/test-libglnx-fdio.c +@@ -1,6 +1,7 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2017 Red Hat, Inc. ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +@@ -18,7 +19,7 @@ + * Boston, MA 02111-1307, USA. + */ + +-#include "config.h" ++#include "libglnx-config.h" + #include "libglnx.h" + #include + #include +@@ -235,6 +236,208 @@ test_filecopy (void) + g_assert (S_ISREG (stbuf.st_mode)); + } + ++static void ++test_filecopy_procfs (void) ++{ ++ const char * const pseudo_files[] = ++ { ++ /* A file in /proc that stat()s as empty (at least on Linux 5.15) */ ++ "/proc/version", ++ /* A file in /sys that stat()s as empty (at least on Linux 5.15) */ ++ "/sys/fs/cgroup/cgroup.controllers", ++ /* A file in /sys that stat()s as non-empty (at least on Linux 5.15) */ ++ "/sys/fs/ext4/features/meta_bg_resize", ++ }; ++ gsize i; ++ ++ for (i = 0; i < G_N_ELEMENTS (pseudo_files); i++) ++ { ++ _GLNX_TEST_DECLARE_ERROR(local_error, error); ++ g_autofree char *contents = NULL; ++ g_autofree char *contents_of_copy = NULL; ++ gsize len; ++ gsize len_copy; ++ ++ if (!g_file_get_contents (pseudo_files[i], &contents, &len, error)) ++ { ++ g_test_message ("Not testing %s: %s", ++ pseudo_files[i], local_error->message); ++ g_clear_error (&local_error); ++ continue; ++ } ++ ++ if (!glnx_file_copy_at (AT_FDCWD, pseudo_files[i], NULL, ++ AT_FDCWD, "copy", ++ (GLNX_FILE_COPY_OVERWRITE | ++ GLNX_FILE_COPY_NOCHOWN | ++ GLNX_FILE_COPY_NOXATTRS), ++ NULL, error)) ++ return; ++ ++ g_assert_no_error (local_error); ++ ++ if (!g_file_get_contents ("copy", &contents_of_copy, &len_copy, error)) ++ return; ++ ++ g_assert_no_error (local_error); ++ ++ g_assert_cmpstr (contents, ==, contents_of_copy); ++ g_assert_cmpuint (len, ==, len_copy); ++ } ++} ++ ++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; +@@ -245,9 +448,11 @@ int main (int argc, char **argv) + g_test_add_func ("/tmpfile", test_tmpfile); + g_test_add_func ("/stdio-file", test_stdio_file); + g_test_add_func ("/filecopy", test_filecopy); ++ g_test_add_func ("/filecopy-procfs", test_filecopy_procfs); + g_test_add_func ("/renameat2-noreplace", test_renameat2_noreplace); + g_test_add_func ("/renameat2-exchange", test_renameat2_exchange); + g_test_add_func ("/fstat", test_fstatat); ++ g_test_add_func ("/fd-reopen", test_fd_reopen); + + ret = g_test_run(); + +diff --git a/subprojects/libglnx/tests/test-libglnx-macros.c b/subprojects/libglnx/tests/test-libglnx-macros.c +index ffde8fa..ec669d3 100644 +--- a/subprojects/libglnx/tests/test-libglnx-macros.c ++++ b/subprojects/libglnx/tests/test-libglnx-macros.c +@@ -1,6 +1,7 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2017 Red Hat, Inc. ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +@@ -18,13 +19,20 @@ + * Boston, MA 02111-1307, USA. + */ + +-#include "config.h" ++#include "libglnx-config.h" + #include "libglnx.h" + #include + #include + #include + #include + ++static void ++test_info (void) ++{ ++ g_info ("hello, world"); ++ g_info ("answer=%d", 42); ++} ++ + static void + test_inset (void) + { +@@ -103,6 +111,7 @@ test_hash_table_foreach (void) + int main (int argc, char **argv) + { + g_test_init (&argc, &argv, NULL); ++ g_test_add_func ("/info", test_info); + g_test_add_func ("/inset", test_inset); + g_test_add_func ("/hash_table_foreach", test_hash_table_foreach); + return g_test_run(); +diff --git a/subprojects/libglnx/tests/test-libglnx-shutil.c b/subprojects/libglnx/tests/test-libglnx-shutil.c +index 6917b89..98e424a 100644 +--- a/subprojects/libglnx/tests/test-libglnx-shutil.c ++++ b/subprojects/libglnx/tests/test-libglnx-shutil.c +@@ -1,6 +1,8 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * +- * Copyright © 2017 Endless Mobile, Inc. ++ * Copyright © 2017-2019 Endless OS Foundation LLC ++ * Copyright © 2024 Collabora Ltd. ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +@@ -18,7 +20,7 @@ + * Boston, MA 02111-1307, USA. + */ + +-#include "config.h" ++#include "libglnx-config.h" + #include "libglnx.h" + #include + #include +@@ -28,9 +30,47 @@ + + #include "libglnx-testlib.h" + ++static void ++test_mkdir_p_parent_unsuitable (void) ++{ ++ _GLNX_TEST_SCOPED_TEMP_DIR; ++ _GLNX_TEST_DECLARE_ERROR(local_error, error); ++ glnx_autofd int dfd = -1; ++ ++ if (!glnx_ensure_dir (AT_FDCWD, "test", 0755, error)) ++ return; ++ if (!glnx_opendirat (AT_FDCWD, "test", FALSE, &dfd, error)) ++ return; ++ ++ if (!glnx_file_replace_contents_at (dfd, "file", ++ (const guint8 *) "", 0, ++ GLNX_FILE_REPLACE_NODATASYNC, ++ NULL, error)) ++ return; ++ ++ if (symlinkat ("nosuchtarget", dfd, "link") < 0) ++ { ++ glnx_throw_errno_prefix (error, "symlinkat"); ++ return; ++ } ++ ++ glnx_shutil_mkdir_p_at (dfd, "file/baz", 0755, NULL, error); ++ g_test_message ("mkdir %s -> %s", "file/baz", ++ local_error ? local_error->message : "success"); ++ g_assert_error (local_error, G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY); ++ g_clear_error (&local_error); ++ ++ glnx_shutil_mkdir_p_at (dfd, "link/baz", 0755, NULL, error); ++ g_test_message ("mkdir %s -> %s", "link/baz", ++ local_error ? local_error->message : "success"); ++ g_assert_error (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND); ++ g_clear_error (&local_error); ++} ++ + static void + test_mkdir_p_enoent (void) + { ++ _GLNX_TEST_SCOPED_TEMP_DIR; + _GLNX_TEST_DECLARE_ERROR(local_error, error); + glnx_autofd int dfd = -1; + +@@ -56,6 +96,7 @@ main (int argc, + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/mkdir-p/enoent", test_mkdir_p_enoent); ++ g_test_add_func ("/mkdir-p/parent-unsuitable", test_mkdir_p_parent_unsuitable); + + ret = g_test_run(); + +diff --git a/subprojects/libglnx/tests/test-libglnx-testing.c b/subprojects/libglnx/tests/test-libglnx-testing.c +new file mode 100644 +index 0000000..449481d +--- /dev/null ++++ b/subprojects/libglnx/tests/test-libglnx-testing.c +@@ -0,0 +1,371 @@ ++/* ++ * Copyright 2022 Simon McVittie ++ * SPDX-License-Identifier: LGPL-2.1-or-later ++ * ++ * 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.1 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, see ++ * . ++ */ ++ ++#include "libglnx-config.h" ++#include "libglnx.h" ++ ++#include ++#include ++ ++#include ++#include ++#include ++#include ++ ++#if GLIB_CHECK_VERSION (2, 38, 0) ++#define GTEST_TAP_OR_VERBOSE "--tap" ++#else ++#define GTEST_TAP_OR_VERBOSE "--verbose" ++#endif ++ ++static const char *null = NULL; ++static const char *nonnull = "not null"; ++ ++static void ++test_assertions (void) ++{ ++ const char *other_nonnull = "not null"; ++ g_autoptr(GVariant) va = g_variant_ref_sink (g_variant_new ("i", 42)); ++ g_autoptr(GVariant) vb = g_variant_ref_sink (g_variant_new ("i", 42)); ++ const char * const strv1[] = {"one", "two", NULL}; ++ const char * const strv2[] = {"one", "two", NULL}; ++ GStatBuf statbuf; ++ ++ g_assert_true (null == NULL); ++ g_assert_false (null != NULL); ++ g_assert_null (null); ++ g_assert_nonnull (nonnull); ++ g_assert_cmpmem (null, 0, null, 0); ++ g_assert_cmpmem (nonnull, strlen (nonnull), other_nonnull, strlen (other_nonnull)); ++ g_assert_cmpfloat_with_epsilon (1.0, 1.00001, 0.01); ++ g_assert_cmpvariant (va, vb); ++ g_assert_no_errno (g_stat ("/", &statbuf)); ++ g_assert_cmpstrv (NULL, NULL); ++ g_assert_cmpstrv (&null, &null); ++ g_assert_cmpstrv (strv1, strv2); ++} ++ ++static void ++test_assertion_failures (void) ++{ ++ static const char * const assertion_failures[] = ++ { ++ "true", ++ "false", ++ "nonnull", ++ "null", ++ "mem_null_nonnull", ++ "mem_nonnull_null", ++ "mem_len", ++ "mem_cmp", ++ "cmpfloat_with_epsilon", ++ "cmpvariant", ++ "errno", ++ "cmpstrv_null_nonnull", ++ "cmpstrv_nonnull_null", ++ "cmpstrv_len", ++ "cmpstrv_cmp", ++ }; ++ g_autoptr(GError) error = NULL; ++ g_autofree char *self = NULL; ++ g_autofree char *dir = NULL; ++ g_autofree char *exe = NULL; ++ gsize i; ++ ++ self = glnx_readlinkat_malloc (-1, "/proc/self/exe", NULL, &error); ++ g_assert_no_error (error); ++ ++ dir = g_path_get_dirname (self); ++ exe = g_build_filename (dir, "testing-helper", NULL); ++ ++ for (i = 0; i < G_N_ELEMENTS (assertion_failures); i++) ++ { ++ g_autofree char *out = NULL; ++ g_autofree char *err = NULL; ++ g_autofree char *name = g_strdup_printf ("/assertion-failure/%s", assertion_failures[i]); ++ int wait_status = -1; ++ const char *argv[] = { NULL, "assertion-failures", "-p", NULL, NULL, NULL }; ++ char *line; ++ char *saveptr = NULL; ++ ++ argv[0] = exe; ++ argv[3] = name; ++ argv[4] = GTEST_TAP_OR_VERBOSE; ++ g_test_message ("%s assertion-failures -p %s %s...", exe, name, GTEST_TAP_OR_VERBOSE); ++ ++ g_spawn_sync (NULL, /* cwd */ ++ (char **) argv, ++ NULL, /* envp */ ++ G_SPAWN_DEFAULT, ++ NULL, /* child setup */ ++ NULL, /* user data */ ++ &out, ++ &err, ++ &wait_status, ++ &error); ++ g_assert_no_error (error); ++ ++ g_assert_nonnull (out); ++ g_assert_nonnull (err); ++ ++ for (line = strtok_r (out, "\n", &saveptr); ++ line != NULL; ++ line = strtok_r (NULL, "\n", &saveptr)) ++ g_test_message ("stdout: %s", line); ++ ++ saveptr = NULL; ++ ++ for (line = strtok_r (err, "\n", &saveptr); ++ line != NULL; ++ line = strtok_r (NULL, "\n", &saveptr)) ++ g_test_message ("stderr: %s", line); ++ ++ g_test_message ("wait status: 0x%x", wait_status); ++ ++ /* It exited with a nonzero status that was not exit status 77 */ ++ G_STATIC_ASSERT (WIFEXITED (0)); ++ G_STATIC_ASSERT (WEXITSTATUS (0) == 0); ++ g_assert_cmphex (wait_status, !=, 0); ++ G_STATIC_ASSERT (WIFEXITED (77 << 8)); ++ G_STATIC_ASSERT (WEXITSTATUS (77 << 8) == 77); ++ g_assert_cmphex (wait_status, !=, (77 << 8)); ++ } ++} ++ ++static void ++test_failures (void) ++{ ++ static const char * const failures[] = ++ { ++ "fail", ++ "fail-printf", ++ }; ++ g_autoptr(GError) error = NULL; ++ g_autofree char *self = NULL; ++ g_autofree char *dir = NULL; ++ g_autofree char *exe = NULL; ++ gsize i; ++ ++ self = glnx_readlinkat_malloc (-1, "/proc/self/exe", NULL, &error); ++ g_assert_no_error (error); ++ ++ dir = g_path_get_dirname (self); ++ exe = g_build_filename (dir, "testing-helper", NULL); ++ ++ for (i = 0; i < G_N_ELEMENTS (failures); i++) ++ { ++ g_autofree char *out = NULL; ++ g_autofree char *err = NULL; ++ int wait_status = -1; ++ const char *argv[] = { NULL, NULL, NULL, NULL }; ++ char *line; ++ char *saveptr = NULL; ++ ++ argv[0] = exe; ++ argv[1] = failures[i]; ++ argv[2] = GTEST_TAP_OR_VERBOSE; ++ g_test_message ("%s %s %s...", exe, failures[i], GTEST_TAP_OR_VERBOSE); ++ ++ g_spawn_sync (NULL, /* cwd */ ++ (char **) argv, ++ NULL, /* envp */ ++ G_SPAWN_DEFAULT, ++ NULL, /* child setup */ ++ NULL, /* user data */ ++ &out, ++ &err, ++ &wait_status, ++ &error); ++ g_assert_no_error (error); ++ ++ for (line = strtok_r (out, "\n", &saveptr); ++ line != NULL; ++ line = strtok_r (NULL, "\n", &saveptr)) ++ g_test_message ("stdout: %s", line); ++ ++ saveptr = NULL; ++ ++ for (line = strtok_r (err, "\n", &saveptr); ++ line != NULL; ++ line = strtok_r (NULL, "\n", &saveptr)) ++ g_test_message ("stderr: %s", line); ++ ++ g_test_message ("wait status: 0x%x", wait_status); ++ ++ G_STATIC_ASSERT (WIFEXITED (0)); ++ G_STATIC_ASSERT (WEXITSTATUS (0) == 0); ++ G_STATIC_ASSERT (WIFEXITED (77 << 8)); ++ G_STATIC_ASSERT (WEXITSTATUS (77 << 8) == 77); ++ ++ g_assert_cmphex (wait_status, !=, 0); ++ g_assert_cmphex (wait_status, !=, (77 << 8)); ++ } ++} ++ ++static void ++test_skips (void) ++{ ++ static const char * const skips[] = ++ { ++ "skip", ++ "skip-printf", ++ "incomplete", ++ "incomplete-printf", ++ }; ++ g_autoptr(GError) error = NULL; ++ g_autofree char *self = NULL; ++ g_autofree char *dir = NULL; ++ g_autofree char *exe = NULL; ++ gsize i; ++ ++ self = glnx_readlinkat_malloc (-1, "/proc/self/exe", NULL, &error); ++ g_assert_no_error (error); ++ ++ dir = g_path_get_dirname (self); ++ exe = g_build_filename (dir, "testing-helper", NULL); ++ ++ for (i = 0; i < G_N_ELEMENTS (skips); i++) ++ { ++ g_autofree char *out = NULL; ++ g_autofree char *err = NULL; ++ int wait_status = -1; ++ const char *argv[] = { NULL, NULL, NULL, NULL }; ++ char *line; ++ char *saveptr = NULL; ++ ++ argv[0] = exe; ++ argv[1] = skips[i]; ++ argv[2] = GTEST_TAP_OR_VERBOSE; ++ g_test_message ("%s %s %s...", exe, skips[i], GTEST_TAP_OR_VERBOSE); ++ ++ g_spawn_sync (NULL, /* cwd */ ++ (char **) argv, ++ NULL, /* envp */ ++ G_SPAWN_DEFAULT, ++ NULL, /* child setup */ ++ NULL, /* user data */ ++ &out, ++ &err, ++ &wait_status, ++ &error); ++ g_assert_no_error (error); ++ ++ for (line = strtok_r (out, "\n", &saveptr); ++ line != NULL; ++ line = strtok_r (NULL, "\n", &saveptr)) ++ g_test_message ("stdout: %s", line); ++ ++ saveptr = NULL; ++ ++ for (line = strtok_r (err, "\n", &saveptr); ++ line != NULL; ++ line = strtok_r (NULL, "\n", &saveptr)) ++ g_test_message ("stderr: %s", line); ++ ++ g_test_message ("wait status: 0x%x", wait_status); ++ ++ G_STATIC_ASSERT (WIFEXITED (0)); ++ G_STATIC_ASSERT (WEXITSTATUS (0) == 0); ++ G_STATIC_ASSERT (WIFEXITED (77 << 8)); ++ G_STATIC_ASSERT (WEXITSTATUS (77 << 8) == 77); ++ ++ /* Ideally the exit status is 77, but it might be 0 with older GLib */ ++ if (wait_status != 0) ++ g_assert_cmphex (wait_status, ==, (77 << 8)); ++ } ++} ++ ++static void ++test_successes (void) ++{ ++ static const char * const successes[] = ++ { ++ "messages", ++ "pass", ++ "summary", ++ }; ++ g_autoptr(GError) error = NULL; ++ g_autofree char *self = NULL; ++ g_autofree char *dir = NULL; ++ g_autofree char *exe = NULL; ++ gsize i; ++ ++ self = glnx_readlinkat_malloc (-1, "/proc/self/exe", NULL, &error); ++ g_assert_no_error (error); ++ ++ dir = g_path_get_dirname (self); ++ exe = g_build_filename (dir, "testing-helper", NULL); ++ ++ for (i = 0; i < G_N_ELEMENTS (successes); i++) ++ { ++ g_autofree char *out = NULL; ++ g_autofree char *err = NULL; ++ int wait_status = -1; ++ const char *argv[] = { NULL, NULL, NULL, NULL }; ++ char *line; ++ char *saveptr = NULL; ++ ++ argv[0] = exe; ++ argv[1] = successes[i]; ++ argv[2] = GTEST_TAP_OR_VERBOSE; ++ g_test_message ("%s %s %s...", exe, successes[i], GTEST_TAP_OR_VERBOSE); ++ ++ g_spawn_sync (NULL, /* cwd */ ++ (char **) argv, ++ NULL, /* envp */ ++ G_SPAWN_DEFAULT, ++ NULL, /* child setup */ ++ NULL, /* user data */ ++ &out, ++ &err, ++ &wait_status, ++ &error); ++ g_assert_no_error (error); ++ ++ for (line = strtok_r (out, "\n", &saveptr); ++ line != NULL; ++ line = strtok_r (NULL, "\n", &saveptr)) ++ g_test_message ("stdout: %s", line); ++ ++ saveptr = NULL; ++ ++ for (line = strtok_r (err, "\n", &saveptr); ++ line != NULL; ++ line = strtok_r (NULL, "\n", &saveptr)) ++ g_test_message ("stderr: %s", line); ++ ++ g_test_message ("wait status: 0x%x", wait_status); ++ ++ G_STATIC_ASSERT (WIFEXITED (0)); ++ G_STATIC_ASSERT (WEXITSTATUS (0) == 0); ++ g_assert_cmphex (wait_status, ==, 0); ++ } ++} ++ ++int ++main (int argc, char **argv) ++{ ++ g_test_init (&argc, &argv, NULL); ++ g_test_add_func ("/assertions", test_assertions); ++ g_test_add_func ("/assertion_failures", test_assertion_failures); ++ g_test_add_func ("/failures", test_failures); ++ g_test_add_func ("/skips", test_skips); ++ g_test_add_func ("/successes", test_successes); ++ return g_test_run(); ++} +diff --git a/subprojects/libglnx/tests/test-libglnx-xattrs.c b/subprojects/libglnx/tests/test-libglnx-xattrs.c +index 82def4a..1de9acd 100644 +--- a/subprojects/libglnx/tests/test-libglnx-xattrs.c ++++ b/subprojects/libglnx/tests/test-libglnx-xattrs.c +@@ -1,6 +1,7 @@ + /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2017 Red Hat, Inc. ++ * SPDX-License-Identifier: LGPL-2.0-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +@@ -18,7 +19,7 @@ + * Boston, MA 02111-1307, USA. + */ + +-#include "config.h" ++#include "libglnx-config.h" + #include "libglnx.h" + #include + #include +@@ -121,7 +122,7 @@ do_write_run (GLnxDirFdIterator *dfd_iter, GError **error) + if (!glnx_fd_get_all_xattrs (fd, ¤t_xattrs, NULL, error)) + return FALSE; + +- for (int i = 0; i < g_variant_n_children (current_xattrs); i++) ++ for (size_t i = 0; i < g_variant_n_children (current_xattrs); i++) + { + const char *name, *value; + g_variant_get_child (current_xattrs, i, "(^&ay^&ay)", &name, &value); +@@ -181,7 +182,7 @@ xattr_thread (gpointer data) + g_autoptr(GError) local_error = NULL; + GError **error = &local_error; + struct XattrWorker *worker = data; +- guint64 end_time = g_get_monotonic_time () + XATTR_THREAD_RUN_TIME_USECS; ++ gint64 end_time = g_get_monotonic_time () + XATTR_THREAD_RUN_TIME_USECS; + guint n_read = 0; + + while (g_get_monotonic_time () < end_time) +diff --git a/subprojects/libglnx/tests/testing-helper.c b/subprojects/libglnx/tests/testing-helper.c +new file mode 100644 +index 0000000..0886678 +--- /dev/null ++++ b/subprojects/libglnx/tests/testing-helper.c +@@ -0,0 +1,342 @@ ++/* ++ * Based on glib/tests/testing-helper.c from GLib ++ * ++ * Copyright 2018-2022 Collabora Ltd. ++ * Copyright 2019 Руслан Ижбулатов ++ * Copyright 2018-2022 Endless OS Foundation LLC ++ * ++ * SPDX-License-Identifier: LGPL-2.1-or-later ++ * ++ * 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.1 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, see ++ * . ++ */ ++ ++#include "libglnx-config.h" ++#include "libglnx.h" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++static const char *null = NULL; ++static const char *nonnull = "not null"; ++ ++static void ++test_pass (void) ++{ ++} ++ ++static void ++test_messages (void) ++{ ++ g_test_message ("This message has multiple lines.\n" ++ "In older GLib, it would corrupt TAP output.\n" ++ "That's why libglnx provides a wrapper.\n"); ++} ++ ++static void ++test_assertion_failure_true (void) ++{ ++ g_assert_true (null != NULL); ++} ++ ++static void ++test_assertion_failure_false (void) ++{ ++ g_assert_false (null == NULL); ++} ++ ++static void ++test_assertion_failure_nonnull (void) ++{ ++ g_assert_nonnull (null); ++} ++ ++static void ++test_assertion_failure_null (void) ++{ ++ g_assert_null (nonnull); ++} ++ ++static void ++test_assertion_failure_mem_null_nonnull (void) ++{ ++ g_assert_cmpmem (null, 0, nonnull, strlen (nonnull)); ++} ++ ++static void ++test_assertion_failure_mem_nonnull_null (void) ++{ ++ g_assert_cmpmem (nonnull, strlen (nonnull), null, 0); ++} ++ ++static void ++test_assertion_failure_mem_len (void) ++{ ++ g_assert_cmpmem (nonnull, strlen (nonnull), nonnull, 0); ++} ++ ++static void ++test_assertion_failure_mem_cmp (void) ++{ ++ g_assert_cmpmem (nonnull, 4, nonnull + 4, 4); ++} ++ ++static void ++test_assertion_failure_cmpfloat_with_epsilon (void) ++{ ++ g_assert_cmpfloat_with_epsilon (1.0, 1.5, 0.001); ++} ++ ++static void ++test_assertion_failure_cmpvariant (void) ++{ ++ g_autoptr(GVariant) a = g_variant_ref_sink (g_variant_new ("i", 42)); ++ g_autoptr(GVariant) b = g_variant_ref_sink (g_variant_new ("u", 42)); ++ ++ g_assert_cmpvariant (a, b); ++} ++ ++static void ++test_assertion_failure_errno (void) ++{ ++ g_assert_no_errno (mkdir ("/", 0755)); ++} ++ ++static void ++test_assertion_failure_cmpstrv_null_nonnull (void) ++{ ++ const char * const b[] = { NULL }; ++ ++ g_assert_cmpstrv (NULL, b); ++} ++ ++static void ++test_assertion_failure_cmpstrv_nonnull_null (void) ++{ ++ const char * const a[] = { NULL }; ++ ++ g_assert_cmpstrv (a, NULL); ++} ++ ++static void ++test_assertion_failure_cmpstrv_len (void) ++{ ++ const char * const a[] = { "one", NULL }; ++ const char * const b[] = { NULL }; ++ ++ g_assert_cmpstrv (a, b); ++} ++ ++static void ++test_assertion_failure_cmpstrv_cmp (void) ++{ ++ const char * const a[] = { "one", "two", NULL }; ++ const char * const b[] = { "one", "three", NULL }; ++ ++ g_assert_cmpstrv (a, b); ++} ++ ++static void ++test_skip (void) ++{ ++ g_test_skip ("not enough tea"); ++} ++ ++static void ++test_skip_printf (void) ++{ ++ const char *beverage = "coffee"; ++ ++ g_test_skip_printf ("not enough %s", beverage); ++} ++ ++static void ++test_fail (void) ++{ ++ g_test_fail (); ++} ++ ++static void ++test_fail_printf (void) ++{ ++ g_test_fail_printf ("this test intentionally left failing"); ++} ++ ++static void ++test_incomplete (void) ++{ ++ g_test_incomplete ("mind reading not implemented yet"); ++} ++ ++static void ++test_incomplete_printf (void) ++{ ++ const char *operation = "telekinesis"; ++ ++ g_test_incomplete_printf ("%s not implemented yet", operation); ++} ++ ++static void ++test_summary (void) ++{ ++ g_test_summary ("Tests that g_test_summary() works with TAP, by outputting a " ++ "known summary message in testing-helper, and checking for " ++ "it in the TAP output later."); ++} ++ ++int ++main (int argc, ++ char *argv[]) ++{ ++ char *argv1; ++ ++ setlocale (LC_ALL, ""); ++ ++#ifdef G_OS_WIN32 ++ /* Windows opens std streams in text mode, with \r\n EOLs. ++ * Sometimes it's easier to force a switch to binary mode than ++ * to account for extra \r in testcases. ++ */ ++ setmode (fileno (stdout), O_BINARY); ++#endif ++ ++ g_return_val_if_fail (argc > 1, 1); ++ argv1 = argv[1]; ++ ++ if (argc > 2) ++ memmove (&argv[1], &argv[2], (argc - 2) * sizeof (char *)); ++ ++ argc -= 1; ++ argv[argc] = NULL; ++ ++ if (g_strcmp0 (argv1, "init-null-argv0") == 0) ++ { ++ int test_argc = 0; ++ char *test_argva[1] = { NULL }; ++ char **test_argv = test_argva; ++ ++ /* Test that `g_test_init()` can handle being called with an empty argv ++ * and argc == 0. While this isn’t recommended, it is possible for another ++ * process to use execve() to call a gtest process this way, so we’d ++ * better handle it gracefully. ++ * ++ * This test can’t be run after `g_test_init()` has been called normally, ++ * as it isn’t allowed to be called more than once in a process. */ ++ g_test_init (&test_argc, &test_argv, NULL); ++ ++ return 0; ++ } ++ ++ g_test_init (&argc, &argv, NULL); ++ g_test_disable_crash_reporting (); ++#if GLIB_CHECK_VERSION(2, 38, 0) ++ g_test_set_nonfatal_assertions (); ++#endif ++ ++ if (g_strcmp0 (argv1, "pass") == 0) ++ { ++ g_test_add_func ("/pass", test_pass); ++ } ++ else if (g_strcmp0 (argv1, "messages") == 0) ++ { ++ g_test_add_func ("/messages", test_messages); ++ } ++ else if (g_strcmp0 (argv1, "skip") == 0) ++ { ++ g_test_add_func ("/skip", test_skip); ++ } ++ else if (g_strcmp0 (argv1, "skip-printf") == 0) ++ { ++ g_test_add_func ("/skip-printf", test_skip_printf); ++ } ++ else if (g_strcmp0 (argv1, "incomplete") == 0) ++ { ++ g_test_add_func ("/incomplete", test_incomplete); ++ } ++ else if (g_strcmp0 (argv1, "incomplete-printf") == 0) ++ { ++ g_test_add_func ("/incomplete-printf", test_incomplete_printf); ++ } ++ else if (g_strcmp0 (argv1, "fail") == 0) ++ { ++ g_test_add_func ("/fail", test_fail); ++ } ++ else if (g_strcmp0 (argv1, "fail-printf") == 0) ++ { ++ g_test_add_func ("/fail-printf", test_fail_printf); ++ } ++ else if (g_strcmp0 (argv1, "all-non-failures") == 0) ++ { ++ g_test_add_func ("/pass", test_pass); ++ g_test_add_func ("/skip", test_skip); ++ g_test_add_func ("/incomplete", test_incomplete); ++ } ++ else if (g_strcmp0 (argv1, "all") == 0) ++ { ++ g_test_add_func ("/pass", test_pass); ++ g_test_add_func ("/skip", test_skip); ++ g_test_add_func ("/incomplete", test_incomplete); ++ g_test_add_func ("/fail", test_fail); ++ } ++ else if (g_strcmp0 (argv1, "skip-options") == 0) ++ { ++ /* The caller is expected to skip some of these with ++ * -p/-r, -s/-x and/or --GTestSkipCount */ ++ g_test_add_func ("/a", test_pass); ++ g_test_add_func ("/b", test_pass); ++ g_test_add_func ("/b/a", test_pass); ++ g_test_add_func ("/b/b", test_pass); ++ g_test_add_func ("/b/b/a", test_pass); ++ g_test_add_func ("/prefix/a", test_pass); ++ g_test_add_func ("/prefix/b/b", test_pass); ++ g_test_add_func ("/prefix-long/a", test_pass); ++ g_test_add_func ("/c/a", test_pass); ++ g_test_add_func ("/d/a", test_pass); ++ } ++ else if (g_strcmp0 (argv1, "summary") == 0) ++ { ++ g_test_add_func ("/summary", test_summary); ++ } ++ else if (g_strcmp0 (argv1, "assertion-failures") == 0) ++ { ++ /* Use -p to select a specific one of these */ ++#define T(x) g_test_add_func ("/assertion-failure/" #x, test_assertion_failure_ ## x) ++ T (true); ++ T (false); ++ T (nonnull); ++ T (null); ++ T (mem_null_nonnull); ++ T (mem_nonnull_null); ++ T (mem_len); ++ T (mem_cmp); ++ T (cmpfloat_with_epsilon); ++ T (cmpvariant); ++ T (errno); ++ T (cmpstrv_null_nonnull); ++ T (cmpstrv_nonnull_null); ++ T (cmpstrv_len); ++ T (cmpstrv_cmp); ++#undef T ++ } ++ else ++ { ++ g_assert_not_reached (); ++ } ++ ++ return g_test_run (); ++} +diff --git a/subprojects/libglnx/tests/use-as-subproject/.gitignore b/subprojects/libglnx/tests/use-as-subproject/.gitignore +new file mode 100644 +index 0000000..ec6149f +--- /dev/null ++++ b/subprojects/libglnx/tests/use-as-subproject/.gitignore +@@ -0,0 +1,5 @@ ++# Copyright 2022 Collabora Ltd. ++# SPDX-License-Identifier: LGPL-2.0-or-later ++ ++/_build/ ++/subprojects/ +diff --git a/subprojects/libglnx/tests/use-as-subproject/README b/subprojects/libglnx/tests/use-as-subproject/README +new file mode 100644 +index 0000000..cc43a09 +--- /dev/null ++++ b/subprojects/libglnx/tests/use-as-subproject/README +@@ -0,0 +1,8 @@ ++This is a simple example of a project that uses libglnx as a subproject. ++The intention is that if this project can successfully build and use libglnx ++as a subproject, then so could Flatpak. ++ ++ +diff --git a/subprojects/libglnx/tests/use-as-subproject/config.h b/subprojects/libglnx/tests/use-as-subproject/config.h +new file mode 100644 +index 0000000..dffc647 +--- /dev/null ++++ b/subprojects/libglnx/tests/use-as-subproject/config.h +@@ -0,0 +1,6 @@ ++/* ++ * Copyright 2022 Collabora Ltd. ++ * SPDX-License-Identifier: LGPL-2.0-or-later ++ */ ++ ++#error Should not use superproject config.h to compile libglnx +diff --git a/subprojects/libglnx/tests/use-as-subproject/dummy-config.h.in b/subprojects/libglnx/tests/use-as-subproject/dummy-config.h.in +new file mode 100644 +index 0000000..bffb52a +--- /dev/null ++++ b/subprojects/libglnx/tests/use-as-subproject/dummy-config.h.in +@@ -0,0 +1,6 @@ ++/* ++ * Copyright 2022 Collabora Ltd. ++ * SPDX-License-Identifier: LGPL-2.0-or-later ++ */ ++ ++#error Should not use superproject generated config.h to compile libglnx +diff --git a/subprojects/libglnx/tests/use-as-subproject/meson.build b/subprojects/libglnx/tests/use-as-subproject/meson.build +new file mode 100644 +index 0000000..59fd736 +--- /dev/null ++++ b/subprojects/libglnx/tests/use-as-subproject/meson.build +@@ -0,0 +1,48 @@ ++# Copyright 2022 Collabora Ltd. ++# SPDX-License-Identifier: LGPL-2.0-or-later ++ ++project( ++ 'use-libglnx-as-subproject', ++ 'c', ++ default_options : [ ++ 'c_std=gnu99', ++ 'warning_level=3', ++ ], ++ version : '0', ++ meson_version : '>=0.49.0', ++) ++ ++configure_file( ++ copy : true, ++ input : 'dummy-config.h.in', ++ output : 'config.h', ++) ++ ++glib_dep = dependency('glib-2.0') ++ ++libglnx = subproject('libglnx') ++libglnx_dep = libglnx.get_variable('libglnx_dep') ++libglnx_testlib_dep = libglnx.get_variable('libglnx_testlib_dep') ++ ++# This executable is compiled at warning_level=3 by default ++executable( ++ 'trivial', ++ 'trivial.c', ++ dependencies : [glib_dep], ++) ++ ++# These can't be compiled at warning_level=3 because they use non-ISO ++# compiler features in the libglnx headers, which would be warnings or ++# errors with -Wpedantic ++executable( ++ 'use-libglnx', ++ 'use-libglnx.c', ++ dependencies : [libglnx_dep, glib_dep], ++ override_options : ['warning_level=2'], ++) ++executable( ++ 'use-testlib', ++ 'use-testlib.c', ++ dependencies : [libglnx_testlib_dep, glib_dep], ++ override_options : ['warning_level=2'], ++) +diff --git a/subprojects/libglnx/tests/use-as-subproject/trivial.c b/subprojects/libglnx/tests/use-as-subproject/trivial.c +new file mode 100644 +index 0000000..4b364f1 +--- /dev/null ++++ b/subprojects/libglnx/tests/use-as-subproject/trivial.c +@@ -0,0 +1,15 @@ ++/* ++ * Copyright 2022 Collabora Ltd. ++ * SPDX-License-Identifier: LGPL-2.0-or-later ++ */ ++ ++#include ++ ++int ++main (void) ++{ ++ GError *error = NULL; ++ ++ g_clear_error (&error); ++ return 0; ++} +diff --git a/subprojects/libglnx/tests/use-as-subproject/use-libglnx.c b/subprojects/libglnx/tests/use-as-subproject/use-libglnx.c +new file mode 100644 +index 0000000..0e14db0 +--- /dev/null ++++ b/subprojects/libglnx/tests/use-as-subproject/use-libglnx.c +@@ -0,0 +1,16 @@ ++/* ++ * Copyright 2022 Collabora Ltd. ++ * SPDX-License-Identifier: LGPL-2.0-or-later ++ */ ++ ++#include ++ ++int ++main (void) ++{ ++ GError *error = NULL; ++ ++ glnx_throw (&error, "whatever"); ++ g_clear_error (&error); ++ return 0; ++} +diff --git a/subprojects/libglnx/tests/use-as-subproject/use-testlib.c b/subprojects/libglnx/tests/use-as-subproject/use-testlib.c +new file mode 100644 +index 0000000..9a955b4 +--- /dev/null ++++ b/subprojects/libglnx/tests/use-as-subproject/use-testlib.c +@@ -0,0 +1,17 @@ ++/* ++ * Copyright 2022 Collabora Ltd. ++ * SPDX-License-Identifier: LGPL-2.0-or-later ++ */ ++ ++#include ++#include ++ ++int ++main (void) ++{ ++ _GLNX_TEST_DECLARE_ERROR (local_error, error); ++ ++ glnx_throw (error, "Whatever"); ++ g_clear_error (&local_error); ++ return 0; ++} +diff --git a/subprojects/Makefile-libglnx.am.inc b/subprojects/Makefile-libglnx.am.inc +index abcdef0..1234567 100644 +--- a/subprojects/Makefile-libglnx.am.inc ++++ b/subprojects/Makefile-libglnx.am.inc +@@ -20,6 +20,11 @@ EXTRA_DIST += \ + subprojects/libglnx/libglnx.m4 \ + $(NULL) + ++BUILT_SOURCES += libglnx-config.h ++CLEANFILES += libglnx-config.h ++libglnx-config.h: Makefile ++ echo '#include "config.h"' > $@ ++ + libglnx_la_SOURCES = \ + subprojects/libglnx/glnx-macros.h \ + subprojects/libglnx/glnx-backport-autocleanups.h \ +@@ -27,6 +32,10 @@ libglnx_la_SOURCES = \ + subprojects/libglnx/glnx-backport-autoptr.h \ + subprojects/libglnx/glnx-backports.h \ + subprojects/libglnx/glnx-backports.c \ ++ subprojects/libglnx/glnx-backport-testutils.h \ ++ subprojects/libglnx/glnx-backport-testutils.c \ ++ subprojects/libglnx/glnx-chase.h \ ++ subprojects/libglnx/glnx-chase.c \ + subprojects/libglnx/glnx-local-alloc.h \ + subprojects/libglnx/glnx-local-alloc.c \ + subprojects/libglnx/glnx-errors.h \ +-- +2.54.0 + diff --git a/SPECS/flatpak.spec b/SPECS/flatpak.spec index 452dca7..211c263 100644 --- a/SPECS/flatpak.spec +++ b/SPECS/flatpak.spec @@ -3,7 +3,7 @@ Name: flatpak Version: 1.12.9 -Release: 3%{?dist} +Release: 4%{?dist} Summary: Application deployment framework for desktop apps License: LGPLv2+ @@ -21,6 +21,44 @@ Patch0: flatpak-Revert-selinux-Permit-using-systemd-userdbd.patch # Backported upstream patch for CVE-2024-42472 Patch1: flatpak-1.12.x-CVE-2024-42472.patch +# CVE-2026-34078 +Patch100: flatpak-1.12.x-update-libglnx-for-glnx-chaseseat.patch +Patch101: CVE-2026-34078-1-flatpak-bwrap-add-dup-ing-variant-flatpak-bwrap-add-args-data-fd-dup.patch +Patch102: CVE-2026-34078-2-utils-add-flatpak-parse-fd.patch +Patch103: CVE-2026-34078-3-flatpak-bwrap-use-glnx-close-fd-as-clear-func.patch +Patch104: CVE-2026-34078-4-run-use-o-path-fds-for-the-runtime-and-app-deploy-directories.patch +Patch105: CVE-2026-34078-5-run-add-usr-fd-and-app-fd-options.patch +Patch106: CVE-2026-34078-6-run-add-ro-bind-fds-to-flatpak-run-app.patch +Patch107: CVE-2026-34078-7-run-add-ro-bind-fd-options.patch +Patch108: CVE-2026-34078-8-portal-use-bind-fd-app-fd-and-usr-fd-options-to-avoid-races.patch +Patch109: CVE-2026-34078-9-run-fix-checking-wrong-variable-in-runtime-fd-selection.patch +Patch110: CVE-2026-34078-10-run-mount-original-app-on-run-parent-app-when-using-app-path.patch +Patch111: CVE-2026-34078-11-portal-update-max-fd-after-creating-the-instance-id-pipe.patch +Patch112: CVE-2026-34078-12-run-fix-fd-tracking-in-flatpak-run-add-app-info-args.patch +Patch113: CVE-2026-34078-13-utils-improve-error-message-when-passing-an-fd-numer-which-is-not-a-fd.patch +Patch114: CVE-2026-34078-14-run-do-not-close-bind-ro-bind.patch +Patch115: CVE-2026-34078-15-run-use-the-same-fd-validation-for-all-fd-options.patch +Patch116: CVE-2026-34078-16-run-add-bind-fd-and-ro-bind-fd-binds-after-all-other-binds.patch +Patch117: CVE-2026-34078-17-portal-use-g-array-index-to-read-from-expose-fds-expose-fds-ro.patch +Patch118: CVE-2026-34078-18-run-fix-backport-mistake.patch +Patch119: CVE-2026-34078-19-run-cope-with-an-empty-runtime.patch +Patch120: CVE-2026-34078-20-dir-in-apply-extra-data-don-t-assume-there-is-always-a-runtime.patch +Patch121: CVE-2026-34078-21-utils-add-flatpak-set-cloexec.patch +Patch122: CVE-2026-34078-22-run-context-mark-fd-arguments-as-close-on-exec.patch +Patch123: CVE-2026-34078-23-utils-move-flatpak-get-path-for-fd-to-here.patch +Patch124: CVE-2026-34078-24-portal-avoid-crash-if-sandbox-expose-ro-fd-is-out-of-range.patch +Patch125: CVE-2026-34078-25-portal-log-and-ignore-unusable-sandbox-expose-fds-instead-of-erroring.patch +Patch126: CVE-2026-34078-26-portal-reinstate-flatpak-get-path-for-fd-checks.patch +Patch127: CVE-2026-34078-27-libtest-allow-adding-a-new-ref-to-an-existing-temporary-ostree-repo.patch +Patch128: CVE-2026-34078-28-app-context-never-close-fds-0-1-or-2.patch +Patch129: CVE-2026-34078-29-app-context-factor-out-flatpak-accept-fd-argument.patch + +# CVE-2026-34079 +Patch130: CVE-2026-34079-1-utils-only-remove-cached-files-in-the-cache-directory.patch +Patch131: CVE-2026-34079-2-utils-do-not-follow-symlinks-in-local-open-file.patch +Patch132: CVE-2026-34079-3-system-helper-only-remove-an-ongoing-pull-if-users-match.patch + + BuildRequires: pkgconfig(appstream-glib) BuildRequires: pkgconfig(dconf) BuildRequires: pkgconfig(fuse) @@ -280,6 +318,12 @@ fi %changelog +* Wed May 20 2026 Jan Grulich - 1.12.9-4 +- Fix arbitrary code execution via crafted symlinks in sandbox-expose options + Resolves: RHEL-165633 +- Fix arbitrary file deletion on host via improper cache file path validation + Resolves: RHEL-170160 + * Wed Sep 04 2024 Kalev Lember - 1.12.9-3 - Fix previous changelog entry