diff --git a/subprojects/libglnx/Makefile-libglnx.am b/subprojects/libglnx/Makefile-libglnx.am index 5934a2b6..2c6ad447 100644 --- a/subprojects/libglnx/Makefile-libglnx.am +++ b/subprojects/libglnx/Makefile-libglnx.am @@ -37,6 +37,8 @@ libglnx_la_SOURCES = \ $(libglnx_srcpath)/glnx-backport-testutils.c \ $(libglnx_srcpath)/glnx-backports.h \ $(libglnx_srcpath)/glnx-backports.c \ + $(libglnx_srcpath)/glnx-chase.h \ + $(libglnx_srcpath)/glnx-chase.c \ $(libglnx_srcpath)/glnx-local-alloc.h \ $(libglnx_srcpath)/glnx-local-alloc.c \ $(libglnx_srcpath)/glnx-errors.h \ diff --git a/subprojects/libglnx/glnx-backports.c b/subprojects/libglnx/glnx-backports.c index d5b68ad0..69f9577c 100644 --- a/subprojects/libglnx/glnx-backports.c +++ b/subprojects/libglnx/glnx-backports.c @@ -106,6 +106,78 @@ _glnx_strv_equal (const gchar * const *strv1, } #endif +#if !GLIB_CHECK_VERSION(2, 76, 0) +gboolean +_glnx_close (gint fd, + GError **error) +{ + int res; + + /* Important: if @error is NULL, we must not do anything that is + * not async-signal-safe. + */ + res = close (fd); + + if (res == -1) + { + int errsv = errno; + + if (errsv == EINTR) + { + /* Just ignore EINTR for now; a retry loop is the wrong thing to do + * on Linux at least. Anyone who wants to add a conditional check + * for e.g. HP-UX is welcome to do so later... + * + * close_func_with_invalid_fds() in gspawn.c has similar logic. + * + * https://lwn.net/Articles/576478/ + * http://lkml.indiana.edu/hypermail/linux/kernel/0509.1/0877.html + * https://bugzilla.gnome.org/show_bug.cgi?id=682819 + * http://utcc.utoronto.ca/~cks/space/blog/unix/CloseEINTR + * https://sites.google.com/site/michaelsafyan/software-engineering/checkforeintrwheninvokingclosethinkagain + * + * `close$NOCANCEL()` in gstdioprivate.h, on macOS, ensures that the fd is + * closed even if it did return EINTR. + */ + return TRUE; + } + + if (error) + { + g_set_error_literal (error, G_FILE_ERROR, + g_file_error_from_errno (errsv), + g_strerror (errsv)); + } + + if (errsv == EBADF) + { + /* There is a bug. Fail an assertion. Note that this function is supposed to be + * async-signal-safe, but in case an assertion fails, all bets are already off. */ + if (fd >= 0) + { + /* Closing an non-negative, invalid file descriptor is a bug. The bug is + * not necessarily in the caller of _glnx_close(), but somebody else + * might have wrongly closed fd. In any case, there is a serious bug + * somewhere. */ + g_critical ("_glnx_close(fd:%d) failed with EBADF. The tracking of file descriptors got messed up", fd); + } + else + { + /* Closing a negative "file descriptor" is less problematic. It's still a nonsensical action + * from the caller. Assert against that too. */ + g_critical ("_glnx_close(fd:%d) failed with EBADF. This is not a valid file descriptor", fd); + } + } + + errno = errsv; + + return FALSE; + } + + return TRUE; +} +#endif + #if !GLIB_CHECK_VERSION(2, 80, 0) /* This function is called between fork() and exec() and hence must be * async-signal-safe (see signal-safety(7)). */ diff --git a/subprojects/libglnx/glnx-backports.h b/subprojects/libglnx/glnx-backports.h index 09e4609a..9ec11e4c 100644 --- a/subprojects/libglnx/glnx-backports.h +++ b/subprojects/libglnx/glnx-backports.h @@ -29,6 +29,7 @@ #include #include +#include #include G_BEGIN_DECLS @@ -53,6 +54,34 @@ G_BEGIN_DECLS } G_STMT_END #endif +#if !GLIB_CHECK_VERSION(2, 76, 0) +gboolean _glnx_close (gint fd, + GError **error); +#else +#define _glnx_close g_close +#endif + +#if !GLIB_CHECK_VERSION(2, 76, 0) +static inline gboolean +g_clear_fd (int *fd_ptr, + GError **error) +{ + int fd = *fd_ptr; + + *fd_ptr = -1; + + if (fd < 0) + return TRUE; + + /* Suppress "Not available before" warning */ + G_GNUC_BEGIN_IGNORE_DEPRECATIONS + /* This importantly calls _glnx_close to always get async-signal-safe if + * error == NULL */ + return _glnx_close (fd, error); + G_GNUC_END_IGNORE_DEPRECATIONS +} +#endif + #if !GLIB_CHECK_VERSION(2, 40, 0) #define g_info(...) g_log (G_LOG_DOMAIN, G_LOG_LEVEL_INFO, __VA_ARGS__) #endif diff --git a/subprojects/libglnx/glnx-chase.c b/subprojects/libglnx/glnx-chase.c new file mode 100644 index 00000000..9ad2fe3b --- /dev/null +++ b/subprojects/libglnx/glnx-chase.c @@ -0,0 +1,789 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2026 Red Hat, Inc. + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * glnx_chaseat was inspired by systemd's chase + */ + +#include "libglnx-config.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#define AUTOFS_SUPER_MAGIC 0x0187 /* man fstatfs */ + +#define GLNX_CHASE_DEBUG_NO_OPENAT2 (1U << 31) +#define GLNX_CHASE_DEBUG_NO_OPEN_TREE (1U << 30) + +#define GLNX_CHASE_ALL_DEBUG_FLAGS \ + (GLNX_CHASE_DEBUG_NO_OPENAT2 | \ + GLNX_CHASE_DEBUG_NO_OPEN_TREE) + +#define GLNX_CHASE_ALL_REGULAR_FLAGS \ + (GLNX_CHASE_NO_AUTOMOUNT | \ + GLNX_CHASE_NOFOLLOW | \ + GLNX_CHASE_RESOLVE_BENEATH | \ + GLNX_CHASE_RESOLVE_IN_ROOT | \ + GLNX_CHASE_RESOLVE_NO_SYMLINKS | \ + GLNX_CHASE_MUST_BE_REGULAR | \ + GLNX_CHASE_MUST_BE_DIRECTORY | \ + GLNX_CHASE_MUST_BE_SOCKET) + +#define GLNX_CHASE_ALL_FLAGS \ + (GLNX_CHASE_ALL_DEBUG_FLAGS | GLNX_CHASE_ALL_REGULAR_FLAGS) + +typedef GQueue GlnxStatxQueue; + +static void +glnx_statx_queue_push (GlnxStatxQueue *queue, + const struct glnx_statx *st) +{ + struct glnx_statx *copy; + + copy = g_memdup2 (st, sizeof (*st)); + g_queue_push_tail (queue, copy); +} + +static void +glnx_statx_queue_free_element (gpointer element, + G_GNUC_UNUSED gpointer userdata) +{ + g_free (element); +} + +static void +glnx_statx_queue_free (GlnxStatxQueue *squeue) +{ + GQueue *queue = (GQueue *) squeue; + + /* Same as g_queue_clear_full (queue, g_free), but works for <2.60 */ + g_queue_foreach (queue, glnx_statx_queue_free_element, NULL); + g_queue_clear (queue); +} + +G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(GlnxStatxQueue, glnx_statx_queue_free) + +static gboolean +glnx_statx_inode_same (const struct glnx_statx *a, + const struct glnx_statx *b) +{ + g_assert ((a->stx_mask & (GLNX_STATX_TYPE | GLNX_STATX_INO)) == + (GLNX_STATX_TYPE | GLNX_STATX_INO)); + g_assert ((b->stx_mask & (GLNX_STATX_TYPE | GLNX_STATX_INO)) == + (GLNX_STATX_TYPE | GLNX_STATX_INO)); + + return ((a->stx_mode ^ b->stx_mode) & S_IFMT) == 0 && + a->stx_dev_major == b->stx_dev_major && + a->stx_dev_minor == b->stx_dev_minor && + a->stx_ino == b->stx_ino; +} + +static gboolean +glnx_statx_mount_same (const struct glnx_statx *a, + const struct glnx_statx *b) +{ + g_assert ((a->stx_mask & (GLNX_STATX_MNT_ID | GLNX_STATX_MNT_ID_UNIQUE)) != 0); + g_assert ((b->stx_mask & (GLNX_STATX_MNT_ID | GLNX_STATX_MNT_ID_UNIQUE)) != 0); + + return a->stx_mnt_id == b->stx_mnt_id; +} + +static gboolean +glnx_chase_statx (int dfd, + int additional_flags, + struct glnx_statx *buf, + GError **error) +{ + if (!glnx_statx (dfd, "", + AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW | additional_flags, + GLNX_STATX_TYPE | GLNX_STATX_INO | + GLNX_STATX_MNT_ID | GLNX_STATX_MNT_ID_UNIQUE, + buf, + error)) + return FALSE; + + if ((buf->stx_mask & (GLNX_STATX_TYPE | GLNX_STATX_INO)) != + (GLNX_STATX_TYPE | GLNX_STATX_INO) || + (buf->stx_mask & (GLNX_STATX_MNT_ID | GLNX_STATX_MNT_ID_UNIQUE)) == 0) + { + errno = ENODATA; + return glnx_throw_errno_prefix (error, + "statx didn't return all required fields"); + } + + return TRUE; +} + +/* TODO: procfs magiclinks handling */ + +/* open_tree subset which transparently falls back to openat. + * + * Returned fd is always OPATH and CLOEXEC. + * + * With NO_AUTOMOUNT this function never triggers automounts. Otherwise, it only + * guarantees to trigger an automount which is on last segment of the path! + * + * flags can be a combinations of: + * - GLNX_CHASE_NO_AUTOMOUNT + * - GLNX_CHASE_NOFOLLOW + */ +static int +chase_open_tree (int dirfd, + const char *path, + GlnxChaseFlags flags, + GError **error) +{ + glnx_autofd int fd = -1; + static gboolean can_open_tree = TRUE; + unsigned int openat_flags = 0; + + g_assert ((flags & ~(GLNX_CHASE_NO_AUTOMOUNT | + GLNX_CHASE_NOFOLLOW | + GLNX_CHASE_ALL_DEBUG_FLAGS)) == 0); + + /* First we try to actually use open_tree, and then fall back to the impl + * using openat. + * Technically racy (static, not synced), but both paths work fine so it + * doesn't matter. */ + if (can_open_tree && (flags & GLNX_CHASE_DEBUG_NO_OPEN_TREE) == 0) + { + unsigned int open_tree_flags = 0; + + open_tree_flags = OPEN_TREE_CLOEXEC; + if ((flags & GLNX_CHASE_NOFOLLOW) != 0) + open_tree_flags |= AT_SYMLINK_NOFOLLOW; + if ((flags & GLNX_CHASE_NO_AUTOMOUNT) != 0) + open_tree_flags |= AT_NO_AUTOMOUNT; + + fd = open_tree (dirfd, path, open_tree_flags); + + /* If open_tree is not supported, or blocked (EPERM), we fall back to + * openat */ + if (fd < 0 && G_IN_SET (errno, + EOPNOTSUPP, + ENOTTY, + ENOSYS, + EAFNOSUPPORT, + EPFNOSUPPORT, + EPROTONOSUPPORT, + ESOCKTNOSUPPORT, + ENOPROTOOPT, + EPERM)) + can_open_tree = FALSE; + else if (fd < 0) + return glnx_fd_throw_errno_prefix (error, "open_tree"); + else + return g_steal_fd (&fd); + } + + openat_flags = O_CLOEXEC | O_PATH; + if ((flags & GLNX_CHASE_NOFOLLOW) != 0) + openat_flags |= O_NOFOLLOW; + + fd = openat (dirfd, path, openat_flags); + if (fd < 0) + return glnx_fd_throw_errno_prefix (error, "openat in open_tree fallback"); + + /* openat does not trigger automounts, so we have to manually do so + * unless NO_AUTOMOUNT was specified */ + if ((flags & GLNX_CHASE_NO_AUTOMOUNT) == 0) + { + struct statfs stfs; + + if (fstatfs (fd, &stfs) < 0) + return glnx_fd_throw_errno_prefix (error, "fstatfs in open_tree fallback"); + + /* fstatfs(2) can then be used to determine if it is, in fact, an + * untriggered automount point (.f_type == AUTOFS_SUPER_MAGIC). */ + if (stfs.f_type == AUTOFS_SUPER_MAGIC) + { + glnx_autofd int new_fd = -1; + + new_fd = openat (fd, ".", openat_flags | O_DIRECTORY); + /* For some reason, openat with O_PATH | O_DIRECTORY does trigger + * automounts, without us having to actually open the file, so let's + * use this here. It only works for directories though. */ + if (new_fd >= 0) + return g_steal_fd (&new_fd); + + if (errno != ENOTDIR) + return glnx_fd_throw_errno_prefix (error, "openat(O_DIRECTORY) in autofs mount open_tree fallback"); + + /* The automount is a directory, so let's try to open the file, + * which can fail because we are missing permissions, but that's + * okay, we only need to trigger automount. */ + new_fd = openat (fd, ".", (openat_flags & ~O_PATH) | + O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY); + glnx_close_fd (&new_fd); + + /* And try again with O_PATH */ + new_fd = openat (dirfd, path, openat_flags); + if (new_fd < 0) + return glnx_fd_throw_errno_prefix (error, "reopening in autofs mount open_tree fallback"); + + if (fstatfs (new_fd, &stfs) < 0) + return glnx_fd_throw_errno_prefix (error, "fstatfs in autofs mount open_tree fallback"); + + /* bail if we didn't manage to trigger the automount */ + if (stfs.f_type == AUTOFS_SUPER_MAGIC) + { + errno = EOPNOTSUPP; + return glnx_fd_throw_errno_prefix (error, "unable to trigger automount"); + } + + return g_steal_fd (&new_fd); + } + } + + return g_steal_fd (&fd); +} + +static int +open_cwd (GlnxChaseFlags flags, + GError **error) +{ + GLNX_AUTO_PREFIX_ERROR ("cannot open working directory", error); + + /* NO_AUTOMOUNT should be fine here because automount must have been + * triggered already for the CWD */ + return chase_open_tree (AT_FDCWD, ".", + (flags & GLNX_CHASE_ALL_DEBUG_FLAGS) | + GLNX_CHASE_NO_AUTOMOUNT | + GLNX_CHASE_NOFOLLOW, + error); +} + +static int +open_root (GlnxChaseFlags flags, + GError **error) +{ + GLNX_AUTO_PREFIX_ERROR ("cannot open root directory", error); + + /* NO_AUTOMOUNT should be fine here because automount must have been + * triggered already for the root */ + return chase_open_tree (AT_FDCWD, "/", + (flags & GLNX_CHASE_ALL_DEBUG_FLAGS) | + GLNX_CHASE_NO_AUTOMOUNT | + GLNX_CHASE_NOFOLLOW, + error); +} + +/* This returns the next segment of a path and tells us if it is the last + * segment. + * + * Importantly, a segment is anything after a "/", even if it is empty or ".". + * + * For example: + * "" -> "" + * "/" -> "" + * "////" -> "" + * "foo/bar" -> "foo", "bar" + * "foo//bar" -> "foo", "bar" + * "///foo//bar" -> "foo", "bar" + * "///foo//bar/" -> "foo", "bar", "" + * "///foo//bar/." -> "foo", "bar", "." + */ +static char * +extract_next_segment (const char **remaining, + gboolean *is_last) +{ + const char *r = *remaining; + const char *s; + size_t len = 0; + + while (r[0] != '\0' && G_IS_DIR_SEPARATOR (r[0])) + r++; + + s = r; + + while (r[0] != '\0' && !G_IS_DIR_SEPARATOR (r[0])) + { + r++; + len++; + } + + *is_last = (r[0] == '\0'); + *remaining = r; + return g_strndup (s, len); +} + +/* This iterates over the segments of path and opens the corresponding + * directories or files. This gives us the opportunity to implement openat2 + * like RESOLVE_ semantics, without actually needing openat2. + * It also allows us to implement features which openat2 does not have because + * we're in full control over the resolving. + */ +static int +chase_manual (int dirfd, + const char *path, + GlnxChaseFlags flags, + GError **error) +{ + gboolean is_absolute; + g_autofree char *buffer = NULL; + const char *remaining; + glnx_autofd int owned_root_fd = -1; + int root_fd; + glnx_autofd int owned_fd = -1; + int fd; + int remaining_follows = GLNX_CHASE_MAX; + struct glnx_statx st; + g_auto(GlnxStatxQueue) path_st = G_QUEUE_INIT; + int no_automount; + + /* Take a shortcut if + * - none of the resolve flags are set (they would require work here) + * - NO_AUTOMOUNT is set (chase_open_tree only triggers the automount for + * last component in some cases) + * + * TODO: if we have a guarantee that the open_tree syscall works, we can + * shortcut even without GLNX_CHASE_NO_AUTOMOUNT + */ + if ((flags & (GLNX_CHASE_NO_AUTOMOUNT | + GLNX_CHASE_RESOLVE_BENEATH | + GLNX_CHASE_RESOLVE_IN_ROOT | + GLNX_CHASE_RESOLVE_NO_SYMLINKS)) == GLNX_CHASE_NO_AUTOMOUNT) + { + GlnxChaseFlags open_tree_flags = + (flags & (GLNX_CHASE_NOFOLLOW | GLNX_CHASE_ALL_DEBUG_FLAGS)); + + return chase_open_tree (dirfd, path, open_tree_flags, error); + } + + no_automount = (flags & GLNX_CHASE_NO_AUTOMOUNT) != 0 ? AT_NO_AUTOMOUNT : 0; + + is_absolute = g_path_is_absolute (path); + + if (is_absolute && (flags & GLNX_CHASE_RESOLVE_BENEATH) != 0) + { + /* Absolute paths always get rejected with RESOLVE_BENEATH with errno + * EXDEV */ + + errno = EXDEV; + return glnx_fd_throw_errno_prefix (error, "absolute path not allowed for RESOLVE_BENEATH"); + } + else if (!is_absolute || + (is_absolute && (flags & GLNX_CHASE_RESOLVE_IN_ROOT) != 0)) + { + /* The absolute path is relative to dirfd with GLNX_CHASE_RESOLVE_IN_ROOT, + * and a relative path is always relative. */ + + /* In both cases we use dirfd as our chase root */ + if (dirfd == AT_FDCWD) + { + owned_root_fd = root_fd = open_cwd (flags, error); + if (root_fd < 0) + return -1; + } + else + { + root_fd = dirfd; + } + } + else + { + /* For absolute paths, we ignore dirfd, we use the actual root / for our + * chase root */ + g_assert (is_absolute); + + owned_root_fd = root_fd = open_root (flags, error); + if (root_fd < 0) + return -1; + } + + /* At this point, we always have (a relative) path, relative to root_fd */ + is_absolute = FALSE; + g_assert (root_fd >= 0); + + /* Add root to path_st, so we can verify if we get back to it */ + if (!glnx_chase_statx (root_fd, no_automount, &st, error)) + return -1; + + glnx_statx_queue_push (&path_st, &st); + + /* Let's start walking the path! */ + buffer = g_strdup (path); + remaining = buffer; + fd = root_fd; + + for (;;) + { + g_autofree char *segment = NULL; + gboolean is_last; + glnx_autofd int next_fd = -1; + + segment = extract_next_segment (&remaining, &is_last); + + /* If we encounter an empty segment ("", "."), we stay where we are and + * ignore the segment, or just exit if it is the last segment. */ + if (g_strcmp0 (segment, "") == 0 || g_strcmp0 (segment, ".") == 0) + { + if (is_last) + break; + continue; + } + + /* Special handling for going down the tree with RESOLVE_ flags */ + if (g_strcmp0 (segment, "..") == 0) + { + /* path_st contains the stat of the root if we're at root, so the + * length is 1 in that case, and going lower than the root is not + * allowed here! */ + + if (path_st.length <= 1 && (flags & GLNX_CHASE_RESOLVE_BENEATH) != 0) + { + /* With RESOLVE_BENEATH, error out if we would end up above the + * root fd */ + errno = EXDEV; + return glnx_fd_throw_errno_prefix (error, "attempted to traverse above root path via \"..\""); + } + else if (path_st.length <= 1 && (flags & GLNX_CHASE_RESOLVE_IN_ROOT) != 0) + { + /* With RESOLVE_IN_ROOT, we pretend that we hit the real root, + * and stay there, just like the kernel does. */ + continue; + } + } + + { + /* Open the next segment. We always use GLNX_CHASE_NOFOLLOW here to be + * able to ensure the RESOLVE flags, and automount behavior. */ + + GlnxChaseFlags open_tree_flags = + GLNX_CHASE_NOFOLLOW | + (flags & (GLNX_CHASE_NO_AUTOMOUNT | GLNX_CHASE_ALL_DEBUG_FLAGS)); + + next_fd = chase_open_tree (fd, segment, open_tree_flags, error); + if (next_fd < 0) + return -1; + } + + if (!glnx_chase_statx (next_fd, no_automount, &st, error)) + return -1; + + /* We resolve links if: they are not in the last component, or if they + * are the last component and NOFOLLOW is not set. */ + if (S_ISLNK (st.stx_mode) && + (!is_last || (flags & GLNX_CHASE_NOFOLLOW) == 0)) + { + g_autofree char *link = NULL; + g_autofree char *new_buffer = NULL; + + /* ...however, we do not resolve symlinks with NO_SYMLINKS, and use + * remaining_follows to ensure we don't loop forever. */ + if ((flags & GLNX_CHASE_RESOLVE_NO_SYMLINKS) != 0 || + --remaining_follows <= 0) + { + errno = ELOOP; + return glnx_fd_throw_errno_prefix (error, "followed too many symlinks"); + } + + /* AT_EMPTY_PATH is implied for readlinkat */ + link = glnx_readlinkat_malloc (next_fd, "", NULL, error); + if (!link) + return -1; + + if (g_path_is_absolute (link) && + (flags & GLNX_CHASE_RESOLVE_BENEATH) != 0) + { + errno = EXDEV; + return glnx_fd_throw_errno_prefix (error, "absolute symlink not allowed for RESOLVE_BENEATH"); + } + + /* The link can be absolute, and we handle that below, by changing the + * dirfd. The path *remains* and absolute path internally, but that is + * okay because we always interpret any path (even absolute ones) as + * being relative to the dirfd */ + new_buffer = g_strdup_printf ("%s/%s", link, remaining); + g_clear_pointer (&buffer, g_free); + buffer = g_steal_pointer (&new_buffer); + remaining = buffer; + + if (g_path_is_absolute (link)) + { + if ((flags & GLNX_CHASE_RESOLVE_IN_ROOT) != 0) + { + /* If the path was absolute, and RESOLVE_IN_ROOT is set, we + * will resolve the remaining path relative to root_fd */ + + g_clear_fd (&owned_fd, NULL); + fd = root_fd; + } + else + { + /* If the path was absolute, we will resolve the remaining + * path relative to the real root */ + + g_clear_fd (&owned_fd, NULL); + fd = owned_fd = open_root (flags, error); + if (fd < 0) + return -1; + } + + /* path_st must only contain the new root at this point */ + if (!glnx_chase_statx (fd, no_automount, &st, error)) + return -1; + + glnx_statx_queue_free (&path_st); + g_queue_init (&path_st); + glnx_statx_queue_push (&path_st, &st); + } + + continue; + } + + /* Either adds an element to path_st or removes one if we got down the + * tree. This also checks that going down the tree ends up at the inode + * we saw before (if we saw it before). */ + if (g_strcmp0 (segment, "..") == 0) + { + g_autofree struct glnx_statx *old_tail = NULL; + struct glnx_statx *lower_st; + + old_tail = g_queue_pop_tail (&path_st); + + lower_st = g_queue_peek_tail (&path_st); + if (lower_st && + (!glnx_statx_mount_same (&st, lower_st) || + !glnx_statx_inode_same (&st, lower_st))) + { + errno = EXDEV; + return glnx_fd_throw_errno_prefix (error, "a parent directory changed while traversing"); + } + } + else + { + glnx_statx_queue_push (&path_st, &st); + } + + /* There is still another path component, but the next fd is not a + * a directory. We need the fd to be a directory though, to open the next + * segment from. So bail with the appropriate error. */ + if (!is_last && !S_ISDIR (st.stx_mode)) + { + errno = ENOTDIR; + return glnx_fd_throw_errno_prefix (error, "a non-final path segment is not a directory"); + } + + g_clear_fd (&owned_fd, NULL); + fd = owned_fd = g_steal_fd (&next_fd); + + if (is_last) + break; + } + + /* We need an owned fd to return. Only having fd and not owned_fd can happen + * if we never finished a single iteration, or if an absolute path with + * RESOLVE_IN_ROOT makes us point at root_fd. + * We just re-open fd to always get an owned fd. + * Note that this only works because in all cases where owned_fd does not + * exists, fd is a directory. */ + if (owned_fd < 0) + { + owned_fd = openat (fd, ".", O_PATH | O_CLOEXEC | O_NOFOLLOW); + if (owned_fd < 0) + return glnx_fd_throw_errno_prefix (error, "reopening failed"); + } + + return g_steal_fd (&owned_fd); +} + +/** + * glnx_chaseat: + * @dirfd: a directory file descriptor + * @path: a path + * @flags: combination of GlnxChaseFlags flags + * @error: a #GError + * + * Behaves similar to openat, but with a number of differences: + * + * - All file descriptors which get returned are O_PATH and O_CLOEXEC. If you + * want to actually open the file for reading or writing, use glnx_fd_reopen, + * openat, or other at-style functions. + * - By default, automounts get triggered and the O_PATH fd will point to inodes + * in the newly mounted filesystem if an automount is encountered. This can be + * turned off with GLNX_CHASE_NO_AUTOMOUNT. + * - The GLNX_CHASE_RESOLVE_ flags can be used to safely deal with symlinks. + * + * Returns: the chased file, or -1 with @error set on error + */ +int +glnx_chaseat (int dirfd, + const char *path, + GlnxChaseFlags flags, + GError **error) +{ + static gboolean can_openat2 = TRUE; + glnx_autofd int fd = -1; + + g_return_val_if_fail (dirfd >= 0 || dirfd == AT_FDCWD, -1); + g_return_val_if_fail (path != NULL, -1); + g_return_val_if_fail ((flags & ~(GLNX_CHASE_ALL_FLAGS)) == 0, -1); + g_return_val_if_fail (error == NULL || *error == NULL, -1); + + { + int must_flags = flags & (GLNX_CHASE_MUST_BE_REGULAR | + GLNX_CHASE_MUST_BE_DIRECTORY | + GLNX_CHASE_MUST_BE_SOCKET); + /* check that no more than one bit is set (= power of two) */ + g_return_val_if_fail ((must_flags & (must_flags - 1)) == 0, -1); + } + + /* TODO: Add a callback which is called for every resolved path segment, to + * allow users to verify and expand the functionality safely. */ + + /* We need the manual impl for NO_AUTOMOUNT, and we can skip this, if we don't + * have openat2 at all. + * Technically racy (static, not synced), but both paths work fine so it + * doesn't matter. */ + if (can_openat2 && (flags & GLNX_CHASE_NO_AUTOMOUNT) == 0 && + (flags & GLNX_CHASE_DEBUG_NO_OPENAT2) == 0) + { + uint64_t openat2_flags = 0; + uint64_t openat2_resolve = 0; + struct open_how how; + + openat2_flags = O_PATH | O_CLOEXEC; + if ((flags & GLNX_CHASE_NOFOLLOW) != 0) + openat2_flags |= O_NOFOLLOW; + + openat2_resolve |= RESOLVE_NO_MAGICLINKS; + if ((flags & GLNX_CHASE_RESOLVE_BENEATH) != 0) + openat2_resolve |= RESOLVE_BENEATH; + if ((flags & GLNX_CHASE_RESOLVE_IN_ROOT) != 0) + openat2_resolve |= RESOLVE_IN_ROOT; + if ((flags & GLNX_CHASE_RESOLVE_NO_SYMLINKS) != 0) + openat2_resolve |= RESOLVE_NO_SYMLINKS; + + how = (struct open_how) { + .flags = openat2_flags, + .mode = 0, + .resolve = openat2_resolve, + }; + + fd = openat2 (dirfd, path, &how, sizeof (how)); + if (fd < 0) + { + /* If the syscall is not implemented (ENOSYS) or blocked by + * seccomp (EPERM), we need to fall back to the manual path chasing + * via open_tree. */ + if (G_IN_SET (errno, ENOSYS, EPERM)) + can_openat2 = FALSE; + else + return glnx_fd_throw_errno (error); + } + } + + if (fd < 0) + { + fd = chase_manual (dirfd, path, flags, error); + if (fd < 0) + return -1; + } + + if ((flags & (GLNX_CHASE_MUST_BE_REGULAR | + GLNX_CHASE_MUST_BE_DIRECTORY | + GLNX_CHASE_MUST_BE_SOCKET)) != 0) + { + struct glnx_statx st; + + if (!glnx_statx (fd, "", + AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW | + ((flags & GLNX_CHASE_NO_AUTOMOUNT) ? AT_NO_AUTOMOUNT : 0), + GLNX_STATX_TYPE, + &st, + error)) + return -1; + + if ((st.stx_mask & GLNX_STATX_TYPE) == 0) + { + errno = ENODATA; + return glnx_fd_throw_errno_prefix (error, "unable to get file type"); + } + + if ((flags & GLNX_CHASE_MUST_BE_REGULAR) != 0 && + !S_ISREG (st.stx_mode)) + { + if (S_ISDIR (st.stx_mode)) + errno = EISDIR; + else + errno = EBADFD; + + return glnx_fd_throw_errno_prefix (error, "not a regular file"); + } + + if ((flags & GLNX_CHASE_MUST_BE_DIRECTORY) != 0 && + !S_ISDIR (st.stx_mode)) + { + errno = ENOTDIR; + return glnx_fd_throw_errno_prefix (error, "not a directory"); + } + + if ((flags & GLNX_CHASE_MUST_BE_SOCKET) != 0 && + !S_ISSOCK (st.stx_mode)) + { + errno = ENOTSOCK; + return glnx_fd_throw_errno_prefix (error, "not a socket"); + } + } + + return g_steal_fd (&fd); +} + +/** + * glnx_chase_and_statxat: + * @dirfd: a directory file descriptor + * @path: a path + * @flags: combination of GlnxChaseFlags flags + * @mask: combination of GLNX_STATX_ flags + * @statbuf: a pointer to a struct glnx_statx which will be filled out + * @error: a #GError + * + * Stats the file at @path relative to @dirfd and fills out @statbuf with the + * result according to the interest mask @mask. + * + * See glnx_chaseat for the meaning of @dirfd, @path, and @flags. + * + * Returns: the chased file, or -1 with @error set on error + */ +int +glnx_chase_and_statxat (int dirfd, + const char *path, + GlnxChaseFlags flags, + unsigned int mask, + struct glnx_statx *statbuf, + GError **error) +{ + glnx_autofd int fd = -1; + + /* other args are checked by glnx_chaseat */ + g_return_val_if_fail (statbuf != NULL, FALSE); + + fd = glnx_chaseat (dirfd, path, flags, error); + if (fd < 0) + return -1; + + if (!glnx_statx (fd, "", + AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW | + ((flags & GLNX_CHASE_NO_AUTOMOUNT) ? AT_NO_AUTOMOUNT : 0), + mask, + statbuf, + error)) + return -1; + + return g_steal_fd (&fd); +} diff --git a/subprojects/libglnx/glnx-chase.h b/subprojects/libglnx/glnx-chase.h new file mode 100644 index 00000000..05dac12d --- /dev/null +++ b/subprojects/libglnx/glnx-chase.h @@ -0,0 +1,51 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2026 Red Hat, Inc. + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include + +typedef enum _GlnxChaseFlags { + /* Default */ + GLNX_CHASE_DEFAULT = 0, + /* Disable triggering of automounts */ + GLNX_CHASE_NO_AUTOMOUNT = 1 << 1, + /* Do not follow the path's right-most component. When the path's right-most + * component refers to symlink, return O_PATH fd of the symlink. */ + GLNX_CHASE_NOFOLLOW = 1 << 2, + /* Do not permit the path resolution to succeed if any component of the + * resolution is not a descendant of the directory indicated by dirfd. */ + GLNX_CHASE_RESOLVE_BENEATH = 1 << 3, + /* Symlinks are resolved relative to the given dirfd instead of root. */ + GLNX_CHASE_RESOLVE_IN_ROOT = 1 << 4, + /* Fail if any symlink is encountered. */ + GLNX_CHASE_RESOLVE_NO_SYMLINKS = 1 << 5, + /* Fail if the path's right-most component is not a regular file */ + GLNX_CHASE_MUST_BE_REGULAR = 1 << 6, + /* Fail if the path's right-most component is not a directory */ + GLNX_CHASE_MUST_BE_DIRECTORY = 1 << 7, + /* Fail if the path's right-most component is not a socket */ + GLNX_CHASE_MUST_BE_SOCKET = 1 << 8, +} GlnxChaseFlags; + +/* How many iterations to execute before returning ELOOP */ +#define GLNX_CHASE_MAX 32 + +G_BEGIN_DECLS + +int glnx_chaseat (int dirfd, + const char *path, + GlnxChaseFlags flags, + GError **error); + +int glnx_chase_and_statxat (int dirfd, + const char *path, + GlnxChaseFlags flags, + unsigned int mask, + struct glnx_statx *statbuf, + GError **error); + +G_END_DECLS diff --git a/subprojects/libglnx/glnx-errors.h b/subprojects/libglnx/glnx-errors.h index e1dc3a56..5434a6f7 100644 --- a/subprojects/libglnx/glnx-errors.h +++ b/subprojects/libglnx/glnx-errors.h @@ -32,6 +32,10 @@ gboolean glnx_throw (GError **error, const char *fmt, ...) G_GNUC_PRINTF (2,3); #define glnx_null_throw(error, args...) \ ({glnx_throw (error, args); NULL;}) +/* Like glnx_throw(), but yields -1 (invalid fd). */ +#define glnx_fd_throw(error, args...) \ + ({glnx_throw (error, args); -1;}) + /* Implementation detail of glnx_throw_prefix() */ void glnx_real_set_prefix_error_va (GError *error, const char *format, @@ -97,7 +101,7 @@ glnx_throw_errno (GError **error) g_strerror (errsv)); /* We also restore the value of errno, since that's * what was done in a long-ago libgsystem commit - * https://git.gnome.org/browse/libgsystem/commit/?id=ed106741f7a0596dc8b960b31fdae671d31d666d + * https://gitlab.gnome.org/Archive/libgsystem/-/commit/ed106741f7a0596dc8b960b31fdae671d31d666d * but I certainly can't remember now why I did that. */ errno = errsv; @@ -108,6 +112,10 @@ glnx_throw_errno (GError **error) #define glnx_null_throw_errno(error) \ ({glnx_throw_errno (error); NULL;}) +/* Like glnx_throw_errno(), but yields -1 (invalid fd). */ +#define glnx_fd_throw_errno(error) \ + ({glnx_throw_errno (error); -1;}) + /* Implementation detail of glnx_throw_errno_prefix() */ void glnx_real_set_prefix_error_from_errno_va (GError **error, gint errsv, @@ -120,6 +128,10 @@ gboolean glnx_throw_errno_prefix (GError **error, const char *fmt, ...) G_GNUC_P #define glnx_null_throw_errno_prefix(error, args...) \ ({glnx_throw_errno_prefix (error, args); NULL;}) +/* Like glnx_throw_errno_prefix(), but yields -1 (invalid fd). */ +#define glnx_fd_throw_errno_prefix(error, args...) \ + ({glnx_throw_errno_prefix (error, args); -1;}) + /* BEGIN LEGACY APIS */ #define glnx_set_error_from_errno(error) \ diff --git a/subprojects/libglnx/glnx-fdio.c b/subprojects/libglnx/glnx-fdio.c index 98732051..59261668 100644 --- a/subprojects/libglnx/glnx-fdio.c +++ b/subprojects/libglnx/glnx-fdio.c @@ -321,6 +321,8 @@ glnx_open_anonymous_tmpfile (int flags, error); } +static const char proc_self_fd_slash[] = "/proc/self/fd/"; + /* Use this after calling glnx_open_tmpfile_linkable_at() to give * the file its final name (link into place). */ @@ -367,8 +369,8 @@ glnx_link_tmpfile_at (GLnxTmpfile *tmpf, else { /* This case we have O_TMPFILE, so our reference to it is via /proc/self/fd */ - char proc_fd_path[strlen("/proc/self/fd/") + DECIMAL_STR_MAX(tmpf->fd) + 1]; - snprintf (proc_fd_path, sizeof (proc_fd_path), "/proc/self/fd/%i", tmpf->fd); + char proc_fd_path[sizeof (proc_self_fd_slash) + DECIMAL_STR_MAX(tmpf->fd)]; + snprintf (proc_fd_path, sizeof (proc_fd_path), "%s%i", proc_self_fd_slash, tmpf->fd); if (replace) { @@ -455,8 +457,8 @@ glnx_tmpfile_reopen_rdonly (GLnxTmpfile *tmpf, else { /* This case we have O_TMPFILE, so our reference to it is via /proc/self/fd */ - char proc_fd_path[strlen("/proc/self/fd/") + DECIMAL_STR_MAX(tmpf->fd) + 1]; - snprintf (proc_fd_path, sizeof (proc_fd_path), "/proc/self/fd/%i", tmpf->fd); + char proc_fd_path[sizeof (proc_self_fd_slash) + DECIMAL_STR_MAX(tmpf->fd)]; + snprintf (proc_fd_path, sizeof (proc_fd_path), "%s%i", proc_self_fd_slash, tmpf->fd); if (!glnx_openat_rdonly (AT_FDCWD, proc_fd_path, TRUE, &rdonly_fd, error)) return FALSE; @@ -1205,3 +1207,75 @@ glnx_file_replace_contents_with_perms_at (int dfd, return TRUE; } + +/** + * glnx_fd_reopen: + * @fd: a file descriptor + * @flags: combination of openat flags + * @error: a #GError + * + * Reopens the specified fd with new flags. This is useful for converting an + * O_PATH fd into a regular one, or to turn O_RDWR fds into O_RDONLY fds. + * + * This implicitly sets `O_CLOEXEC | O_NOCTTY` in @flags. + * + * `O_CREAT` isn't allowed in @flags. + * + * This doesn't work on sockets (since they cannot be open()ed, ever). + * + * This implicitly resets the file read index to 0. + * + * If AT_FDCWD is specified as file descriptor, the function returns an fd to + * the current working directory. + * + * If the specified file descriptor refers to a symlink via O_PATH, then this + * function cannot be used to follow that symlink. Because we cannot have + * non-O_PATH fds to symlinks reopening it without O_PATH will always result in + * ELOOP. Or in other words: if you have an O_PATH fd to a symlink you can + * reopen it only if you pass O_PATH again. + */ +int +glnx_fd_reopen (int fd, + int flags, + GError **error) +{ + glnx_autofd int new_fd = -1; + + g_return_val_if_fail (fd >= 0 || fd == AT_FDCWD, -1); + g_return_val_if_fail ((flags & O_CREAT) == 0, -1); + + /* */ + flags |= O_CLOEXEC | O_NOCTTY; + + /* O_NOFOLLOW is not allowed in fd_reopen(), because after all this is + * primarily implemented via a symlink-based interface in /proc/self/fd. Let's + * refuse this here early. Note that the kernel would generate ELOOP here too, + * hence this manual check is mostly redundant – the only reason we add it + * here is so that the O_DIRECTORY special case (see below) behaves the same + * way as the non-O_DIRECTORY case. */ + if ((flags & O_NOFOLLOW) != 0) + { + errno = ELOOP; + return glnx_fd_throw_errno (error); + } + + if ((flags & O_DIRECTORY) != 0 || fd == AT_FDCWD) + { + /* If we shall reopen the fd as directory we can just go via "." and thus + * bypass the whole magic /proc/ directory, and make ourselves independent + * of that being mounted. */ + new_fd = openat (fd, ".", flags | O_DIRECTORY); + } + else + { + g_autofree char *proc_fd_path = NULL; + + proc_fd_path = g_strdup_printf ("/proc/self/fd/%d", fd); + new_fd = open (proc_fd_path, flags); + } + + if (new_fd < 0) + return glnx_fd_throw_errno (error); + + return g_steal_fd (&new_fd); +} diff --git a/subprojects/libglnx/glnx-fdio.h b/subprojects/libglnx/glnx-fdio.h index af534795..acd546df 100644 --- a/subprojects/libglnx/glnx-fdio.h +++ b/subprojects/libglnx/glnx-fdio.h @@ -22,6 +22,7 @@ #pragma once #include +#include #include #include #include @@ -313,6 +314,37 @@ glnx_fstatat (int dfd, return TRUE; } +/** + * glnx_statx: + * @dfd: Directory FD to stat beneath + * @path: Path to stat beneath @dfd + * @flags: Flags to pass to statx() + * @mask: Mask to pass to statx() + * @buf: (out caller-allocates): Return location for statx details + * @error: Return location for a #GError, or %NULL + * + * Wrapper around statx() which adds #GError support and ensures that it + * retries on %EINTR. + * + * The mask to pass must be a combination of GLNX_STATX_* flags which are + * defined by glnx, which map up with the struct glnx_statx. + * + * Returns: %TRUE on success, or %FALSE setting both @error and `errno` + * Since: UNRELEASED + */ +static inline gboolean +glnx_statx (int dfd, + const char *path, + unsigned flags, + unsigned int mask, + struct glnx_statx *buf, + GError **error) +{ + if (TEMP_FAILURE_RETRY (glnx_statx_syscall (dfd, path, flags, mask, buf)) != 0) + return glnx_throw_errno_prefix (error, "statx(%s)", path); + return TRUE; +} + /** * glnx_fstatat_allow_noent: * @dfd: Directory FD to stat beneath @@ -383,4 +415,8 @@ glnx_unlinkat (int dfd, return TRUE; } +int glnx_fd_reopen (int fd, + int flags, + GError **error); + G_END_DECLS diff --git a/subprojects/libglnx/glnx-local-alloc.h b/subprojects/libglnx/glnx-local-alloc.h index 65ae747f..a6e0e9b4 100644 --- a/subprojects/libglnx/glnx-local-alloc.h +++ b/subprojects/libglnx/glnx-local-alloc.h @@ -43,7 +43,6 @@ glnx_local_obj_unref (void *v) if (o) g_object_unref (o); } -#define glnx_unref_object __attribute__ ((cleanup(glnx_local_obj_unref))) /* Backwards-compat with older libglnx */ #define glnx_steal_fd g_steal_fd diff --git a/subprojects/libglnx/glnx-lockfile.c b/subprojects/libglnx/glnx-lockfile.c index fcda84cf..30e638c1 100644 --- a/subprojects/libglnx/glnx-lockfile.c +++ b/subprojects/libglnx/glnx-lockfile.c @@ -66,6 +66,8 @@ glnx_make_lock_file(int dfd, const char *p, int operation, GLnxLockFile *out_loc g_autofree char *t = NULL; int r; + g_return_val_if_fail (p != NULL, FALSE); + /* * We use UNPOSIX locks if they are available. They have nice * semantics, and are mostly compatible with NFS. However, diff --git a/subprojects/libglnx/glnx-missing-syscall.h b/subprojects/libglnx/glnx-missing-syscall.h index c32a3f73..f3bdb3ba 100644 --- a/subprojects/libglnx/glnx-missing-syscall.h +++ b/subprojects/libglnx/glnx-missing-syscall.h @@ -33,6 +33,7 @@ #include "libglnx-config.h" #include +#include #if !HAVE_DECL_RENAMEAT2 # ifndef __NR_renameat2 @@ -236,3 +237,355 @@ inline_close_range (unsigned int low, #define close_range(low, high, flags) inline_close_range(low, high, flags) #define HAVE_CLOSE_RANGE #endif + +#ifndef __IGNORE_statx +# if defined(__aarch64__) +# define systemd_NR_statx 291 +# elif defined(__alpha__) +# define systemd_NR_statx 522 +# elif defined(__arc__) || defined(__tilegx__) +# define systemd_NR_statx 291 +# elif defined(__arm__) +# define systemd_NR_statx 397 +# elif defined(__i386__) +# define systemd_NR_statx 383 +# elif defined(__ia64__) +# define systemd_NR_statx 1350 +# elif defined(__loongarch_lp64) +# define systemd_NR_statx 291 +# elif defined(__m68k__) +# define systemd_NR_statx 379 +# elif defined(_MIPS_SIM) +# if _MIPS_SIM == _MIPS_SIM_ABI32 +# define systemd_NR_statx 4366 +# elif _MIPS_SIM == _MIPS_SIM_NABI32 +# define systemd_NR_statx 6330 +# elif _MIPS_SIM == _MIPS_SIM_ABI64 +# define systemd_NR_statx 5326 +# else +# error "Unknown MIPS ABI" +# endif +# elif defined(__hppa__) +# define systemd_NR_statx 349 +# elif defined(__powerpc__) +# define systemd_NR_statx 383 +# elif defined(__riscv) +# if __riscv_xlen == 32 +# define systemd_NR_statx 291 +# elif __riscv_xlen == 64 +# define systemd_NR_statx 291 +# else +# error "Unknown RISC-V ABI" +# endif +# elif defined(__s390__) +# define systemd_NR_statx 379 +# elif defined(__sparc__) +# define systemd_NR_statx 360 +# elif defined(__x86_64__) +# if defined(__ILP32__) +# define systemd_NR_statx (332 | /* __X32_SYSCALL_BIT */ 0x40000000) +# else +# define systemd_NR_statx 332 +# endif +# elif !defined(missing_arch_template) +# warning "statx() syscall number is unknown for your architecture" +# endif + +/* may be an (invalid) negative number due to libseccomp, see PR 13319 */ +# if defined __NR_statx && __NR_statx >= 0 +# if defined systemd_NR_statx +G_STATIC_ASSERT (__NR_statx == systemd_NR_statx); +# endif +# else +# if defined __NR_statx +# undef __NR_statx +# endif +# if defined systemd_NR_statx && systemd_NR_statx >= 0 +# define __NR_statx systemd_NR_statx +# endif +# endif +#endif + +#if !defined(HAVE_GLNX_STATX) && defined(__NR_statx) +#define GLNX_STATX_TYPE 0x00000001U /* Want/got stx_mode & S_IFMT */ +#define GLNX_STATX_MODE 0x00000002U /* Want/got stx_mode & ~S_IFMT */ +#define GLNX_STATX_NLINK 0x00000004U /* Want/got stx_nlink */ +#define GLNX_STATX_UID 0x00000008U /* Want/got stx_uid */ +#define GLNX_STATX_GID 0x00000010U /* Want/got stx_gid */ +#define GLNX_STATX_ATIME 0x00000020U /* Want/got stx_atime */ +#define GLNX_STATX_MTIME 0x00000040U /* Want/got stx_mtime */ +#define GLNX_STATX_CTIME 0x00000080U /* Want/got stx_ctime */ +#define GLNX_STATX_INO 0x00000100U /* Want/got stx_ino */ +#define GLNX_STATX_SIZE 0x00000200U /* Want/got stx_size */ +#define GLNX_STATX_BLOCKS 0x00000400U /* Want/got stx_blocks */ +#define GLNX_STATX_BASIC_STATS 0x000007ffU /* The stuff in the normal stat struct */ +#define GLNX_STATX_BTIME 0x00000800U /* Want/got stx_btime */ +#define GLNX_STATX_MNT_ID 0x00001000U /* Got stx_mnt_id */ +#define GLNX_STATX_DIOALIGN 0x00002000U /* Want/got direct I/O alignment info */ +#define GLNX_STATX_MNT_ID_UNIQUE 0x00004000U /* Want/got extended stx_mount_id */ +#define GLNX_STATX_SUBVOL 0x00008000U /* Want/got stx_subvol */ +#define GLNX_STATX_WRITE_ATOMIC 0x00010000U /* Want/got atomic_write_* fields */ +#define GLNX_STATX_DIO_READ_ALIGN 0x00020000U /* Want/got dio read alignment info */ +#define GLNX_STATX__RESERVED 0x80000000U /* Reserved for future struct statx expansion */ + +struct glnx_statx_timestamp +{ + int64_t tv_sec; + uint32_t tv_nsec; + int32_t __reserved; +}; + +struct glnx_statx +{ + uint32_t stx_mask; + uint32_t stx_blksize; + uint64_t stx_attributes; + uint32_t stx_nlink; + uint32_t stx_uid; + uint32_t stx_gid; + uint16_t stx_mode; + uint16_t __spare0[1]; + uint64_t stx_ino; + uint64_t stx_size; + uint64_t stx_blocks; + uint64_t stx_attributes_mask; + struct glnx_statx_timestamp stx_atime; + struct glnx_statx_timestamp stx_btime; + struct glnx_statx_timestamp stx_ctime; + struct glnx_statx_timestamp stx_mtime; + uint32_t stx_rdev_major; + uint32_t stx_rdev_minor; + uint32_t stx_dev_major; + uint32_t stx_dev_minor; + uint64_t stx_mnt_id; + uint32_t stx_dio_mem_align; + uint32_t stx_dio_offset_align; + uint64_t stx_subvol; + uint32_t stx_atomic_write_unit_min; + uint32_t stx_atomic_write_unit_max; + uint32_t stx_atomic_write_segments_max; + uint32_t stx_dio_read_offset_align; + uint32_t stx_atomic_write_unit_max_opt; + uint32_t __spare2[1]; + uint64_t __spare3[8]; +}; + +static inline int +glnx_statx_syscall (int dfd, + const char *filename, + unsigned flags, + unsigned int mask, + struct glnx_statx *buf) +{ + memset (buf, 0xbf, sizeof (*buf)); + return syscall (__NR_statx, dfd, filename, flags, mask, buf); + return 0; +} + +#define HAVE_GLNX_STATX +#endif + +/* Copied from systemd git: ff83795469 ("boot: Improve log message") + * - open_tree + * - openat2 + */ + +#ifndef __IGNORE_open_tree +# if defined(__aarch64__) +# define systemd_NR_open_tree 428 +# elif defined(__alpha__) +# define systemd_NR_open_tree 538 +# elif defined(__arc__) || defined(__tilegx__) +# define systemd_NR_open_tree 428 +# elif defined(__arm__) +# define systemd_NR_open_tree 428 +# elif defined(__i386__) +# define systemd_NR_open_tree 428 +# elif defined(__ia64__) +# define systemd_NR_open_tree 1452 +# elif defined(__loongarch_lp64) +# define systemd_NR_open_tree 428 +# elif defined(__m68k__) +# define systemd_NR_open_tree 428 +# elif defined(_MIPS_SIM) +# if _MIPS_SIM == _MIPS_SIM_ABI32 +# define systemd_NR_open_tree 4428 +# elif _MIPS_SIM == _MIPS_SIM_NABI32 +# define systemd_NR_open_tree 6428 +# elif _MIPS_SIM == _MIPS_SIM_ABI64 +# define systemd_NR_open_tree 5428 +# else +# error "Unknown MIPS ABI" +# endif +# elif defined(__hppa__) +# define systemd_NR_open_tree 428 +# elif defined(__powerpc__) +# define systemd_NR_open_tree 428 +# elif defined(__riscv) +# if __riscv_xlen == 32 +# define systemd_NR_open_tree 428 +# elif __riscv_xlen == 64 +# define systemd_NR_open_tree 428 +# else +# error "Unknown RISC-V ABI" +# endif +# elif defined(__s390__) +# define systemd_NR_open_tree 428 +# elif defined(__sparc__) +# define systemd_NR_open_tree 428 +# elif defined(__x86_64__) +# if defined(__ILP32__) +# define systemd_NR_open_tree (428 | /* __X32_SYSCALL_BIT */ 0x40000000) +# else +# define systemd_NR_open_tree 428 +# endif +# elif !defined(missing_arch_template) +# warning "open_tree() syscall number is unknown for your architecture" +# endif + +/* may be an (invalid) negative number due to libseccomp, see PR 13319 */ +# if defined __NR_open_tree && __NR_open_tree >= 0 +# if defined systemd_NR_open_tree +G_STATIC_ASSERT (__NR_open_tree == systemd_NR_open_tree); +# endif +# else +# if defined __NR_open_tree +# undef __NR_open_tree +# endif +# if defined systemd_NR_open_tree && systemd_NR_open_tree >= 0 +# define __NR_open_tree systemd_NR_open_tree +# endif +# endif +#endif + +#if !defined(HAVE_OPEN_TREE) && defined(__NR_open_tree) +#ifndef OPEN_TREE_CLONE +#define OPEN_TREE_CLONE 1 +#endif + +#ifndef OPEN_TREE_CLOEXEC +#define OPEN_TREE_CLOEXEC O_CLOEXEC +#endif + +static inline int +inline_open_tree (int dfd, + const char *filename, + unsigned flags) +{ + return syscall(__NR_open_tree, dfd, filename, flags); +} +#define open_tree inline_open_tree +#define HAVE_OPEN_TREE +#endif + +#ifndef __IGNORE_openat2 +# if defined(__aarch64__) +# define systemd_NR_openat2 437 +# elif defined(__alpha__) +# define systemd_NR_openat2 547 +# elif defined(__arc__) || defined(__tilegx__) +# define systemd_NR_openat2 437 +# elif defined(__arm__) +# define systemd_NR_openat2 437 +# elif defined(__i386__) +# define systemd_NR_openat2 437 +# elif defined(__ia64__) +# define systemd_NR_openat2 1461 +# elif defined(__loongarch_lp64) +# define systemd_NR_openat2 437 +# elif defined(__m68k__) +# define systemd_NR_openat2 437 +# elif defined(_MIPS_SIM) +# if _MIPS_SIM == _MIPS_SIM_ABI32 +# define systemd_NR_openat2 4437 +# elif _MIPS_SIM == _MIPS_SIM_NABI32 +# define systemd_NR_openat2 6437 +# elif _MIPS_SIM == _MIPS_SIM_ABI64 +# define systemd_NR_openat2 5437 +# else +# error "Unknown MIPS ABI" +# endif +# elif defined(__hppa__) +# define systemd_NR_openat2 437 +# elif defined(__powerpc__) +# define systemd_NR_openat2 437 +# elif defined(__riscv) +# if __riscv_xlen == 32 +# define systemd_NR_openat2 437 +# elif __riscv_xlen == 64 +# define systemd_NR_openat2 437 +# else +# error "Unknown RISC-V ABI" +# endif +# elif defined(__s390__) +# define systemd_NR_openat2 437 +# elif defined(__sparc__) +# define systemd_NR_openat2 437 +# elif defined(__x86_64__) +# if defined(__ILP32__) +# define systemd_NR_openat2 (437 | /* __X32_SYSCALL_BIT */ 0x40000000) +# else +# define systemd_NR_openat2 437 +# endif +# elif !defined(missing_arch_template) +# warning "openat2() syscall number is unknown for your architecture" +# endif + +/* may be an (invalid) negative number due to libseccomp, see PR 13319 */ +# if defined __NR_openat2 && __NR_openat2 >= 0 +# if defined systemd_NR_openat2 +G_STATIC_ASSERT (__NR_openat2 == systemd_NR_openat2); +# endif +# else +# if defined __NR_openat2 +# undef __NR_openat2 +# endif +# if defined systemd_NR_openat2 && systemd_NR_openat2 >= 0 +# define __NR_openat2 systemd_NR_openat2 +# endif +# endif +#endif + +#if !defined(HAVE_OPENAT2) && defined(__NR_openat2) +#ifndef RESOLVE_NO_XDEV +#define RESOLVE_NO_XDEV 0x01 +#endif + +#ifndef RESOLVE_NO_MAGICLINKS +#define RESOLVE_NO_MAGICLINKS 0x02 +#endif + +#ifndef RESOLVE_NO_SYMLINKS +#define RESOLVE_NO_SYMLINKS 0x04 +#endif + +#ifndef RESOLVE_BENEATH +#define RESOLVE_BENEATH 0x08 +#endif + +#ifndef RESOLVE_IN_ROOT +#define RESOLVE_IN_ROOT 0x10 +#endif + +#ifndef RESOLVE_CACHED +#define RESOLVE_CACHED 0x20 +#endif + +struct inline_open_how { + uint64_t flags; + uint64_t mode; + uint64_t resolve; +}; +#define open_how inline_open_how + +static inline int +inline_openat2 (int dfd, + const char *filename, + void *buffer, + size_t size) +{ + return syscall(__NR_openat2, dfd, filename, buffer, size); +} +#define openat2 inline_openat2 +#define HAVE_OPENAT2 +#endif diff --git a/subprojects/libglnx/libglnx.h b/subprojects/libglnx/libglnx.h index 63d73adc..2b5e07f9 100644 --- a/subprojects/libglnx/libglnx.h +++ b/subprojects/libglnx/libglnx.h @@ -31,6 +31,7 @@ G_BEGIN_DECLS #include #include #include +#include #include #include #include diff --git a/subprojects/libglnx/meson.build b/subprojects/libglnx/meson.build index 7303fa32..0fff39d2 100644 --- a/subprojects/libglnx/meson.build +++ b/subprojects/libglnx/meson.build @@ -76,6 +76,8 @@ libglnx_sources = [ 'glnx-backport-testutils.h', 'glnx-backports.c', 'glnx-backports.h', + 'glnx-chase.c', + 'glnx-chase.h', 'glnx-console.c', 'glnx-console.h', 'glnx-dirfd.c', diff --git a/subprojects/libglnx/tests/meson.build b/subprojects/libglnx/tests/meson.build index 6c46b45c..5b773e8e 100644 --- a/subprojects/libglnx/tests/meson.build +++ b/subprojects/libglnx/tests/meson.build @@ -34,6 +34,7 @@ if get_option('tests') test_names = [ 'backports', + 'chase', 'errors', 'fdio', 'macros', diff --git a/subprojects/libglnx/tests/test-libglnx-chase.c b/subprojects/libglnx/tests/test-libglnx-chase.c new file mode 100644 index 00000000..b0ce1b41 --- /dev/null +++ b/subprojects/libglnx/tests/test-libglnx-chase.c @@ -0,0 +1,609 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2026 Red Hat, Inc. + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "libglnx-config.h" +#include "libglnx.h" +#include +#include +#include +#include +#include + +#include "libglnx-testlib.h" + +#define GLNX_CHASE_DEBUG_NO_OPENAT2 (1U << 31) +#define GLNX_CHASE_DEBUG_NO_OPEN_TREE (1U << 30) + +const char *test_paths[] = { + "file/baz", + "file/baz/", + "file/baz/.", + "file/baz/../baz", + "file////baz/..//baz", + "file////baz/..//../file/baz", +}; + +static ino_t +get_ino (int fd) +{ + int r; + struct stat st; + + r = fstatat (fd, "", &st, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); + g_assert_cmpint (r, >=, 0); + + return st.st_ino; +} + +static ino_t +path_get_ino (const char *path) +{ + int r; + struct stat st; + + r = fstatat (AT_FDCWD, path, &st, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); + g_assert_cmpint (r, >=, 0); + + return st.st_ino; +} + +static char * +get_abspath (int dfd, + const char *path) +{ + g_autofree char *proc_fd_path = NULL; + g_autofree char *abs = NULL; + g_autoptr(GError) error = NULL; + + proc_fd_path = g_strdup_printf ("/proc/self/fd/%d", dfd); + abs = glnx_readlinkat_malloc (AT_FDCWD, proc_fd_path, NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (abs); + + return g_strdup_printf ("%s/%s", abs, path); +} + +static void +check_chase (int dfd, + const char *path, + GlnxChaseFlags flags, + int expected_ino) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int chase_fd = -1; + + /* let's try to test the openat2 impl */ + chase_fd = glnx_chaseat (dfd, path, flags, &error); + g_assert_no_error (error); + g_assert_cmpint (chase_fd, >=, 0); + g_assert_cmpint (get_ino (chase_fd), ==, expected_ino); + g_clear_fd (&chase_fd, NULL); + + /* let's try to test the open_tree impl */ + chase_fd = glnx_chaseat (dfd, path, + flags | GLNX_CHASE_DEBUG_NO_OPENAT2, + &error); + g_assert_no_error (error); + g_assert_cmpint (chase_fd, >=, 0); + g_assert_cmpint (get_ino (chase_fd), ==, expected_ino); + g_clear_fd (&chase_fd, NULL); + + /* let's try to test the openat impl */ + chase_fd = glnx_chaseat (dfd, path, + flags | + GLNX_CHASE_DEBUG_NO_OPENAT2 | + GLNX_CHASE_DEBUG_NO_OPEN_TREE, + &error); + g_assert_no_error (error); + g_assert_cmpint (chase_fd, >=, 0); + g_assert_cmpint (get_ino (chase_fd), ==, expected_ino); + g_clear_fd (&chase_fd, NULL); +} + +static void +check_chase_error (int dfd, + const char *path, + GlnxChaseFlags flags, + GQuark err_domain, + gint err_code) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int chase_fd = -1; + + /* let's try to test the openat2 impl */ + chase_fd = glnx_chaseat (dfd, path, flags, &error); + g_assert_cmpint (chase_fd, <, 0); + g_assert_error (error, err_domain, err_code); + g_clear_error (&error); + + /* let's try to test the open_tree impl */ + chase_fd = glnx_chaseat (dfd, path, + flags | GLNX_CHASE_DEBUG_NO_OPENAT2, + &error); + g_assert_cmpint (chase_fd, <, 0); + g_assert_error (error, err_domain, err_code); + g_clear_error (&error); + + /* let's try to test the openat impl */ + chase_fd = glnx_chaseat (dfd, path, + flags | + GLNX_CHASE_DEBUG_NO_OPENAT2 | + GLNX_CHASE_DEBUG_NO_OPEN_TREE, + &error); + g_assert_cmpint (chase_fd, <, 0); + g_assert_error (error, err_domain, err_code); + g_clear_error (&error); +} + +static void +test_chase_relative (void) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int dfd = -1; + int expected_ino; + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755, + &dfd, + NULL, &error)); + g_assert_no_error (error); + g_assert_cmpint (dfd, >=, 0); + + expected_ino = get_ino (dfd); + + for (size_t i = 0; i < G_N_ELEMENTS (test_paths); i++) + check_chase (AT_FDCWD, test_paths[i], 0, expected_ino); + + check_chase_error (AT_FDCWD, "nope", 0, G_IO_ERROR, G_IO_ERROR_NOT_FOUND); +} + +static void +test_chase_relative_fd (void) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int dfd = -1; + int expected_ino; + glnx_autofd int cwdfd = -1; + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755, + &dfd, + NULL, &error)); + g_assert_no_error (error); + g_assert_cmpint (dfd, >=, 0); + + expected_ino = get_ino (dfd); + + cwdfd = openat (AT_FDCWD, ".", O_PATH | O_CLOEXEC | O_NOFOLLOW); + g_assert_cmpint (cwdfd, >=, 0); + + for (size_t i = 0; i < G_N_ELEMENTS (test_paths); i++) + check_chase (cwdfd, test_paths[i], 0, expected_ino); + + check_chase_error (cwdfd, "nope", 0, G_IO_ERROR, G_IO_ERROR_NOT_FOUND); +} + +static void +test_chase_absolute (void) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int dfd = -1; + int expected_ino; + glnx_autofd int cwdfd = -1; + g_autofree char *proc_fd_path = NULL; + g_autofree char *cwd_path = NULL; + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755, + &dfd, + NULL, &error)); + g_assert_no_error (error); + g_assert_cmpint (dfd, >=, 0); + + expected_ino = get_ino (dfd); + + cwdfd = openat (AT_FDCWD, ".", O_PATH | O_CLOEXEC | O_NOFOLLOW); + g_assert_cmpint (cwdfd, >=, 0); + + cwd_path = get_abspath (cwdfd, ""); + + for (size_t i = 0; i < G_N_ELEMENTS (test_paths); i++) + { + g_autofree char *abspath = NULL; + + abspath = g_strdup_printf ("%s/%s", cwd_path, test_paths[i]); + check_chase (AT_FDCWD, abspath, 0, expected_ino); + } + + check_chase_error (AT_FDCWD, "/nope/nope/nope/345298308497623012313243543", 0, + G_IO_ERROR, G_IO_ERROR_NOT_FOUND); +} + +static void +test_chase_link (void) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int dfd = -1; + int link_ino; + int target_ino; + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755, + &dfd, + NULL, &error)); + g_assert_no_error (error); + g_assert_cmpint (dfd, >=, 0); + + g_assert_cmpint (symlinkat ("file/baz", AT_FDCWD, "link"), ==, 0); + + target_ino = get_ino (dfd); + link_ino = path_get_ino ("link"); + + check_chase (AT_FDCWD, "link", 0, target_ino); + check_chase (AT_FDCWD, "link/", 0, target_ino); + check_chase (AT_FDCWD, "link///", 0, target_ino); + check_chase (AT_FDCWD, "link/.//.", 0, target_ino); + check_chase (AT_FDCWD, "link", 0, target_ino); + + check_chase (AT_FDCWD, "link", GLNX_CHASE_NOFOLLOW, link_ino); + check_chase (AT_FDCWD, "./file/../link", GLNX_CHASE_NOFOLLOW, link_ino); + check_chase (AT_FDCWD, "link/", GLNX_CHASE_NOFOLLOW, target_ino); + check_chase (AT_FDCWD, "././link/.", GLNX_CHASE_NOFOLLOW, target_ino); + check_chase (AT_FDCWD, "link/.//", GLNX_CHASE_NOFOLLOW, target_ino); + + check_chase (AT_FDCWD, "link", + GLNX_CHASE_NOFOLLOW | GLNX_CHASE_RESOLVE_NO_SYMLINKS, + link_ino); + check_chase_error (AT_FDCWD, "link", + GLNX_CHASE_RESOLVE_NO_SYMLINKS, + G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS); +} + +static void +test_chase_resolve (void) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int foo_dfd = -1; + glnx_autofd int bar_dfd = -1; + g_autofree char *foo_abspath = NULL; + int ino; + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "foo", 0755, + &foo_dfd, + NULL, &error)); + g_assert_no_error (error); + g_assert_cmpint (foo_dfd, >=, 0); + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "foo/bar", 0755, + &bar_dfd, + NULL, &error)); + g_assert_no_error (error); + g_assert_cmpint (bar_dfd, >=, 0); + + foo_abspath = get_abspath (foo_dfd, ""); + + g_assert_cmpint (symlinkat ("..", foo_dfd, "link1"), ==, 0); + g_assert_cmpint (symlinkat ("bar/../..", foo_dfd, "link2"), ==, 0); + g_assert_cmpint (symlinkat (foo_abspath, foo_dfd, "link3"), ==, 0); + g_assert_cmpint (symlinkat ("/bar", foo_dfd, "link4"), ==, 0); + g_assert_cmpint (symlinkat ("link1/foo", foo_dfd, "link5"), ==, 0); + g_assert_cmpint (symlinkat ("link7", foo_dfd, "link6"), ==, 0); + g_assert_cmpint (symlinkat ("link6", foo_dfd, "link7"), ==, 0); + + ino = get_ino (bar_dfd); + + /* A bunch of different ways to get from CWD and foo to bar */ + check_chase (foo_dfd, "./bar", 0, ino); + check_chase (foo_dfd, "../foo/bar", 0, ino); + check_chase (foo_dfd, "link1/foo/bar", 0, ino); + check_chase (AT_FDCWD, "foo/link1/foo/bar", 0, ino); + check_chase (foo_dfd, "link2/foo/bar", 0, ino); + check_chase (AT_FDCWD, ".///foo/./link2/foo/bar", 0, ino); + check_chase (foo_dfd, "link3/bar", 0, ino); + check_chase (AT_FDCWD, ".///foo/./link3/bar", 0, ino); + check_chase (foo_dfd, "link5/bar", 0, ino); + + /* check that NO_SYMLINKS works with a component in the middle */ + check_chase_error (AT_FDCWD, "foo/link3/bar", + GLNX_CHASE_RESOLVE_NO_SYMLINKS, + G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS); + + /* link6 points to link 7, points to link6, ... This should error out! */ + check_chase_error (foo_dfd, "link6/bar", 0, + G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS); + + /* Test with links which never go below the dfd */ + check_chase (AT_FDCWD, "foo/link1/foo/bar", + GLNX_CHASE_RESOLVE_BENEATH, + ino); + check_chase (AT_FDCWD, "foo/link2/foo/bar", + GLNX_CHASE_RESOLVE_BENEATH, + ino); + /* An absolute link is always below the dfd */ + check_chase_error (AT_FDCWD, "foo/link3/foo/bar", + GLNX_CHASE_RESOLVE_BENEATH, + G_IO_ERROR, G_IO_ERROR_FAILED); + + /* Same, but from foo instead of cwd */ + check_chase_error (foo_dfd, "link1/foo/bar", + GLNX_CHASE_RESOLVE_BENEATH, + G_IO_ERROR, G_IO_ERROR_FAILED); + check_chase_error (foo_dfd, "link2/foo/bar", + GLNX_CHASE_RESOLVE_BENEATH, + G_IO_ERROR, G_IO_ERROR_FAILED); + check_chase_error (foo_dfd, "link3/foo/bar", + GLNX_CHASE_RESOLVE_BENEATH, + G_IO_ERROR, G_IO_ERROR_FAILED); + + /* Check that trying to be below the dfd with RESOLVE_IN_ROOT resolves to the + * dfd itself */ + check_chase (foo_dfd, "link1/bar", + GLNX_CHASE_RESOLVE_IN_ROOT, + ino); + check_chase (foo_dfd, "link2/bar", + GLNX_CHASE_RESOLVE_IN_ROOT, + ino); + /* The absolute link is relative to dfd with RESOLVE_IN_ROOT, so this + * fails... */ + check_chase_error (foo_dfd, "link3", + GLNX_CHASE_RESOLVE_IN_ROOT, + G_IO_ERROR, G_IO_ERROR_NOT_FOUND); + /* ... but the link /bar resolves correctly from foo as dfd. */ + check_chase (foo_dfd, "link4", + GLNX_CHASE_RESOLVE_IN_ROOT, + ino); +} + +static void +test_chase_resolve_in_root_absolute (void) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int foo_dfd = -1; + glnx_autofd int bar_dfd = -1; + glnx_autofd int baz_dfd = -1; + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "foo", 0755, + &foo_dfd, + NULL, &error)); + g_assert_no_error (error); + g_assert_cmpint (foo_dfd, >=, 0); + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "foo/bar", 0755, + &bar_dfd, + NULL, &error)); + g_assert_no_error (error); + g_assert_cmpint (bar_dfd, >=, 0); + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "foo/bar/baz", 0755, + &baz_dfd, + NULL, &error)); + g_assert_no_error (error); + g_assert_cmpint (baz_dfd, >=, 0); + + /* Test the absolute symlink doesn't break tracking of the root level */ + g_assert_cmpint (symlinkat ("/..", baz_dfd, "link1"), ==, 0); + + /* We should not be able to break out of the root! */ + check_chase (bar_dfd, "./baz/link1", GLNX_CHASE_RESOLVE_IN_ROOT, get_ino (bar_dfd)); +} + +static void +check_chase_and_statxat (int dfd, + const char *path, + GlnxChaseFlags flags, + ino_t expected_ino, + mode_t expected_type) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int chase_fd = -1; + struct glnx_statx stx; + + /* let's try to test the openat2 impl */ + chase_fd = glnx_chase_and_statxat (dfd, path, flags, + GLNX_STATX_TYPE | GLNX_STATX_INO, + &stx, &error); + g_assert_cmpint (chase_fd, >=, 0); + g_assert_no_error (error); + g_assert_cmpint (stx.stx_ino, ==, expected_ino); + g_assert_cmpint (stx.stx_mode & S_IFMT, ==, expected_type); + g_clear_fd (&chase_fd, NULL); + + /* let's try to test the open_tree impl */ + chase_fd = glnx_chase_and_statxat (dfd, path, + flags | GLNX_CHASE_DEBUG_NO_OPENAT2, + GLNX_STATX_TYPE | GLNX_STATX_INO, + &stx, &error); + g_assert_cmpint (chase_fd, >=, 0); + g_assert_no_error (error); + g_assert_cmpint (stx.stx_ino, ==, expected_ino); + g_assert_cmpint (stx.stx_mode & S_IFMT, ==, expected_type); + g_clear_fd (&chase_fd, NULL); + + /* let's try to test the openat impl */ + chase_fd = glnx_chase_and_statxat (dfd, path, + flags | + GLNX_CHASE_DEBUG_NO_OPENAT2 | + GLNX_CHASE_DEBUG_NO_OPEN_TREE, + GLNX_STATX_TYPE | GLNX_STATX_INO, + &stx, &error); + g_assert_cmpint (chase_fd, >=, 0); + g_assert_no_error (error); + g_assert_cmpint (stx.stx_ino, ==, expected_ino); + g_assert_cmpint (stx.stx_mode & S_IFMT, ==, expected_type); + g_clear_fd (&chase_fd, NULL); +} + +static void +check_chase_and_statxat_error (int dfd, + const char *path, + GlnxChaseFlags flags, + GQuark err_domain, + gint err_code) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int chase_fd = -1; + struct glnx_statx stx; + + /* let's try to test the openat2 impl */ + chase_fd = glnx_chase_and_statxat (dfd, path, flags, + GLNX_STATX_TYPE | GLNX_STATX_INO, + &stx, &error); + g_assert_cmpint (chase_fd, <, 0); + g_assert_error (error, err_domain, err_code); + g_clear_error (&error); + + /* let's try to test the open_tree impl */ + chase_fd = glnx_chase_and_statxat (dfd, path, + flags | GLNX_CHASE_DEBUG_NO_OPENAT2, + GLNX_STATX_TYPE | GLNX_STATX_INO, + &stx, &error); + g_assert_cmpint (chase_fd, <, 0); + g_assert_error (error, err_domain, err_code); + g_clear_error (&error); + + /* let's try to test the openat impl */ + chase_fd = glnx_chase_and_statxat (dfd, path, + flags | + GLNX_CHASE_DEBUG_NO_OPENAT2 | + GLNX_CHASE_DEBUG_NO_OPEN_TREE, + GLNX_STATX_TYPE | GLNX_STATX_INO, + &stx, &error); + g_assert_cmpint (chase_fd, <, 0); + g_assert_error (error, err_domain, err_code); + g_clear_error (&error); +} + +static void +test_chase_and_statxat_basic (void) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int dfd = -1; + glnx_autofd int file_fd = -1; + ino_t expected_ino; + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755, + &dfd, + NULL, &error)); + g_assert_no_error (error); + g_assert_cmpint (dfd, >=, 0); + + expected_ino = get_ino (dfd); + + /* Test with various path forms */ + for (size_t i = 0; i < G_N_ELEMENTS (test_paths); i++) + check_chase_and_statxat (AT_FDCWD, test_paths[i], 0, expected_ino, S_IFDIR); + + /* Create a regular file and test it */ + file_fd = openat (dfd, "testfile", O_WRONLY | O_CREAT | O_CLOEXEC, 0644); + g_assert_cmpint (file_fd, >=, 0); + g_clear_fd (&file_fd, NULL); + + expected_ino = path_get_ino ("file/baz/testfile"); + check_chase_and_statxat (AT_FDCWD, "file/baz/testfile", 0, expected_ino, S_IFREG); + + /* Test error cases */ + check_chase_and_statxat_error (AT_FDCWD, "nope", 0, G_IO_ERROR, G_IO_ERROR_NOT_FOUND); +} + +static void +test_chase_and_statxat_symlink (void) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int dfd = -1; + glnx_autofd int chase_fd = -1; + ino_t link_ino; + ino_t target_ino; + struct glnx_statx stx; + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755, + &dfd, + NULL, &error)); + g_assert_no_error (error); + g_assert_cmpint (dfd, >=, 0); + + g_assert_cmpint (symlinkat ("file/baz", AT_FDCWD, "fstatlink"), ==, 0); + + target_ino = get_ino (dfd); + link_ino = path_get_ino ("fstatlink"); + + /* Following symlinks should give us the directory */ + check_chase_and_statxat (AT_FDCWD, "fstatlink", 0, target_ino, S_IFDIR); + check_chase_and_statxat (AT_FDCWD, "fstatlink/", 0, target_ino, S_IFDIR); + + /* With NOFOLLOW, we should get the symlink itself */ + check_chase_and_statxat (AT_FDCWD, "fstatlink", GLNX_CHASE_NOFOLLOW, link_ino, S_IFLNK); + + /* Verify we can distinguish between regular files, directories, and symlinks */ + chase_fd = glnx_chase_and_statxat (AT_FDCWD, "fstatlink", GLNX_CHASE_NOFOLLOW, + GLNX_STATX_TYPE | GLNX_STATX_INO, + &stx, &error); + g_assert_cmpint (chase_fd, >=, 0); + g_assert_no_error (error); + g_assert_true (S_ISLNK (stx.stx_mode)); + g_clear_fd (&chase_fd, NULL); + + chase_fd = glnx_chase_and_statxat (AT_FDCWD, "fstatlink", 0, + GLNX_STATX_TYPE | GLNX_STATX_INO, + &stx, &error); + g_assert_cmpint (chase_fd, >=, 0); + g_assert_no_error (error); + g_assert_true (S_ISDIR (stx.stx_mode)); + g_clear_fd (&chase_fd, NULL); + + /* Test with RESOLVE_NO_SYMLINKS */ + check_chase_and_statxat_error (AT_FDCWD, "fstatlink", + GLNX_CHASE_RESOLVE_NO_SYMLINKS, + G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS); +} + +static void +test_chase_and_statxat_permissions (void) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int dfd = -1; + glnx_autofd int file_fd = -1; + glnx_autofd int chase_fd = -1; + struct glnx_statx stx; + mode_t expected_mode = 0640; + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "permtest", 0755, + &dfd, + NULL, &error)); + g_assert_no_error (error); + + /* Create a file with specific permissions */ + file_fd = openat (dfd, "testfile", O_WRONLY | O_CREAT | O_CLOEXEC, expected_mode); + g_assert_cmpint (file_fd, >=, 0); + g_clear_fd (&file_fd, NULL); + + /* Verify that glnx_chase_and_statxat returns the correct permissions */ + chase_fd = glnx_chase_and_statxat (dfd, "testfile", 0, + GLNX_STATX_TYPE | GLNX_STATX_MODE, + &stx, &error); + g_assert_cmpint (chase_fd, >=, 0); + g_assert_no_error (error); + g_assert_cmpint (stx.stx_mode & 0777, ==, expected_mode); + g_assert_true (S_ISREG (stx.stx_mode)); + g_clear_fd (&chase_fd, NULL); +} + +int main (int argc, char **argv) +{ + _GLNX_TEST_SCOPED_TEMP_DIR; + int ret; + + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/chase-relative", test_chase_relative); + g_test_add_func ("/chase-relative-fd", test_chase_relative_fd); + g_test_add_func ("/chase-absolute", test_chase_absolute); + g_test_add_func ("/chase-link", test_chase_link); + g_test_add_func ("/chase-resolve", test_chase_resolve); + g_test_add_func ("/chase-resolve-in-root-absolute", test_chase_resolve_in_root_absolute); + g_test_add_func ("/chase-and-statxat-basic", test_chase_and_statxat_basic); + g_test_add_func ("/chase-and-statxat-symlink", test_chase_and_statxat_symlink); + g_test_add_func ("/chase-and-statxat-permissions", test_chase_and_statxat_permissions); + + ret = g_test_run(); + + return ret; +} diff --git a/subprojects/libglnx/tests/test-libglnx-fdio.c b/subprojects/libglnx/tests/test-libglnx-fdio.c index b9aa682a..cfa5b648 100644 --- a/subprojects/libglnx/tests/test-libglnx-fdio.c +++ b/subprojects/libglnx/tests/test-libglnx-fdio.c @@ -286,6 +286,158 @@ test_filecopy_procfs (void) } } +static void +test_fd_reopen (void) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int dfd = -1; + glnx_autofd int opath_fd = -1; + glnx_autofd int regular_fd = -1; + glnx_autofd int testfile_fd = -1; + glnx_autofd int link_opath_fd = -1; + glnx_autofd int reopened_fd = -1; + struct stat st1, st2; + const char *test_data = "test content"; + char buf[100]; + ssize_t n; + gboolean ok; + int flags; + + /* Create a test directory and file */ + ok = glnx_shutil_mkdir_p_at_open (AT_FDCWD, "reopen_test", 0755, &dfd, NULL, &error); + g_assert_no_error (error); + g_assert_true (ok); + g_assert_no_errno (dfd); + + glnx_file_replace_contents_at (dfd, "testfile", + (const void *) test_data, strlen (test_data), + GLNX_FILE_REPLACE_NODATASYNC, NULL, &error); + g_assert_no_error (error); + + /* Test 1: Reopen O_PATH fd as regular fd for reading and writing */ + opath_fd = openat (dfd, "testfile", O_PATH | O_CLOEXEC); + g_assert_no_errno (opath_fd); + + regular_fd = glnx_fd_reopen (opath_fd, O_RDWR, &error); + g_assert_no_errno (regular_fd); + g_assert_no_error (error); + + flags = fcntl (regular_fd, F_GETFL); + g_assert_no_errno (flags); + g_assert_cmpint (flags & (O_RDONLY | O_WRONLY | O_RDWR), ==, O_RDWR); + g_assert_cmpint (flags & (O_PATH | O_DIRECTORY | O_NOFOLLOW), ==, 0); + flags = fcntl (regular_fd, F_GETFD); + g_assert_no_errno (flags); + g_assert_cmpint (flags & FD_CLOEXEC, ==, FD_CLOEXEC); + + /* Verify we can read from the reopened fd */ + n = read (regular_fd, buf, sizeof (buf)); + g_assert_cmpmem (buf, n, test_data, strlen (test_data)); + + g_clear_fd (®ular_fd, NULL); + g_clear_fd (&opath_fd, NULL); + + /* Test 2: Reopen directory fd with O_DIRECTORY */ + opath_fd = openat (AT_FDCWD, "reopen_test", O_PATH | O_CLOEXEC); + g_assert_no_errno (opath_fd); + + reopened_fd = glnx_fd_reopen (opath_fd, O_RDONLY | O_DIRECTORY, &error); + g_assert_no_error (error); + g_assert_no_errno (reopened_fd); + + flags = fcntl (reopened_fd, F_GETFL); + g_assert_no_errno (flags); + g_assert_cmpint (flags & (O_RDONLY | O_WRONLY | O_RDWR), ==, O_RDONLY); + g_assert_cmpint (flags & (O_PATH | O_DIRECTORY | O_NOFOLLOW), ==, O_DIRECTORY); + flags = fcntl (reopened_fd, F_GETFD); + g_assert_no_errno (flags); + g_assert_cmpint (flags & FD_CLOEXEC, ==, FD_CLOEXEC); + + /* Verify both fds point to the same inode */ + g_assert_no_errno (fstat (opath_fd, &st1)); + g_assert_no_errno (fstat (reopened_fd, &st2)); + g_assert_cmpint (st1.st_ino, ==, st2.st_ino); + + g_clear_fd (&reopened_fd, NULL); + g_clear_fd (&opath_fd, NULL); + + /* Test 3: Reopen AT_FDCWD */ + reopened_fd = glnx_fd_reopen (AT_FDCWD, O_RDONLY | O_DIRECTORY, &error); + g_assert_no_error (error); + g_assert_no_errno (reopened_fd); + + g_clear_fd (&reopened_fd, NULL); + + /* Test 4: Test that O_NOFOLLOW is rejected */ + opath_fd = openat (dfd, "testfile", O_PATH | O_CLOEXEC); + g_assert_no_errno (opath_fd); + + regular_fd = glnx_fd_reopen (opath_fd, O_RDONLY | O_NOFOLLOW, &error); + g_assert_cmpint (regular_fd, <, 0); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS); + g_clear_error (&error); + + g_clear_fd (&opath_fd, NULL); + + /* Test 5: Reopen O_PATH fd to symlink with O_PATH (should work) */ + g_assert_no_errno (symlinkat ("testfile", dfd, "testlink")); + + link_opath_fd = openat (dfd, "testlink", O_PATH | O_NOFOLLOW); + g_assert_no_errno (link_opath_fd); + + /* Verify it's a symlink */ + g_assert_no_errno (fstatat (link_opath_fd, "", &st1, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)); + g_assert_true (S_ISLNK (st1.st_mode)); + + /* Reopen with O_PATH should work */ + reopened_fd = glnx_fd_reopen (link_opath_fd, O_PATH, &error); + g_assert_no_error (error); + g_assert_no_errno (reopened_fd); + + flags = fcntl (reopened_fd, F_GETFL); + g_assert_no_errno (flags); + g_assert_cmpint (flags & (O_RDONLY | O_WRONLY | O_RDWR), ==, O_RDONLY); + g_assert_cmpint (flags & (O_PATH | O_DIRECTORY | O_NOFOLLOW), ==, O_PATH); + flags = fcntl (reopened_fd, F_GETFD); + g_assert_no_errno (flags); + g_assert_cmpint (flags & FD_CLOEXEC, ==, FD_CLOEXEC); + + /* Verify both point to the same symlink */ + g_assert_no_errno (fstatat (reopened_fd, "", &st2, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)); + g_assert_cmpint (st1.st_ino, ==, st2.st_ino); + g_assert_true (S_ISLNK (st2.st_mode)); + + g_clear_fd (&reopened_fd, NULL); + + /* Test 6: Reopening O_PATH fd to symlink without O_PATH should fail with ELOOP */ + reopened_fd = glnx_fd_reopen (link_opath_fd, O_RDONLY, &error); + g_assert_cmpint (reopened_fd, <, 0); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS); + g_clear_error (&error); + + g_clear_fd (&link_opath_fd, NULL); + + /* Test 7: Verify read index is reset */ + testfile_fd = openat (dfd, "testfile", O_RDONLY | O_CLOEXEC); + g_assert_no_errno (testfile_fd); + + /* Read some data to advance the read index */ + n = read (testfile_fd, buf, 4); + g_assert_cmpint (n, ==, 4); + + /* Reopen should reset the read index */ + reopened_fd = glnx_fd_reopen (testfile_fd, O_RDONLY, &error); + g_assert_no_error (error); + g_assert_no_errno (reopened_fd); + + /* Should read from the beginning again */ + n = read (reopened_fd, buf, sizeof (buf)); + g_assert_cmpmem (buf, n, test_data, strlen (test_data)); + + g_clear_fd (&reopened_fd, NULL); + g_clear_fd (&testfile_fd, NULL); +} + int main (int argc, char **argv) { _GLNX_TEST_SCOPED_TEMP_DIR; @@ -300,6 +452,7 @@ int main (int argc, char **argv) g_test_add_func ("/renameat2-noreplace", test_renameat2_noreplace); g_test_add_func ("/renameat2-exchange", test_renameat2_exchange); g_test_add_func ("/fstat", test_fstatat); + g_test_add_func ("/fd-reopen", test_fd_reopen); ret = g_test_run();