chase: Add glnx_chaseat which functions similar to openat2

The selling features are:

* Support for RESOLVE_BENEATH, RESOLVE_IN_ROOT and RESOLVE_NO_SYMLINKS
* Fallback from openat2 to open_tree to openat for compatibility
* Triggering of automounts
This commit is contained in:
Sebastian Wick
2026-02-17 23:00:36 +01:00
parent 2a75ac86e9
commit a973baad08
4 changed files with 781 additions and 0 deletions

734
glnx-chase.c Normal file
View File

@@ -0,0 +1,734 @@
/* -*- 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 (1 << 31)
#define GLNX_CHASE_DEBUG_NO_OPEN_TREE (1 << 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_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;
g_queue_push_tail (&path_st, g_memdup2 (&st, sizeof (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);
g_queue_push_tail (&path_st, g_memdup2 (&st, sizeof (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)
{
struct glnx_statx *lower_st;
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
{
g_queue_push_tail (&path_st, g_memdup2 (&st, sizeof (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);
}

44
glnx-chase.h Normal file
View File

@@ -0,0 +1,44 @@
/* -*- 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);
G_END_DECLS

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