From 4f8674709dce57e7f5422a475f61556162f930d8 Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Fri, 12 Jun 2026 13:42:13 +0200 Subject: [PATCH 1/2] 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 2/2] 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();