diff --git a/glnx-chase.c b/glnx-chase.c index 9ad2fe3b3..2251cb71f 100644 --- a/glnx-chase.c +++ b/glnx-chase.c @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -46,6 +47,13 @@ #define GLNX_CHASE_ALL_FLAGS \ (GLNX_CHASE_ALL_DEBUG_FLAGS | GLNX_CHASE_ALL_REGULAR_FLAGS) +typedef int (* GlnxChaseCallback)(int next_fd, + int current_fd, + const char *segment, + int open_tree_flags, + gpointer user_data, + GError **error); + typedef GQueue GlnxStatxQueue; static void @@ -328,10 +336,12 @@ 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 +359,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 +367,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 +478,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 +629,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 +653,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 +699,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 +753,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 @@ -787,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();