Update subtree: libglnx 2026-04-07

* fdio: Avoid relying on VLAs or gcc-specific constant-folding
* errors: Fix URL to an old libgsystem commit
* lockfile: Assert non-null path in make_lock_file for analyzers
* backports: Add g_clear_fd
* glnx-errors.h: add glnx_fd_throw[_*] variants
* fdio: Add glnx_fd_reopen
* local-alloc: Remove duplicate definition of glnx_unref_object
* fdio: Add glnx_statx
* chase: Add glnx_chaseat which functions similar to openat2
* chase: Add glnx_chase_and_statxat

Signed-off-by: Simon McVittie <smcv@collabora.com>
This commit is contained in:
Simon McVittie
2026-04-07 20:39:41 +01:00
16 changed files with 2191 additions and 6 deletions

View File

@@ -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 \

View File

@@ -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)). */

View File

@@ -29,6 +29,7 @@
#include <string.h>
#include <glib-unix.h>
#include <glib/gstdio.h>
#include <gio/gio.h>
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

View File

@@ -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 <fcntl.h>
#include <stdint.h>
#include <sys/mount.h>
#include <sys/statfs.h>
#include <sys/syscall.h>
#include <sys/vfs.h>
#include <unistd.h>
#include <glnx-backports.h>
#include <glnx-errors.h>
#include <glnx-fdio.h>
#include <glnx-local-alloc.h>
#include <glnx-missing.h>
#include <glnx-chase.h>
#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);
}

View File

@@ -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 <glib.h>
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

View File

@@ -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) \

View File

@@ -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);
}

View File

@@ -22,6 +22,7 @@
#pragma once
#include <glnx-backport-autocleanups.h>
#include <glnx-missing.h>
#include <gio/gfiledescriptorbased.h>
#include <limits.h>
#include <dirent.h>
@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -33,6 +33,7 @@
#include "libglnx-config.h"
#include <glib.h>
#include <stdint.h>
#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

View File

@@ -31,6 +31,7 @@ G_BEGIN_DECLS
#include <glnx-backport-autocleanups.h>
#include <glnx-backport-testutils.h>
#include <glnx-backports.h>
#include <glnx-chase.h>
#include <glnx-lockfile.h>
#include <glnx-errors.h>
#include <glnx-dirfd.h>

View File

@@ -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',

View File

@@ -34,6 +34,7 @@ if get_option('tests')
test_names = [
'backports',
'chase',
'errors',
'fdio',
'macros',

View File

@@ -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 <glib.h>
#include <stdlib.h>
#include <gio/gio.h>
#include <err.h>
#include <string.h>
#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;
}

View File

@@ -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 (&regular_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();