From 1c391d734c2ed554dca2b66358ddeceb60149663 Mon Sep 17 00:00:00 2001 From: Simon McVittie Date: Thu, 5 Mar 2026 13:20:41 +0000 Subject: [PATCH 1/8] backports, local-alloc: Provide a backport of g_autofd The functionality that was prototyped in libglnx as glnx_fd_close and then glnx_autofd was later added to GLib as g_autofd. glnx_close_fd() doesn't have a direct equivalent in GLib, so keep it intact, but using the backported _glnx_clear_fd_ignore_error as its implementation. g_clear_fd() is the closest thing in GLib, but g_clear_fd() guarantees to set errno on failure (making it useful for error-checking), whereas glnx_close_fd() guarantees to leave errno untouched, making it more useful for cleanup code paths that recover from a syscall or similar function that sets errno: int fd = ...; success = (fsync (fd) == 0); glnx_close_fd (&fd); return success; /* if false, errno indicates why fsync failed */ (Of course in many cases, including this simple example, it would have been easier to use g_autofd.) glnx_autofd and glnx_fd_close are now equivalent to the backport of g_autofd, so document them as deprecated. There's essentially no cost to retaining them, so don't apply deprecation attributes. Signed-off-by: Simon McVittie --- glnx-backports.h | 21 +++++++++++++++++++++ glnx-local-alloc.h | 31 ++++++++----------------------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/glnx-backports.h b/glnx-backports.h index 9ec11e4cf..a0177ad99 100644 --- a/glnx-backports.h +++ b/glnx-backports.h @@ -82,6 +82,27 @@ g_clear_fd (int *fd_ptr, } #endif +/* This is part of the backport of g_autofd, but we define it + * unconditionally because it's also used to implement glnx_close_fd() */ +static inline void +_glnx_clear_fd_ignore_error (int *fd_ptr) +{ + /* Don't overwrite thread-local errno if closing the fd fails */ + int errsv = errno; + + if (!g_clear_fd (fd_ptr, NULL)) + { + /* Do nothing: we ignore all errors, except for EBADF which + * is a programming error, checked for by g_close(). */ + } + + errno = errsv; +} + +#if !GLIB_CHECK_VERSION(2, 76, 0) +#define g_autofd __attribute__((cleanup(_glnx_clear_fd_ignore_error))) +#endif + #if !GLIB_CHECK_VERSION(2, 40, 0) #define g_info(...) g_log (G_LOG_DOMAIN, G_LOG_LEVEL_INFO, __VA_ARGS__) #endif diff --git a/glnx-local-alloc.h b/glnx-local-alloc.h index a6e0e9b4f..9ac8a027d 100644 --- a/glnx-local-alloc.h +++ b/glnx-local-alloc.h @@ -51,38 +51,23 @@ glnx_local_obj_unref (void *v) * glnx_close_fd: * @fdp: Pointer to fd * - * 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. + * Same as `g_clear_fd()`, but ignoring the error (if any) and making sure + * not to alter `errno`. As a result, this function can be used for cleanup + * in contexts where `errno` needs to be preserved. */ -static inline void -glnx_close_fd (int *fdp) -{ - int errsv; - - g_assert (fdp); - - int fd = g_steal_fd (fdp); - if (fd >= 0) - { - errsv = errno; - if (close (fd) < 0) - g_assert (errno != EBADF); - errno = errsv; - } -} +#define glnx_close_fd _glnx_clear_fd_ignore_error /** * glnx_fd_close: * - * Deprecated in favor of `glnx_autofd`. + * Deprecated in favor of `g_autofd`. */ -#define glnx_fd_close __attribute__((cleanup(glnx_close_fd))) +#define glnx_fd_close g_autofd /** * glnx_autofd: * - * Call close() on a variable location when it goes out of scope. + * Deprecated in favor of `g_autofd`. */ -#define glnx_autofd __attribute__((cleanup(glnx_close_fd))) +#define glnx_autofd g_autofd G_END_DECLS From 606dbdbc200bcc88d66edf82b25322bb53949549 Mon Sep 17 00:00:00 2001 From: Simon McVittie Date: Thu, 5 Mar 2026 16:01:43 +0000 Subject: [PATCH 2/8] testlib: Add an assertion that a fd has really been closed We can't easily assert this without triggering warnings from tools like valgrind by doing an invalid operation on a closed fd, so we only check this when under `-m undefined`. Originally contributed to GLib 2.76 in GNOME/glib@b3934133 "gstdio: Add g_clear_fd() and g_autofd". The implementation in GLib used g_fsync() as a portable thing that we can do with a fd, but that function is newer than our minimum GLib version, and libglnx isn't portable to non-Unix anyway, so use fnctl() instead. Signed-off-by: Simon McVittie --- tests/libglnx-testlib.c | 19 +++++++++++++++++++ tests/libglnx-testlib.h | 5 ++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/libglnx-testlib.c b/tests/libglnx-testlib.c index 3eb2ba143..22c38981d 100644 --- a/tests/libglnx-testlib.c +++ b/tests/libglnx-testlib.c @@ -23,6 +23,7 @@ #include "libglnx-testlib.h" #include +#include #include @@ -72,3 +73,21 @@ _glnx_test_auto_temp_dir_leave (_GLnxTestAutoTempDir *dir) g_free (dir->old_cwd); g_free (dir); } + +void +_glnx_test_assert_fd_was_closed (int fd) +{ + /* We can't tell a fd was really closed without behaving as though it + * was still valid */ + if (g_test_undefined ()) + { + int result; + int errsv; + + errno = 0; + result = fcntl (fd, F_GETFD, 0); + errsv = errno; + g_assert_cmpint (result, <, 0); + g_assert_cmpint (errsv, ==, EBADF); + } +} diff --git a/tests/libglnx-testlib.h b/tests/libglnx-testlib.h index dccc7e558..009de74b8 100644 --- a/tests/libglnx-testlib.h +++ b/tests/libglnx-testlib.h @@ -1,7 +1,7 @@ /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- * * Copyright (C) 2017 Red Hat, Inc. - * Copyright 2019 Collabora Ltd. + * Copyright 2019-2022 Collabora Ltd. * SPDX-License-Identifier: LGPL-2.0-or-later * * This library is free software; you can redistribute it and/or @@ -23,6 +23,7 @@ #pragma once #include +#include #include "glnx-backport-autoptr.h" @@ -47,3 +48,5 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC(_GLnxTestAutoTempDir, _glnx_test_auto_temp_dir_lea #define _GLNX_TEST_SCOPED_TEMP_DIR \ G_GNUC_UNUSED g_autoptr(_GLnxTestAutoTempDir) temp_dir = _glnx_test_auto_temp_dir_enter () + +void _glnx_test_assert_fd_was_closed (int fd); From b478f3e7374a6ee85a71ea2cd42640c4c590b5bd Mon Sep 17 00:00:00 2001 From: Simon McVittie Date: Thu, 5 Mar 2026 16:03:16 +0000 Subject: [PATCH 3/8] tests: Assert that glnx_close_fd preserves errno We document glnx_close_fd as preserving errno, so let's assert that it really does. There are three code paths we need to exercise: 1. fd < 0: glnx_close_fd does nothing, successfully 2. fd >= 0 and close() succeeds 3. fd >= 0 and close() fails The first two are easy, but it's difficult to make close() fail on-demand with only valid code. close(2) documents EIO, but it's difficult to cause an I/O error on-demand. Similarly, close(2) documents ENOSPC and EDQUOT on NFS, but we are unlikely to have a full NFS filesystem available during testing. Instead, we can trigger a failure via the programming error of passing a fd to glnx_close_fd that was already closed, which makes close(2) fail with EBADF. In older libglnx, we wouldn't have been able to test this because it caused an assertion failure, but in GLib and new libglnx it only causes a critical warning, which we can catch and ignore. See also GLib commit GNOME/glib@f1f711dc "tests: Test EBADF and errno handling when closing fds". GLib doesn't have a 1:1 equivalent of glnx_close_fd as public API, but an internal version is used to implement g_autofd. Signed-off-by: Simon McVittie --- tests/test-libglnx-fdio.c | 121 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/tests/test-libglnx-fdio.c b/tests/test-libglnx-fdio.c index cfa5b648a..e237589c9 100644 --- a/tests/test-libglnx-fdio.c +++ b/tests/test-libglnx-fdio.c @@ -29,6 +29,125 @@ #include "libglnx-testlib.h" +static void +test_close (void) +{ + g_autoptr(GError) error = NULL; + int errsv; + int fd = -2; + int fd_borrowed; + + g_test_summary ("Exercise glnx_close_fd"); + + /* Closing a non-fd is a no-op, and preserves errno. + * EILSEQ is an arbitrary valid value of errno that is unlikely + * to be set accidentally as a side-effect of I/O. */ + g_test_message ("Closing a non-fd is a no-op and preserves errno..."); + errno = EILSEQ; + glnx_close_fd (&fd); + errsv = errno; + g_assert_cmpint (fd, ==, -1); + g_assert_cmpint (errsv, ==, EILSEQ); + + /* Closing a valid fd really closes it, and preserves errno. */ + g_test_message ("Closing a valid fd preserves errno..."); + glnx_opendirat (AT_FDCWD, "/", TRUE, &fd, &error); + g_assert_no_error (error); + g_assert_cmpint (fd, >=, 0); + fd_borrowed = fd; + errno = EILSEQ; + glnx_close_fd (&fd); + errsv = errno; + g_assert_cmpint (fd, ==, -1); + g_assert_cmpint (errsv, ==, EILSEQ); + _glnx_test_assert_fd_was_closed (fd_borrowed); +} + +/* Exercise glnx_close_fd in the case where close() fails. + * The only convenient way we can arrange for this to happen is to use + * an invalid fd, which is a programming error. + * + * This function is only run under g_test_undefined(), and assumes the + * implementation detail that GLib responds to that programming error + * with a critical warning rather than a fatal error. */ +static void +test_close_ebadf_subprocess (void) +{ + g_autoptr(GError) error = NULL; + int errsv; + int fd = -2; + int non_fd; + + /* Preparation: Open a fd, and close it, leaving non_fd set to the + * file descriptor number. */ + glnx_opendirat (AT_FDCWD, "/", TRUE, &fd, &error); + g_assert_no_error (error); + g_assert_cmpint (fd, >=, 0); + close (fd); + non_fd = fd; + _glnx_test_assert_fd_was_closed (non_fd); + + /* "Closing" the non-fd provokes a critical warning. */ + g_log_set_always_fatal (G_LOG_FATAL_MASK); + g_log_set_fatal_mask ("GLib", G_LOG_FATAL_MASK); + + errno = EILSEQ; + glnx_close_fd (&non_fd); + errsv = errno; + g_assert_cmpint (non_fd, ==, -1); + + /* We preserved errno. */ + g_assert_cmpint (errsv, ==, EILSEQ); + g_print ("Closing invalid fd preserved errno\n"); +} + +static void +test_close_ebadf (void) +{ + g_test_summary ("Exercise glnx_close_fd when close() fails"); + + /* If close() fails, it still preserves errno. + * The only convenient way to make close() fail on-demand is EBADF. */ + + if (g_test_subprocess ()) + { + test_close_ebadf_subprocess (); + return; + } + + if (g_test_undefined ()) + { + g_test_message ("Closing invalid fd preserves errno..."); + +#if GLIB_CHECK_VERSION (2, 38, 0) + g_test_trap_subprocess (NULL, 0, G_TEST_SUBPROCESS_DEFAULT); +#else + if (g_test_trap_fork (0, 0)) + { + test_close_ebadf_subprocess (); + _exit (0); + } +#endif + + g_test_trap_assert_passed (); + g_test_trap_assert_stdout ("*Closing invalid fd preserved errno*"); +#if !GLIB_CHECK_VERSION(2, 76, 0) + /* We can assert that our backport emits this message */ + g_test_trap_assert_stderr ("*_glnx_close(fd:*) failed with EBADF*"); +#else + /* We can't assert anything this specific about GLib's, + * but as an implementation detail, it's currently very similar */ +# if 0 + g_test_trap_assert_stderr ("*g_close(fd:*) failed with EBADF*"); +# endif +#endif + } + else + { + g_test_skip ("Can't test this without provoking undefined behaviour"); + } +} + static gboolean renameat_test_setup (int *out_srcfd, int *out_destfd, GError **error) @@ -445,6 +564,8 @@ int main (int argc, char **argv) g_test_init (&argc, &argv, NULL); + g_test_add_func ("/close", test_close); + g_test_add_func ("/close/ebadf", test_close_ebadf); g_test_add_func ("/tmpfile", test_tmpfile); g_test_add_func ("/stdio-file", test_stdio_file); g_test_add_func ("/filecopy", test_filecopy); From dbc0d4f5531d512b83e3b8ba1905d5d91f1fe781 Mon Sep 17 00:00:00 2001 From: bbhtt Date: Wed, 8 Apr 2026 19:01:37 +0530 Subject: [PATCH 4/8] backports: Wrap g_clear_fd to silence deprecations for newer Glib When building with newer GLib with GLIB_VERSION_MAX_ALLOWED set to < 2.76 in downstream, g_clear_fd is marked deprecated in libglnx and triggers deprecation warnings at each call site. This forwards to g_clear_fd while silencing deprecation warnings for Glib >= 2.76 Fixes: https://gitlab.gnome.org/GNOME/libglnx/-/issues/7 --- glnx-backports.h | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/glnx-backports.h b/glnx-backports.h index 9ec11e4cf..904b2e095 100644 --- a/glnx-backports.h +++ b/glnx-backports.h @@ -80,6 +80,19 @@ g_clear_fd (int *fd_ptr, return _glnx_close (fd, error); G_GNUC_END_IGNORE_DEPRECATIONS } + +#else + +static inline gboolean +_glnx_clear_fd (int *fd_ptr, + GError **error) +{ + G_GNUC_BEGIN_IGNORE_DEPRECATIONS + return g_clear_fd (fd_ptr, error); + G_GNUC_END_IGNORE_DEPRECATIONS +} +#define g_clear_fd _glnx_clear_fd + #endif #if !GLIB_CHECK_VERSION(2, 40, 0) From 72b1d539e984fbfc6c4e7c6323eea33de251c9a0 Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Tue, 21 Apr 2026 12:56:40 +0200 Subject: [PATCH 5/8] build: Add meson.override_dependency('libglnx', libglnx_dep) This makes it possible to use a standard dependency('libglnx') call in a parent project when libglnx is used as a subproject. --- meson.build | 3 +++ 1 file changed, 3 insertions(+) diff --git a/meson.build b/meson.build index 0fff39d21..ecffe6aa1 100644 --- a/meson.build +++ b/meson.build @@ -110,6 +110,9 @@ libglnx_dep = declare_dependency( dependencies : libglnx_deps, include_directories : libglnx_inc, link_with : libglnx) +if meson.version().version_compare('>= 0.54.0') + meson.override_dependency('libglnx', libglnx_dep) +endif subdir('tests') From 015ec9c63e75badf18352a1e45d2ebe786985fd1 Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Thu, 26 Mar 2026 23:12:14 +0100 Subject: [PATCH 6/8] fdio: Add support for name_to_handle_at --- glnx-chase.h | 2 + glnx-fdio.c | 179 ++++++++++++++++++++++++++++++++++++++ glnx-fdio.h | 7 ++ glnx-missing-syscall.h | 99 +++++++++++++++++++++ meson.build | 1 + tests/test-libglnx-fdio.c | 106 ++++++++++++++++++++++ 6 files changed, 394 insertions(+) diff --git a/glnx-chase.h b/glnx-chase.h index 05dac12da..c68faf3d3 100644 --- a/glnx-chase.h +++ b/glnx-chase.h @@ -8,6 +8,8 @@ #include +#include + typedef enum _GlnxChaseFlags { /* Default */ GLNX_CHASE_DEFAULT = 0, diff --git a/glnx-fdio.c b/glnx-fdio.c index 59261668a..62b0dcae8 100644 --- a/glnx-fdio.c +++ b/glnx-fdio.c @@ -33,6 +33,7 @@ #include #include +#include #include #include #include @@ -42,6 +43,18 @@ #include #include +/* From systemd mountpoint-util.c at d2b27a7: + * This is the original MAX_HANDLE_SZ definition from the kernel, when the API + * was introduced. We use that in place of any more currently defined value to + * future-proof things: if the size is increased in the API headers, and our code + * is recompiled then it would cease working on old kernels, as those refuse any + * sizes larger than this value with EINVAL right-away. Hence, let's disconnect + * ourselves from any such API changes, and stick to the original definition + * from when it was introduced. We use it as a start value only anyway (see + * below), and hence should be able to deal with large file handles anyway. + */ +#define ORIGINAL_MAX_HANDLE_SZ 128 + /* The standardized version of BTRFS_IOC_CLONE */ #ifndef FICLONE #define FICLONE _IOW(0x94, 9, int) @@ -1279,3 +1292,169 @@ glnx_fd_reopen (int fd, return g_steal_fd (&new_fd); } + +static gboolean +glnx_name_to_handle_at_internal (int fd, + int flags, + struct file_handle **handle_out, + uint64_t *mnt_id_out, + GError **error) +{ + size_t handle_bytes = ORIGINAL_MAX_HANDLE_SZ; + + for (;;) + { + g_autofree struct file_handle *handle = NULL; + uint64_t mnt_id_unique = 0; + unsigned int mnt_id = 0; + int *mnt_id_ptr; + int r; + + /* The kernel ABI involves an int * for backward compatibility, + * but with AT_HANDLE_MNT_ID_UNIQUE it's really expecting a + * uint64_t and will write a full 64-bit ID into it. */ + if ((flags & AT_HANDLE_MNT_ID_UNIQUE)) + mnt_id_ptr = (int *) &mnt_id_unique; + else + mnt_id_ptr = (int *) &mnt_id; + + handle = g_malloc0 (offsetof (struct file_handle, f_handle) + handle_bytes); + handle->handle_bytes = handle_bytes; + r = name_to_handle_at (fd, "", + handle, + mnt_id_ptr, + flags | AT_EMPTY_PATH); + + if (r < 0) + { + if (errno != EOVERFLOW) + return glnx_throw_errno (error); + + if (handle->handle_bytes <= handle_bytes) + return glnx_throw (error, "No file handle available"); + } + + if (r >= 0) + { + if (handle_out) + *handle_out = g_steal_pointer (&handle); + if (mnt_id_out) + *mnt_id_out = (flags & AT_HANDLE_MNT_ID_UNIQUE) ? mnt_id_unique : mnt_id; + return TRUE; + } + + handle_bytes = handle->handle_bytes; + } +} + +/** + * glnx_name_to_handle_at: + * @dfd: Directory FD to stat beneath + * @path: Path to get the handle to beneath @dfd + * @flags: Flags + * @handle_out: (out) (transfer full): Return location for the `struct file_handle` + * @mnt_id_out: (out caller-allocates): Return location for the mount id + * @error: Return location for a #GError, or %NULL + * + * Wrapper around name_to_handle_at() which adds #GError support, takes care of + * allocating the right size, and falls back to glnx_statx() for + * AT_HANDLE_MNT_ID_UNIQUE. + * + * The @mnt_id_out is pointer to a 64 bit location, but can contain either a + * traditional 32 bit mount id or a 64 bit unique mount id if + * AT_HANDLE_MNT_ID_UNIQUE is set. + * + * The @flags must be a combination of AT_SYMLINK_FOLLOW, AT_EMPTY_PATH, + * AT_HANDLE_FID, AT_HANDLE_MNT_ID_UNIQUE. + * + * Returns: %TRUE on success, or %FALSE setting both @error and `errno` + * Since: UNRELEASED + */ +gboolean +glnx_name_to_handle_at (int dfd, + const char *path, + int flags, + struct file_handle **handle_out, + uint64_t *mnt_id_out, + GError **error) +{ + int fd = -1; + glnx_autofd int fd_owned = -1; + uint64_t mnt_id; + g_autoptr(GError) local_error = NULL; + + g_return_val_if_fail (dfd >= 0 || dfd == AT_FDCWD, FALSE); + g_return_val_if_fail (path != NULL, FALSE); + g_return_val_if_fail ((flags & ~(AT_SYMLINK_FOLLOW | + AT_EMPTY_PATH | + AT_HANDLE_FID | + AT_HANDLE_MNT_ID_UNIQUE)) == 0, FALSE); + + if ((flags & AT_EMPTY_PATH) && path[0] == '\0') + { + fd = dfd; + } + else + { + int chase_flags = GLNX_CHASE_NO_AUTOMOUNT; + + if ((flags & AT_SYMLINK_FOLLOW) == 0) + chase_flags |= GLNX_CHASE_NOFOLLOW; + + fd = fd_owned = glnx_chaseat (dfd, path, chase_flags, error); + if (fd < 0) + return FALSE; + } + + if (glnx_name_to_handle_at_internal (fd, + flags, + handle_out, mnt_id_out, + &local_error)) + return TRUE; + + if (errno != EINVAL || (flags & AT_HANDLE_MNT_ID_UNIQUE) == 0) + { + g_propagate_prefixed_error (error, g_steal_pointer (&local_error), + "name_to_handle_at: "); + return FALSE; + } + g_clear_error (&local_error); + + { + struct glnx_statx stx; + int statx_flags = AT_EMPTY_PATH; + + if ((flags & AT_SYMLINK_FOLLOW) == 0) + statx_flags |= AT_SYMLINK_NOFOLLOW; + + if (!glnx_statx (fd, "", statx_flags, GLNX_STATX_MNT_ID_UNIQUE, &stx, error)) + { + g_prefix_error (error, "statx: "); + return FALSE; + } + + if ((stx.stx_mask & GLNX_STATX_MNT_ID_UNIQUE) == 0) + { + errno = ENODATA; + return glnx_throw_errno_prefix (error, + "unique mount ID not in statx result"); + } + + mnt_id = stx.stx_mnt_id; + } + + if (!glnx_name_to_handle_at_internal (fd, + flags & (~AT_HANDLE_MNT_ID_UNIQUE), + handle_out, NULL, + &local_error)) + { + g_propagate_prefixed_error (error, g_steal_pointer (&local_error), + "name_to_handle_at: "); + return FALSE; + } + + if (mnt_id_out) + *mnt_id_out = mnt_id; + + return TRUE; +} diff --git a/glnx-fdio.h b/glnx-fdio.h index acd546dfb..1a5c4ee99 100644 --- a/glnx-fdio.h +++ b/glnx-fdio.h @@ -419,4 +419,11 @@ int glnx_fd_reopen (int fd, int flags, GError **error); +gboolean glnx_name_to_handle_at (int dfd, + const char *path, + int flags, + struct file_handle **handle_out, + uint64_t *mnt_id_out, + GError **error); + G_END_DECLS diff --git a/glnx-missing-syscall.h b/glnx-missing-syscall.h index f3bdb3baf..2f1026406 100644 --- a/glnx-missing-syscall.h +++ b/glnx-missing-syscall.h @@ -589,3 +589,102 @@ inline_openat2 (int dfd, #define openat2 inline_openat2 #define HAVE_OPENAT2 #endif + +#ifndef __IGNORE_name_to_handle_at +# if defined(__aarch64__) +# define systemd_NR_name_to_handle_at 264 +# elif defined(__alpha__) +# define systemd_NR_name_to_handle_at 497 +# elif defined(__arc__) || defined(__tilegx__) +# define systemd_NR_name_to_handle_at 264 +# elif defined(__arm__) +# define systemd_NR_name_to_handle_at 370 +# elif defined(__i386__) +# define systemd_NR_name_to_handle_at 341 +# elif defined(__ia64__) +# define systemd_NR_name_to_handle_at 1326 +# elif defined(__loongarch_lp64) +# define systemd_NR_name_to_handle_at 264 +# elif defined(__m68k__) +# define systemd_NR_name_to_handle_at 340 +# elif defined(_MIPS_SIM) +# if _MIPS_SIM == _MIPS_SIM_ABI32 +# define systemd_NR_name_to_handle_at 4339 +# elif _MIPS_SIM == _MIPS_SIM_NABI32 +# define systemd_NR_name_to_handle_at 6303 +# elif _MIPS_SIM == _MIPS_SIM_ABI64 +# define systemd_NR_name_to_handle_at 5298 +# else +# error "Unknown MIPS ABI" +# endif +# elif defined(__hppa__) +# define systemd_NR_name_to_handle_at 325 +# elif defined(__powerpc__) +# define systemd_NR_name_to_handle_at 345 +# elif defined(__riscv) +# if __riscv_xlen == 32 +# define systemd_NR_name_to_handle_at 264 +# elif __riscv_xlen == 64 +# define systemd_NR_name_to_handle_at 264 +# else +# error "Unknown RISC-V ABI" +# endif +# elif defined(__s390__) +# define systemd_NR_name_to_handle_at 335 +# elif defined(__sparc__) +# define systemd_NR_name_to_handle_at 332 +# elif defined(__x86_64__) +# if defined(__ILP32__) +# define systemd_NR_name_to_handle_at (303 | /* __X32_SYSCALL_BIT */ 0x40000000) +# else +# define systemd_NR_name_to_handle_at 303 +# endif +# elif !defined(missing_arch_template) +# warning "name_to_handle_at() syscall number is unknown for your architecture" +# endif + +/* may be an (invalid) negative number due to libseccomp, see PR 13319 */ +# if defined __NR_name_to_handle_at && __NR_name_to_handle_at >= 0 +# if defined systemd_NR_name_to_handle_at +G_STATIC_ASSERT (__NR_name_to_handle_at == systemd_NR_name_to_handle_at); +# endif +# else +# if defined __NR_name_to_handle_at +# undef __NR_name_to_handle_at +# endif +# if defined systemd_NR_name_to_handle_at && systemd_NR_name_to_handle_at >= 0 +# define __NR_name_to_handle_at systemd_NR_name_to_handle_at +# endif +# endif +#endif + +#ifndef AT_HANDLE_FID +#define AT_HANDLE_FID 0x200 +#endif + +#ifndef AT_HANDLE_MNT_ID_UNIQUE +#define AT_HANDLE_MNT_ID_UNIQUE 0x001 +#endif + +#ifndef AT_HANDLE_CONNECTABLE +#define AT_HANDLE_CONNECTABLE 0x002 +#endif + +#if !HAVE_DECL_NAME_TO_HANDLE_AT && defined(__NR_name_to_handle_at) +struct file_handle { + unsigned int handle_bytes; + int handle_type; + unsigned char f_handle[0]; +}; + +static inline int +name_to_handle_at (int fd, + const char *name, + struct file_handle *handle, + int *mnt_id, + int flags) +{ + return syscall(__NR_name_to_handle_at, fd, name, handle, mnt_id, flags); +} + +#endif diff --git a/meson.build b/meson.build index 0fff39d21..ca44f1d94 100644 --- a/meson.build +++ b/meson.build @@ -26,6 +26,7 @@ check_functions = [ 'renameat2', 'memfd_create', 'copy_file_range', + 'name_to_handle_at', ] conf = configuration_data() foreach check_function : check_functions diff --git a/tests/test-libglnx-fdio.c b/tests/test-libglnx-fdio.c index cfa5b648a..3004345b6 100644 --- a/tests/test-libglnx-fdio.c +++ b/tests/test-libglnx-fdio.c @@ -286,6 +286,111 @@ test_filecopy_procfs (void) } } +static void +test_name_to_handle_at (void) +{ + g_autoptr(GError) error = NULL; + g_autofree struct file_handle *handle1 = NULL; + g_autofree struct file_handle *handle2 = NULL; + g_autofree struct file_handle *handle3 = NULL; + uint64_t mnt_id1 = 0; + uint64_t mnt_id2 = 0; + uint64_t mnt_id3 = 0; + glnx_autofd int dfd = -1; + gboolean ok; + + /* Create a test directory and file */ + ok = glnx_shutil_mkdir_p_at_open (AT_FDCWD, "handle_test", 0755, &dfd, NULL, &error); + g_assert_no_error (error); + g_assert_true (ok); + + ok = glnx_file_replace_contents_at (dfd, "testfile", + (const guint8 *)"test", 4, + GLNX_FILE_REPLACE_NODATASYNC, NULL, &error); + g_assert_no_error (error); + g_assert_true (ok); + + ok = glnx_name_to_handle_at (dfd, "testfile", 0, &handle1, &mnt_id1, &error); + + /* Skip the test if the syscall is not supported */ + if (!ok && g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) + return g_test_skip ("name_to_handle_at not supported"); + + /* Test 1: Get handle for a regular file */ + g_assert_no_error (error); + g_assert_true (ok); + g_assert_nonnull (handle1); + g_assert_cmpuint (mnt_id1, >, 0); + + /* Test 2: Get handle for the same file again - should have same handle and mount ID */ + ok = glnx_name_to_handle_at (dfd, "testfile", 0, &handle2, &mnt_id2, &error); + g_assert_no_error (error); + g_assert_true (ok); + g_assert_nonnull (handle2); + g_assert_cmpuint (mnt_id1, ==, mnt_id2); + g_assert_cmpuint (handle1->handle_bytes, ==, handle2->handle_bytes); + g_assert_cmpuint (handle1->handle_type, ==, handle2->handle_type); + g_assert_cmpmem (handle1->f_handle, handle1->handle_bytes, + handle2->f_handle, handle2->handle_bytes); + + g_clear_pointer (&handle1, g_free); + g_clear_pointer (&handle2, g_free); + + /* Test 3: Get handle for a directory */ + ok = glnx_name_to_handle_at (AT_FDCWD, "handle_test", 0, &handle1, &mnt_id1, &error); + g_assert_no_error (error); + g_assert_true (ok); + g_assert_nonnull (handle1); + g_assert_cmpuint (mnt_id1, >, 0); + + g_clear_pointer (&handle1, g_free); + + /* Test 4: Test with AT_EMPTY_PATH */ + ok = glnx_name_to_handle_at (dfd, "", AT_EMPTY_PATH, &handle1, &mnt_id1, &error); + g_assert_no_error (error); + g_assert_true (ok); + g_assert_nonnull (handle1); + g_assert_cmpuint (mnt_id1, >, 0); + + g_clear_pointer (&handle1, g_free); + + /* Test 5: Create symlink and test AT_SYMLINK_FOLLOW */ + g_assert_no_errno (symlinkat ("testfile", dfd, "testlink")); + + ok = glnx_name_to_handle_at (dfd, "testlink", 0, &handle1, &mnt_id1, &error); + g_assert_no_error (error); + g_assert_true (ok); + g_assert_nonnull (handle1); + + ok = glnx_name_to_handle_at (dfd, "testlink", AT_SYMLINK_FOLLOW, &handle2, &mnt_id2, &error); + g_assert_no_error (error); + g_assert_true (ok); + g_assert_nonnull (handle2); + + /* files are on the same mount, so we should get the same kind of handle */ + g_assert_true (handle1->handle_bytes == handle2->handle_bytes); + g_assert_true (handle1->handle_type == handle2->handle_type); + /* handle1 != handle2 */ + g_assert_false (memcmp (handle1->f_handle, handle2->f_handle, handle1->handle_bytes) == 0); + + /* Following the symlink should give us the same handle as the target */ + ok = glnx_name_to_handle_at (dfd, "testfile", 0, &handle3, &mnt_id3, &error); + g_assert_no_error (error); + g_assert_true (ok); + g_assert_cmpmem (handle2->f_handle, handle2->handle_bytes, + handle3->f_handle, handle3->handle_bytes); + + g_clear_pointer (&handle1, g_free); + g_clear_pointer (&handle2, g_free); + g_clear_pointer (&handle3, g_free); + + /* Test 6: Error case - non-existent file */ + ok = glnx_name_to_handle_at (dfd, "nosuchfile", 0, &handle1, &mnt_id1, &error); + g_assert_false (ok); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND); + g_clear_error (&error); +} + static void test_fd_reopen (void) { @@ -452,6 +557,7 @@ int main (int argc, char **argv) g_test_add_func ("/renameat2-noreplace", test_renameat2_noreplace); g_test_add_func ("/renameat2-exchange", test_renameat2_exchange); g_test_add_func ("/fstat", test_fstatat); + g_test_add_func ("/name-to-handle-at", test_name_to_handle_at); g_test_add_func ("/fd-reopen", test_fd_reopen); ret = g_test_run(); From 4f8674709dce57e7f5422a475f61556162f930d8 Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Fri, 12 Jun 2026 13:42:13 +0200 Subject: [PATCH 7/8] chase: Add internal glnx_chaseat_full for a strategic callback It takes a callback which gets called every time we try to open the next segment of the path. This allows implementing more specific and advanced use cases to be implemented without adding more complexity to the chase algorithm itself. --- glnx-chase.c | 110 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 73 insertions(+), 37 deletions(-) diff --git a/glnx-chase.c b/glnx-chase.c index 9ad2fe3b3..d7edf34e5 100644 --- a/glnx-chase.c +++ b/glnx-chase.c @@ -46,6 +46,13 @@ #define GLNX_CHASE_ALL_FLAGS \ (GLNX_CHASE_ALL_DEBUG_FLAGS | GLNX_CHASE_ALL_REGULAR_FLAGS) +typedef int (* GlnxChaseCallback)(int next_fd, + int current_fd, + const char *segment, + int open_tree_flags, + gpointer user_data, + GError **error); + typedef GQueue GlnxStatxQueue; static void @@ -328,10 +335,12 @@ extract_next_segment (const char **remaining, * we're in full control over the resolving. */ static int -chase_manual (int dirfd, - const char *path, - GlnxChaseFlags flags, - GError **error) +chase_manual (int dirfd, + const char *path, + GlnxChaseFlags flags, + GlnxChaseCallback callback, + gpointer user_data, + GError **error) { gboolean is_absolute; g_autofree char *buffer = NULL; @@ -349,6 +358,7 @@ chase_manual (int dirfd, * - none of the resolve flags are set (they would require work here) * - NO_AUTOMOUNT is set (chase_open_tree only triggers the automount for * last component in some cases) + * - there is no callback * * TODO: if we have a guarantee that the open_tree syscall works, we can * shortcut even without GLNX_CHASE_NO_AUTOMOUNT @@ -356,7 +366,8 @@ chase_manual (int dirfd, if ((flags & (GLNX_CHASE_NO_AUTOMOUNT | GLNX_CHASE_RESOLVE_BENEATH | GLNX_CHASE_RESOLVE_IN_ROOT | - GLNX_CHASE_RESOLVE_NO_SYMLINKS)) == GLNX_CHASE_NO_AUTOMOUNT) + GLNX_CHASE_RESOLVE_NO_SYMLINKS)) == GLNX_CHASE_NO_AUTOMOUNT && + callback == NULL) { GlnxChaseFlags open_tree_flags = (flags & (GLNX_CHASE_NOFOLLOW | GLNX_CHASE_ALL_DEBUG_FLAGS)); @@ -466,10 +477,25 @@ chase_manual (int dirfd, GlnxChaseFlags open_tree_flags = GLNX_CHASE_NOFOLLOW | (flags & (GLNX_CHASE_NO_AUTOMOUNT | GLNX_CHASE_ALL_DEBUG_FLAGS)); + g_autoptr(GError) local_error = NULL; + + next_fd = chase_open_tree (fd, segment, open_tree_flags, &local_error); + + /* Note that the callback can be called with next_fd < 0. + * If so, the error is already set, but may be cleared by + * the callback if it can recover from an error that already + * occurred. */ + if (callback) + { + next_fd = callback (next_fd, fd, segment, open_tree_flags, + user_data, &local_error); + } - next_fd = chase_open_tree (fd, segment, open_tree_flags, error); if (next_fd < 0) - return -1; + { + g_propagate_error (error, g_steal_pointer (&local_error)); + return -1; + } } if (!glnx_chase_statx (next_fd, no_automount, &st, error)) @@ -602,30 +628,13 @@ chase_manual (int dirfd, return g_steal_fd (&owned_fd); } -/** - * glnx_chaseat: - * @dirfd: a directory file descriptor - * @path: a path - * @flags: combination of GlnxChaseFlags flags - * @error: a #GError - * - * Behaves similar to openat, but with a number of differences: - * - * - All file descriptors which get returned are O_PATH and O_CLOEXEC. If you - * want to actually open the file for reading or writing, use glnx_fd_reopen, - * openat, or other at-style functions. - * - By default, automounts get triggered and the O_PATH fd will point to inodes - * in the newly mounted filesystem if an automount is encountered. This can be - * turned off with GLNX_CHASE_NO_AUTOMOUNT. - * - The GLNX_CHASE_RESOLVE_ flags can be used to safely deal with symlinks. - * - * Returns: the chased file, or -1 with @error set on error - */ -int -glnx_chaseat (int dirfd, - const char *path, - GlnxChaseFlags flags, - GError **error) +static int +glnx_chaseat_full (int dirfd, + const char *path, + GlnxChaseFlags flags, + GlnxChaseCallback callback, + gpointer user_data, + GError **error) { static gboolean can_openat2 = TRUE; glnx_autofd int fd = -1; @@ -643,15 +652,14 @@ glnx_chaseat (int dirfd, g_return_val_if_fail ((must_flags & (must_flags - 1)) == 0, -1); } - /* TODO: Add a callback which is called for every resolved path segment, to - * allow users to verify and expand the functionality safely. */ - /* We need the manual impl for NO_AUTOMOUNT, and we can skip this, if we don't * have openat2 at all. * Technically racy (static, not synced), but both paths work fine so it * doesn't matter. */ - if (can_openat2 && (flags & GLNX_CHASE_NO_AUTOMOUNT) == 0 && - (flags & GLNX_CHASE_DEBUG_NO_OPENAT2) == 0) + if (can_openat2 && + (flags & GLNX_CHASE_NO_AUTOMOUNT) == 0 && + (flags & GLNX_CHASE_DEBUG_NO_OPENAT2) == 0 && + callback == NULL) { uint64_t openat2_flags = 0; uint64_t openat2_resolve = 0; @@ -690,7 +698,7 @@ glnx_chaseat (int dirfd, if (fd < 0) { - fd = chase_manual (dirfd, path, flags, error); + fd = chase_manual (dirfd, path, flags, callback, user_data, error); if (fd < 0) return -1; } @@ -744,6 +752,34 @@ glnx_chaseat (int dirfd, return g_steal_fd (&fd); } +/** + * glnx_chaseat: + * @dirfd: a directory file descriptor + * @path: a path + * @flags: combination of GlnxChaseFlags flags + * @error: a #GError + * + * Behaves similar to openat, but with a number of differences: + * + * - All file descriptors which get returned are O_PATH and O_CLOEXEC. If you + * want to actually open the file for reading or writing, use glnx_fd_reopen, + * openat, or other at-style functions. + * - By default, automounts get triggered and the O_PATH fd will point to inodes + * in the newly mounted filesystem if an automount is encountered. This can be + * turned off with GLNX_CHASE_NO_AUTOMOUNT. + * - The GLNX_CHASE_RESOLVE_ flags can be used to safely deal with symlinks. + * + * Returns: the chased file, or -1 with @error set on error + */ +int +glnx_chaseat (int dirfd, + const char *path, + GlnxChaseFlags flags, + GError **error) +{ + return glnx_chaseat_full (dirfd, path, flags, NULL, NULL, error); +} + /** * glnx_chase_and_statxat: * @dirfd: a directory file descriptor From ae7355612a68d39472ff3699104845d2cc5dfb4d Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Fri, 12 Jun 2026 13:45:22 +0200 Subject: [PATCH 8/8] chase: Add glnx_chase_and_mkdirat We found that there is a common use case where we need to get a subdirectory (potentially multiple levels) which might not exist yet. Adding another flag for this to GlnxChaseFlags is what systemd has done, but creating a directory takes a mode, so the flag creates directories with a fixed mode. This approach instead takes the mode as argument. --- glnx-chase.c | 87 ++++++++++++++++++++++++++++++++++++++ glnx-chase.h | 6 +++ tests/test-libglnx-chase.c | 69 ++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+) diff --git a/glnx-chase.c b/glnx-chase.c index d7edf34e5..2251cb71f 100644 --- a/glnx-chase.c +++ b/glnx-chase.c @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -823,3 +824,89 @@ glnx_chase_and_statxat (int dirfd, return g_steal_fd (&fd); } + +static int +chase_and_mkdir (int next_fd, + int current_fd, + const char *segment, + G_GNUC_UNUSED int open_tree_flags, + gpointer user_data, + GError **error) +{ + mode_t mode = GPOINTER_TO_INT (user_data); + glnx_autofd int new_fd = -1; + + /* if chase managed to get the next segment, we already got our answer */ + if (next_fd >= 0) + return next_fd; + + /* if the problem isn't that the file doesn't exist, we propagate the error */ + g_assert (*error != NULL); + if (!g_error_matches (*error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return -1; + g_clear_error (error); + + /* create the directory with the specified mode */ + if (!glnx_ensure_dir (current_fd, segment, mode, error)) + return -1; + + /* Get a fd to the created dir. + * This is racy, meaning another process can modify the filesystem and we + * might open a directory that we did not create and might have a different + * mode. There isn't a kernel API which returns a fd or an inode which makes + * this race unavoidable. This is fine though because the semantics of + * `glnx_chase_and_mkdirat` accept arbitrary directories (even with different + * modes) when chasing the path. + */ + new_fd = openat (current_fd, segment, + O_CLOEXEC | O_PATH | O_DIRECTORY | O_NOFOLLOW); + if (new_fd < 0) + return glnx_fd_throw_errno_prefix (error, "opening created dir failed"); + + return g_steal_fd (&new_fd); +} + +/** + * glnx_chase_and_mkdirat: + * @dirfd: a directory file descriptor + * @path: a path + * @flags: restricted combination of GlnxChaseFlags flags + * @mode: the mode for new directories + * @error: a #GError + * + * Same as glnx_chase with `GLNX_CHASE_MUST_BE_DIRECTORY`, but when a path + * segment does not exist, a directory is created for the segment with the mode + * @mode. + * This essentially implement a fd-relative equivalent of g_mkdir_with_parents() + * or `mkdir -p`. + * Note, that directories in the path which already exist can have arbitrary + * modes. + * + * See glnx_chaseat for the meaning of @dirfd and @path. + * + * The @flags argument is the same as in glnx_chaseat, but setting + * `GLNX_CHASE_MUST_BE_REGULAR`, `GLNX_CHASE_MUST_BE_SOCKET`, or + * `GLNX_CHASE_MUST_BE_DIRECTORY` is an error. + * + * Returns: the chased file, or -1 with @error set on error + */ +int +glnx_chase_and_mkdirat (int dirfd, + const char *path, + GlnxChaseFlags flags, + mode_t mode, + GError **error) +{ + g_return_val_if_fail ((flags & ~(GLNX_CHASE_ALL_FLAGS)) == 0, -1); + g_return_val_if_fail ((flags & (GLNX_CHASE_MUST_BE_REGULAR | + GLNX_CHASE_MUST_BE_SOCKET | + GLNX_CHASE_MUST_BE_DIRECTORY)) == 0, -1); + g_return_val_if_fail ((mode & ~((mode_t) 07777)) == 0, -1); + + /* This function always implies GLNX_CHASE_MUST_BE_DIRECTORY */ + flags |= GLNX_CHASE_MUST_BE_DIRECTORY; + + return glnx_chaseat_full (dirfd, path, flags, + chase_and_mkdir, GINT_TO_POINTER (mode), + error); +} diff --git a/glnx-chase.h b/glnx-chase.h index c68faf3d3..a4cb43d61 100644 --- a/glnx-chase.h +++ b/glnx-chase.h @@ -50,4 +50,10 @@ int glnx_chase_and_statxat (int dirfd, struct glnx_statx *statbuf, GError **error); +int glnx_chase_and_mkdirat (int dirfd, + const char *path, + GlnxChaseFlags flags, + mode_t mode, + GError **error); + G_END_DECLS diff --git a/tests/test-libglnx-chase.c b/tests/test-libglnx-chase.c index b0ce1b414..0a7a6551b 100644 --- a/tests/test-libglnx-chase.c +++ b/tests/test-libglnx-chase.c @@ -50,6 +50,18 @@ path_get_ino (const char *path) return st.st_ino; } +static mode_t +get_mode (int fd) +{ + int r; + struct stat st; + + r = fstatat (fd, "", &st, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); + g_assert_cmpint (r, >=, 0); + + return st.st_mode & 0777; +} + static char * get_abspath (int dfd, const char *path) @@ -586,6 +598,62 @@ test_chase_and_statxat_permissions (void) g_clear_fd (&chase_fd, NULL); } +static void +test_chase_and_mkdir (void) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int dfd = -1; + glnx_autofd int file_fd = -1; + glnx_autofd int chase_fd = -1; + ino_t expected_ino; + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "d1", 0755, + &dfd, + NULL, &error)); + g_assert_no_error (error); + + chase_fd = glnx_chase_and_mkdirat (AT_FDCWD, "d1/d2/d3", + GLNX_CHASE_DEFAULT, + 0755, + &error); + g_assert_cmpint (chase_fd, >=, 0); + g_assert_no_error (error); + + file_fd = openat (AT_FDCWD, "d1/d2/d3", O_PATH | O_CLOEXEC | O_NOFOLLOW); + g_assert_cmpint (file_fd, >=, 0); + expected_ino = get_ino (file_fd); + + g_assert_cmpint (get_ino (chase_fd), ==, expected_ino); + + g_assert_cmpint (get_mode (dfd), ==, 0755); + g_assert_cmpint (get_mode (chase_fd), ==, 0755); + + g_clear_fd (&chase_fd, NULL); + g_clear_fd (&file_fd, NULL); + + chase_fd = glnx_chase_and_mkdirat (AT_FDCWD, "d1/d4/d5", + GLNX_CHASE_DEFAULT, + 0700, + &error); + g_assert_cmpint (chase_fd, >=, 0); + g_assert_no_error (error); + + file_fd = openat (AT_FDCWD, "d1", O_PATH | O_CLOEXEC | O_NOFOLLOW); + g_assert_cmpint (file_fd, >=, 0); + g_assert_cmpint (get_mode (file_fd), ==, 0755); + g_clear_fd (&file_fd, NULL); + + file_fd = openat (AT_FDCWD, "d1/d4", O_PATH | O_CLOEXEC | O_NOFOLLOW); + g_assert_cmpint (file_fd, >=, 0); + g_assert_cmpint (get_mode (file_fd), ==, 0700); + g_clear_fd (&file_fd, NULL); + + file_fd = openat (AT_FDCWD, "d1/d4/d5", O_PATH | O_CLOEXEC | O_NOFOLLOW); + g_assert_cmpint (file_fd, >=, 0); + g_assert_cmpint (get_mode (file_fd), ==, 0700); + g_clear_fd (&file_fd, NULL); +} + int main (int argc, char **argv) { _GLNX_TEST_SCOPED_TEMP_DIR; @@ -602,6 +670,7 @@ int main (int argc, char **argv) g_test_add_func ("/chase-and-statxat-basic", test_chase_and_statxat_basic); g_test_add_func ("/chase-and-statxat-symlink", test_chase_and_statxat_symlink); g_test_add_func ("/chase-and-statxat-permissions", test_chase_and_statxat_permissions); + g_test_add_func ("/chase-and-mkdir", test_chase_and_mkdir); ret = g_test_run();