some chaseat() love (#40825)

This commit is contained in:
Zbigniew Jędrzejewski-Szmek
2026-03-03 14:53:20 +01:00
committed by GitHub
15 changed files with 276 additions and 131 deletions
+152 -64
View File
@@ -31,15 +31,33 @@
CHASE_MUST_BE_REGULAR | \
CHASE_MUST_BE_SOCKET)
bool unsafe_transition(const struct stat *a, const struct stat *b) {
/* Returns true if the transition from a to b is safe, i.e. that we never transition from unprivileged to
* privileged files or directories. Why bother? So that unprivileged code can't symlink to privileged files
* making us believe we read something safe even though it isn't safe in the specific context we open it in. */
static bool uid_unsafe_transition(uid_t a, uid_t b) {
/* Returns true if the transition from a to b is safe, i.e. that we never transition from
* unprivileged to privileged files or directories. Why bother? So that unprivileged code can't
* symlink to privileged files making us believe we read something safe even though it isn't safe in
* the specific context we open it in. */
if (a->st_uid == 0) /* Transitioning from privileged to unprivileged is always fine */
if (a == 0) /* Transitioning from privileged to unprivileged is always fine */
return false;
return a->st_uid != b->st_uid; /* Otherwise we need to stay within the same UID */
return a != b; /* Otherwise we need to stay within the same UID */
}
int statx_unsafe_transition(const struct statx *a, const struct statx *b) {
assert(a);
assert(b);
if (!FLAGS_SET(a->stx_mask, STATX_UID) || !FLAGS_SET(b->stx_mask, STATX_UID))
return -ENODATA;
return uid_unsafe_transition(a->stx_uid, b->stx_uid);
}
bool stat_unsafe_transition(const struct stat *a, const struct stat *b) {
assert(a);
assert(b);
return uid_unsafe_transition(a->st_uid, b->st_uid);
}
static int log_unsafe_transition(int a, int b, const char *path, ChaseFlags flags) {
@@ -128,11 +146,11 @@ static int chaseat_needs_absolute(int dir_fd, const char *path) {
int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int *ret_fd) {
_cleanup_free_ char *buffer = NULL, *done = NULL;
_cleanup_close_ int fd = -EBADF, root_fd = -EBADF;
unsigned max_follow = CHASE_MAX; /* how many symlinks to follow before giving up and returning ELOOP */
bool exists = true, append_trail_slash = false;
struct stat st; /* stat obtained from fd */
struct statx root_stx, stx;
bool need_absolute = false; /* allocate early to avoid compiler warnings around goto */
const char *todo;
unsigned mask = STATX_TYPE|STATX_UID|STATX_INO;
int r;
assert(!FLAGS_SET(flags, CHASE_PREFIX_ROOT));
@@ -226,7 +244,6 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
if (r < 0)
return r;
if (r > 0) {
/* Shortcut the common case where no root dir is specified, and no special flags are given to
* a regular open() */
if (!ret_path &&
@@ -246,8 +263,16 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
if (fd < 0)
return -errno;
if (fstat(fd, &st) < 0)
return -errno;
r = xstatx_full(fd,
/* path= */ NULL,
/* statx_flags= */ 0,
XSTATX_MNT_ID_BEST,
mask,
/* optional_mask= */ 0,
/* mandatory_attributes= */ 0,
&stx);
if (r < 0)
return r;
exists = true;
goto success;
@@ -259,15 +284,6 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
dir_fd = _dir_fd;
flags &= ~CHASE_AT_RESOLVE_IN_ROOT;
} else if (FLAGS_SET(flags, CHASE_AT_RESOLVE_IN_ROOT)) {
/* If we get AT_FDCWD or dir_fd points to "/", then we always resolve symlinks relative to
* the host's root. Hence, CHASE_AT_RESOLVE_IN_ROOT is meaningless. */
r = dir_fd_is_root_or_cwd(dir_fd);
if (r < 0)
return r;
if (r > 0)
flags &= ~CHASE_AT_RESOLVE_IN_ROOT;
}
if (!ret_path && ret_fd && (flags & (CHASE_AT_RESOLVE_IN_ROOT|CHASE_NO_SHORTCUT_MASK)) == 0) {
@@ -307,8 +323,17 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
if (fd < 0)
return -errno;
if (fstat(fd, &st) < 0)
return -errno;
r = xstatx_full(fd,
/* path= */ NULL,
/* statx_flags= */ 0,
XSTATX_MNT_ID_BEST,
mask,
/* optional_mask= */ 0,
/* mandatory_attributes= */ 0,
&stx);
if (r < 0)
return r;
root_stx = stx; /* remember stat data of the root, so that we can recognize it later */
/* If we get AT_FDCWD, we always resolve symlinks relative to the host's root. Only if a positive
* directory file descriptor is provided we will look at CHASE_AT_RESOLVE_IN_ROOT to determine
@@ -334,12 +359,18 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
if (FLAGS_SET(flags, CHASE_MUST_BE_DIRECTORY) + FLAGS_SET(flags, CHASE_MUST_BE_REGULAR) + FLAGS_SET(flags, CHASE_MUST_BE_SOCKET) > 1)
return -EBADSLT;
for (todo = buffer;;) {
todo = buffer;
for (unsigned n_steps = 0;; n_steps++) {
_cleanup_free_ char *first = NULL;
_cleanup_close_ int child = -EBADF;
struct stat st_child;
struct statx stx_child;
const char *e;
/* If people change our tree behind our back, they might send us in circles. Put a limit on
* things */
if (n_steps > CHASE_MAX)
return -ELOOP;
r = path_find_first_component(&todo, /* accept_dot_dot= */ true, &e);
if (r < 0)
return r;
@@ -354,27 +385,49 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
if (streq(first, "..")) {
_cleanup_free_ char *parent = NULL;
_cleanup_close_ int fd_parent = -EBADF;
struct stat st_parent;
struct statx stx_parent;
/* If we already are at the top, then going up will not change anything. This is
* in-line with how the kernel handles this. */
if (empty_or_root(done) && FLAGS_SET(flags, CHASE_AT_RESOLVE_IN_ROOT)) {
if (FLAGS_SET(flags, CHASE_STEP))
goto chased_one;
continue;
* in-line with how the kernel handles this. We check this both by path and by
* inode/mount identity check. The latter is load-bearing if concurrent access of the
* root tree we operate in is allowed, where an inode is moved up the tree while we
* look at it, and thus get the current path wrong and think we are deeper down than
* we actually are. */
if (FLAGS_SET(flags, CHASE_AT_RESOLVE_IN_ROOT)) {
bool is_root = empty_or_root(done);
if (!is_root && statx_inode_same(&stx, &root_stx)) {
r = statx_mount_same(&stx, &root_stx);
if (r < 0)
return r;
is_root = r > 0;
}
if (is_root) {
if (FLAGS_SET(flags, CHASE_STEP))
goto chased_one;
continue;
}
}
fd_parent = openat(fd, "..", O_CLOEXEC|O_NOFOLLOW|O_PATH|O_DIRECTORY);
if (fd_parent < 0)
return -errno;
if (fstat(fd_parent, &st_parent) < 0)
return -errno;
r = xstatx_full(fd_parent,
/* path= */ NULL,
/* statx_flags= */ 0,
XSTATX_MNT_ID_BEST,
mask,
/* optional_mask= */ 0,
/* mandatory_attributes= */ 0,
&stx_parent);
if (r < 0)
return r;
/* If we opened the same directory, that _may_ indicate that we're at the host root
* directory. Let's confirm that in more detail with dir_fd_is_root(). And if so,
* going up won't change anything. */
if (stat_inode_same(&st_parent, &st)) {
if (statx_inode_same(&stx_parent, &stx)) {
r = dir_fd_is_root(fd);
if (r < 0)
return r;
@@ -414,35 +467,51 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
if (FLAGS_SET(flags, CHASE_STEP))
goto chased_one;
if (FLAGS_SET(flags, CHASE_SAFE) &&
unsafe_transition(&st, &st_parent))
return log_unsafe_transition(fd, fd_parent, path, flags);
if (FLAGS_SET(flags, CHASE_SAFE)) {
r = statx_unsafe_transition(&stx, &stx_parent);
if (r < 0)
return r;
if (r > 0)
return log_unsafe_transition(fd, fd_parent, path, flags);
}
/* If the path ends on a "..", and CHASE_PARENT is specified then our current 'fd' is
* the child of the returned normalized path, not the parent as requested. To correct
* this we have to go *two* levels up. */
if (FLAGS_SET(flags, CHASE_PARENT) && isempty(todo)) {
_cleanup_close_ int fd_grandparent = -EBADF;
struct stat st_grandparent;
struct statx stx_grandparent;
fd_grandparent = openat(fd_parent, "..", O_CLOEXEC|O_NOFOLLOW|O_PATH|O_DIRECTORY);
if (fd_grandparent < 0)
return -errno;
if (fstat(fd_grandparent, &st_grandparent) < 0)
return -errno;
r = xstatx_full(fd_grandparent,
/* path= */ NULL,
/* statx_flags= */ 0,
XSTATX_MNT_ID_BEST,
mask,
/* optional_mask= */ 0,
/* mandatory_attributes= */ 0,
&stx_grandparent);
if (r < 0)
return r;
if (FLAGS_SET(flags, CHASE_SAFE) &&
unsafe_transition(&st_parent, &st_grandparent))
return log_unsafe_transition(fd_parent, fd_grandparent, path, flags);
if (FLAGS_SET(flags, CHASE_SAFE)) {
r = statx_unsafe_transition(&stx_parent, &stx_grandparent);
if (r < 0)
return r;
if (r > 0)
return log_unsafe_transition(fd_parent, fd_grandparent, path, flags);
}
st = st_grandparent;
stx = stx_grandparent;
close_and_replace(fd, fd_grandparent);
break;
}
/* update fd and stat */
st = st_parent;
stx = stx_parent;
close_and_replace(fd, fd_parent);
continue;
}
@@ -478,28 +547,35 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
}
/* ... and then check what it actually is. */
if (fstat(child, &st_child) < 0)
return -errno;
r = xstatx_full(child,
/* path= */ NULL,
/* statx_flags= */ 0,
XSTATX_MNT_ID_BEST,
mask,
/* optional_mask= */ 0,
/* mandatory_attributes= */ 0,
&stx_child);
if (r < 0)
return r;
if (FLAGS_SET(flags, CHASE_SAFE) &&
unsafe_transition(&st, &st_child))
return log_unsafe_transition(fd, child, path, flags);
if (FLAGS_SET(flags, CHASE_SAFE)) {
r = statx_unsafe_transition(&stx, &stx_child);
if (r < 0)
return r;
if (r > 0)
return log_unsafe_transition(fd, child, path, flags);
}
if (FLAGS_SET(flags, CHASE_NO_AUTOFS) &&
fd_is_fs_type(child, AUTOFS_SUPER_MAGIC) > 0)
return log_autofs_mount_point(child, path, flags);
if (S_ISLNK(st_child.st_mode) && !(FLAGS_SET(flags, CHASE_NOFOLLOW) && isempty(todo))) {
if (S_ISLNK(stx_child.stx_mode) && !(FLAGS_SET(flags, CHASE_NOFOLLOW) && isempty(todo))) {
_cleanup_free_ char *destination = NULL;
if (FLAGS_SET(flags, CHASE_PROHIBIT_SYMLINKS))
return log_prohibited_symlink(child, flags);
/* This is a symlink, in this case read the destination. But let's make sure we
* don't follow symlinks without bounds. */
if (--max_follow <= 0)
return -ELOOP;
r = readlinkat_malloc(fd, first, &destination);
if (r < 0)
return r;
@@ -516,12 +592,24 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
if (fd < 0)
return fd;
if (fstat(fd, &st) < 0)
return -errno;
r = xstatx_full(fd,
/* path= */ NULL,
/* statx_flags= */ 0,
XSTATX_MNT_ID_BEST,
mask,
/* optional_mask= */ 0,
/* mandatory_attributes= */ 0,
&stx);
if (r < 0)
return r;
if (FLAGS_SET(flags, CHASE_SAFE) &&
unsafe_transition(&st_child, &st))
return log_unsafe_transition(child, fd, path, flags);
if (FLAGS_SET(flags, CHASE_SAFE)) {
r = statx_unsafe_transition(&stx_child, &stx);
if (r < 0)
return r;
if (r > 0)
return log_unsafe_transition(child, fd, path, flags);
}
/* When CHASE_AT_RESOLVE_IN_ROOT is not set, now the chased path may be
* outside of the specified dir_fd. Let's make the result absolute. */
@@ -555,26 +643,26 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
break;
/* And iterate again, but go one directory further down. */
st = st_child;
stx = stx_child;
close_and_replace(fd, child);
}
success:
if (exists) {
if (FLAGS_SET(flags, CHASE_MUST_BE_DIRECTORY)) {
r = stat_verify_directory(&st);
r = statx_verify_directory(&stx);
if (r < 0)
return r;
}
if (FLAGS_SET(flags, CHASE_MUST_BE_REGULAR)) {
r = stat_verify_regular(&st);
r = statx_verify_regular(&stx);
if (r < 0)
return r;
}
if (FLAGS_SET(flags, CHASE_MUST_BE_SOCKET)) {
r = stat_verify_socket(&st);
r = statx_verify_socket(&stx);
if (r < 0)
return r;
}
+3 -2
View File
@@ -32,10 +32,11 @@ typedef enum ChaseFlags {
CHASE_MUST_BE_SOCKET = 1 << 16, /* Fail if returned inode fd is not a socket */
} ChaseFlags;
bool unsafe_transition(const struct stat *a, const struct stat *b);
int statx_unsafe_transition(const struct statx *a, const struct statx *b);
bool stat_unsafe_transition(const struct stat *a, const struct stat *b);
/* How many iterations to execute before returning -ELOOP */
#define CHASE_MAX 32
#define CHASE_MAX 128U
int chase(const char *path_with_prefix, const char *root, ChaseFlags flags, char **ret_path, int *ret_fd);
+1
View File
@@ -27,6 +27,7 @@ int dirent_ensure_type(int dir_fd, struct dirent *de) {
r = xstatx_full(dir_fd,
de->d_name,
AT_SYMLINK_NOFOLLOW|AT_NO_AUTOMOUNT,
/* xstatx_flags= */ 0,
/* mandatory_mask= */ STATX_TYPE,
/* optional_mask= */ STATX_INO,
/* mandatory_attributes= */ 0,
+10 -7
View File
@@ -1061,10 +1061,6 @@ int path_is_root_at(int dir_fd, const char *path) {
dir_fd = fd;
}
_cleanup_close_ int root_fd = open("/", O_PATH|O_DIRECTORY|O_CLOEXEC);
if (root_fd < 0)
return -errno;
/* Even if the root directory has the same inode as our fd, the fd may not point to the root
* directory "/", and we also need to check that the mount ids are the same. Otherwise, a construct
* like the following could be used to trick us:
@@ -1073,10 +1069,10 @@ int path_is_root_at(int dir_fd, const char *path) {
* $ mount --bind / /tmp/x
*/
return fds_are_same_mount(dir_fd, root_fd);
return fds_inode_and_mount_same(dir_fd, XAT_FDROOT);
}
int fds_are_same_mount(int fd1, int fd2) {
int fds_inode_and_mount_same(int fd1, int fd2) {
struct statx sx1, sx2;
int r;
@@ -1089,13 +1085,20 @@ int fds_are_same_mount(int fd1, int fd2) {
if (r < 0)
return r;
if (fd1 == fd2) /* Shortcut things if fds are the same (only after validating the fd) */
return true;
r = xstatx(fd2, /* path = */ NULL, AT_EMPTY_PATH,
STATX_TYPE|STATX_INO|STATX_MNT_ID,
&sx2);
if (r < 0)
return r;
return statx_inode_same(&sx1, &sx2) && statx_mount_same(&sx1, &sx2);
r = statx_mount_same(&sx1, &sx2);
if (r <= 0)
return r;
return statx_inode_same(&sx1, &sx2);
}
int resolve_xat_fdroot(int *fd, const char **path, char **ret_buffer) {
+1 -1
View File
@@ -179,7 +179,7 @@ static inline int dir_fd_is_root_or_cwd(int dir_fd) {
return IN_SET(dir_fd, AT_FDCWD, XAT_FDROOT) ? true : path_is_root_at(dir_fd, NULL);
}
int fds_are_same_mount(int fd1, int fd2);
int fds_inode_and_mount_same(int fd1, int fd2);
int resolve_xat_fdroot(int *fd, const char **path, char **ret_buffer);
+1
View File
@@ -250,6 +250,7 @@ int is_mount_point_at(int dir_fd, const char *path, int flags) {
at_flags_normalize_nofollow(flags) |
AT_NO_AUTOMOUNT | /* don't trigger automounts – mounts are a local concept, hence no need to trigger automounts to determine STATX_ATTR_MOUNT_ROOT */
AT_STATX_DONT_SYNC, /* don't go to the network for this – for similar reasons */
/* xstatx_flags = */ 0,
STATX_TYPE|STATX_INO,
/* optional_mask = */ 0,
STATX_ATTR_MOUNT_ROOT,
+68 -33
View File
@@ -49,22 +49,35 @@ static int verify_stat_at(
return verify ? r : r >= 0;
}
static int mode_verify_regular(mode_t mode) {
if (S_ISDIR(mode))
return -EISDIR;
if (S_ISLNK(mode))
return -ELOOP;
if (!S_ISREG(mode))
return -EBADFD;
return 0;
}
int stat_verify_regular(const struct stat *st) {
assert(st);
/* Checks whether the specified stat() structure refers to a regular file. If not returns an
* appropriate error code. */
if (S_ISDIR(st->st_mode))
return -EISDIR;
return mode_verify_regular(st->st_mode);
}
if (S_ISLNK(st->st_mode))
return -ELOOP;
int statx_verify_regular(const struct statx *stx) {
assert(stx);
if (!S_ISREG(st->st_mode))
return -EBADFD;
if (!FLAGS_SET(stx->stx_mask, STATX_TYPE))
return -ENODATA;
return 0;
return mode_verify_regular(stx->stx_mode);
}
int verify_regular_at(int fd, const char *path, bool follow) {
@@ -78,31 +91,29 @@ int fd_verify_regular(int fd) {
return verify_regular_at(fd, /* path= */ NULL, /* follow= */ false);
}
int stat_verify_directory(const struct stat *st) {
assert(st);
if (S_ISLNK(st->st_mode))
static int mode_verify_directory(mode_t mode) {
if (S_ISLNK(mode))
return -ELOOP;
if (!S_ISDIR(st->st_mode))
if (!S_ISDIR(mode))
return -ENOTDIR;
return 0;
}
int stat_verify_directory(const struct stat *st) {
assert(st);
return mode_verify_directory(st->st_mode);
}
int statx_verify_directory(const struct statx *stx) {
assert(stx);
if (!FLAGS_SET(stx->stx_mask, STATX_TYPE))
return -ENODATA;
if (S_ISLNK(stx->stx_mode))
return -ELOOP;
if (!S_ISDIR(stx->stx_mode))
return -ENOTDIR;
return 0;
return mode_verify_directory(stx->stx_mode);
}
int fd_verify_directory(int fd) {
@@ -142,21 +153,31 @@ int is_symlink(const char *path) {
return verify_stat_at(AT_FDCWD, path, false, stat_verify_symlink, false);
}
int stat_verify_socket(const struct stat *st) {
assert(st);
if (S_ISLNK(st->st_mode))
static mode_t mode_verify_socket(mode_t mode) {
if (S_ISLNK(mode))
return -ELOOP;
if (S_ISDIR(st->st_mode))
if (S_ISDIR(mode))
return -EISDIR;
if (!S_ISSOCK(st->st_mode))
if (!S_ISSOCK(mode))
return -ENOTSOCK;
return 0;
}
int stat_verify_socket(const struct stat *st) {
assert(st);
return mode_verify_socket(st->st_mode);
}
int statx_verify_socket(const struct statx *stx) {
assert(stx);
return mode_verify_socket(stx->stx_mode);
}
int is_socket(const char *path) {
assert(!isempty(path));
return verify_stat_at(AT_FDCWD, path, /* follow= */ true, stat_verify_socket, /* verify= */ false);
@@ -307,7 +328,8 @@ DEFINE_STATX_BITS_TO_STRING(statx_attributes, uint64_t, statx_attribute_to_name,
int xstatx_full(int fd,
const char *path,
int flags,
int statx_flags,
XStatXFlags xstatx_flags,
unsigned mandatory_mask,
unsigned optional_mask,
uint64_t mandatory_attributes,
@@ -323,10 +345,13 @@ int xstatx_full(int fd,
* 3. Takes separate mandatory and optional mask params, plus mandatory attributes.
* Returns -EUNATCH if statx() does not return all masks specified as mandatory,
* > 0 if all optional masks are supported, 0 otherwise.
* 4. Supports a new flag XSTATX_MNT_ID_BEST which acquires STATX_MNT_ID_UNIQUE if available and
* STATX_MNT_ID if not.
*/
assert(fd >= 0 || IN_SET(fd, AT_FDCWD, XAT_FDROOT));
assert((mandatory_mask & optional_mask) == 0);
assert(!FLAGS_SET(xstatx_flags, XSTATX_MNT_ID_BEST) || !((mandatory_mask|optional_mask) & (STATX_MNT_ID|STATX_MNT_ID_UNIQUE)));
assert(ret);
_cleanup_free_ char *p = NULL;
@@ -334,12 +359,21 @@ int xstatx_full(int fd,
if (r < 0)
return r;
if (statx(fd, strempty(path),
flags|(isempty(path) ? AT_EMPTY_PATH : 0),
mandatory_mask|optional_mask,
unsigned request_mask = mandatory_mask|optional_mask;
if (FLAGS_SET(xstatx_flags, XSTATX_MNT_ID_BEST))
request_mask |= STATX_MNT_ID|STATX_MNT_ID_UNIQUE;
if (statx(fd,
strempty(path),
statx_flags|(isempty(path) ? AT_EMPTY_PATH : 0),
request_mask,
&sx) < 0)
return negative_errno();
if (FLAGS_SET(xstatx_flags, XSTATX_MNT_ID_BEST) &&
!(sx.stx_mask & (STATX_MNT_ID|STATX_MNT_ID_UNIQUE)))
return log_debug_errno(SYNTHETIC_ERRNO(EUNATCH), "statx() did not return either STATX_MNT_ID or STATX_MNT_ID_UNIQUE.");
if (!FLAGS_SET(sx.stx_mask, mandatory_mask)) {
if (DEBUG_LOGGING) {
_cleanup_free_ char *mask_str = statx_mask_to_string(mandatory_mask & ~sx.stx_mask);
@@ -654,14 +688,15 @@ bool statx_inode_same(const struct statx *a, const struct statx *b) {
a->stx_ino == b->stx_ino;
}
bool statx_mount_same(const struct statx *a, const struct statx *b) {
int statx_mount_same(const struct statx *a, const struct statx *b) {
if (!statx_is_set(a) || !statx_is_set(b))
return false;
assert(FLAGS_SET(a->stx_mask, STATX_MNT_ID));
assert(FLAGS_SET(b->stx_mask, STATX_MNT_ID));
if ((FLAGS_SET(a->stx_mask, STATX_MNT_ID) && FLAGS_SET(b->stx_mask, STATX_MNT_ID)) ||
(FLAGS_SET(a->stx_mask, STATX_MNT_ID_UNIQUE) && FLAGS_SET(b->stx_mask, STATX_MNT_ID_UNIQUE)))
return a->stx_mnt_id == b->stx_mnt_id;
return a->stx_mnt_id == b->stx_mnt_id;
return -ENODATA;
}
usec_t statx_timestamp_load(const struct statx_timestamp *ts) {
+11 -4
View File
@@ -7,6 +7,7 @@
#include "basic-forward.h"
int stat_verify_regular(const struct stat *st);
int statx_verify_regular(const struct statx *stx);
int verify_regular_at(int fd, const char *path, bool follow);
int fd_verify_regular(int fd);
@@ -21,6 +22,7 @@ int fd_verify_symlink(int fd);
int is_symlink(const char *path);
int stat_verify_socket(const struct stat *st);
int statx_verify_socket(const struct statx *stx);
int is_socket(const char *path);
int stat_verify_linked(const struct stat *st);
@@ -45,9 +47,14 @@ static inline int null_or_empty_path(const char *fn) {
return null_or_empty_path_with_root(fn, NULL);
}
typedef enum XStatXFlags {
XSTATX_MNT_ID_BEST = 1 << 0, /* Like STATX_MNT_ID_UNIQUE if available, STATX_MNT_ID otherwise */
} XStatXFlags;
int xstatx_full(int fd,
const char *path,
int flags,
int statx_flags,
XStatXFlags xstatx_flags,
unsigned mandatory_mask,
unsigned optional_mask,
uint64_t mandatory_attributes,
@@ -56,11 +63,11 @@ int xstatx_full(int fd,
static inline int xstatx(
int fd,
const char *path,
int flags,
int statx_flags,
unsigned mandatory_mask,
struct statx *ret) {
return xstatx_full(fd, path, flags, mandatory_mask, 0, 0, ret);
return xstatx_full(fd, path, statx_flags, 0, mandatory_mask, 0, 0, ret);
}
int fd_is_read_only_fs(int fd);
@@ -108,7 +115,7 @@ bool stat_inode_same(const struct stat *a, const struct stat *b);
bool stat_inode_unmodified(const struct stat *a, const struct stat *b);
bool statx_inode_same(const struct statx *a, const struct statx *b);
bool statx_mount_same(const struct statx *a, const struct statx *b);
int statx_mount_same(const struct statx *a, const struct statx *b);
int xstatfsat(int dir_fd, const char *path, struct statfs *ret);
+5 -3
View File
@@ -429,11 +429,13 @@ int getcrtime_at(
* concept is useful for determining how "old" a file really is, and hence using the older of the two makes
* most sense. */
r = xstatx_full(fd, path,
r = xstatx_full(fd,
path,
at_flags_normalize_nofollow(at_flags)|AT_STATX_DONT_SYNC,
/* mandatory_mask = */ 0,
/* xstatx_flags= */ 0,
/* mandatory_mask= */ 0,
STATX_BTIME,
/* mandatory_attributes = */ 0,
/* mandatory_attributes= */ 0,
&sx);
if (r > 0 && sx.stx_btime.tv_sec != 0) /* > 0: all optional masks are supported */
a = statx_timestamp_load(&sx.stx_btime);
+8 -3
View File
@@ -866,7 +866,8 @@ static DirectoryOwnership validate_directory_fd(
r = xstatx_full(fd,
/* path= */ NULL,
AT_EMPTY_PATH,
/* mandatory_mask= */ STATX_TYPE|STATX_UID|STATX_MNT_ID|STATX_INO,
/* xstatx_flags= */ XSTATX_MNT_ID_BEST,
/* mandatory_mask= */ STATX_TYPE|STATX_UID|STATX_INO,
/* optional_mask= */ 0,
/* mandatory_attributes= */ STATX_ATTR_MOUNT_ROOT,
&stx);
@@ -933,7 +934,8 @@ static DirectoryOwnership validate_directory_fd(
r = xstatx_full(new_parent_fd,
/* path= */ NULL,
AT_EMPTY_PATH,
/* mandatory_mask= */ STATX_UID|STATX_MNT_ID|STATX_INO,
/* xstatx_flags= */ XSTATX_MNT_ID_BEST,
/* mandatory_mask= */ STATX_UID|STATX_INO,
/* optional_mask= */ 0,
/* mandatory_attributes= */ STATX_ATTR_MOUNT_ROOT,
&new_stx);
@@ -947,7 +949,10 @@ static DirectoryOwnership validate_directory_fd(
return DIRECTORY_IS_OTHERWISE_OWNED;
}
if (stx.stx_mnt_id != new_stx.stx_mnt_id) {
r = statx_mount_same(&stx, &new_stx);
if (r < 0)
return log_debug_errno(r, "Failed to compare mount IDs: %m");
if (!r) {
/* NB, this check is probably redundant, given we also check
* STATX_ATTR_MOUNT_ROOT. The only reason we have it here is to provide extra safety
* in case the mount tree is rearranged concurrently with our traversal, so that
+1
View File
@@ -287,6 +287,7 @@ static int verify_fsroot_dir(
r = xstatx_full(dir_fd, f,
AT_SYMLINK_NOFOLLOW,
/* xstatx_flags= */ 0,
STATX_TYPE|STATX_INO,
/* optional_mask = */ 0,
STATX_ATTR_MOUNT_ROOT,
+1 -1
View File
@@ -54,7 +54,7 @@ int switch_root(const char *new_root,
if (new_root_fd < 0)
return log_error_errno(errno, "Failed to open target directory '%s': %m", new_root);
r = fds_are_same_mount(old_root_fd, new_root_fd); /* checks if referenced inodes and mounts match */
r = fds_inode_and_mount_same(old_root_fd, new_root_fd); /* checks if referenced inodes and mounts match */
if (r < 0)
return log_error_errno(r, "Failed to check if old and new root directory/mount are the same: %m");
if (r > 0) {
+3 -6
View File
@@ -738,7 +738,7 @@ TEST(path_is_root_at) {
test_path_is_root_at_one(true);
}
TEST(fds_are_same_mount) {
TEST(fds_inode_and_mount_same) {
_cleanup_close_ int fd1 = -EBADF, fd2 = -EBADF, fd3 = -EBADF, fd4 = -EBADF;
fd1 = open("/sys", O_CLOEXEC|O_PATH|O_DIRECTORY|O_NOFOLLOW);
@@ -749,11 +749,8 @@ TEST(fds_are_same_mount) {
if (fd1 < 0 || fd2 < 0 || fd3 < 0 || fd4 < 0)
return (void) log_tests_skipped_errno(errno, "Failed to open /sys or /proc or /");
if (fds_are_same_mount(fd1, fd4) > 0 && fds_are_same_mount(fd2, fd4) > 0)
return (void) log_tests_skipped("Cannot test fds_are_same_mount() as /sys and /proc are not mounted");
assert_se(fds_are_same_mount(fd1, fd2) == 0);
assert_se(fds_are_same_mount(fd2, fd3) > 0);
assert_se(fds_inode_and_mount_same(fd1, fd2) == 0);
assert_se(fds_inode_and_mount_same(fd2, fd3) > 0);
}
TEST(fd_get_path) {
+4 -4
View File
@@ -871,10 +871,10 @@ TEST(xat_fdroot) {
ASSERT_OK_POSITIVE(path_is_root_at(fd, "."));
ASSERT_OK_POSITIVE(path_is_root_at(fd, "/"));
ASSERT_OK_POSITIVE(fds_are_same_mount(fd, fd));
ASSERT_OK_POSITIVE(fds_are_same_mount(XAT_FDROOT, XAT_FDROOT));
ASSERT_OK_POSITIVE(fds_are_same_mount(fd, XAT_FDROOT));
ASSERT_OK_POSITIVE(fds_are_same_mount(XAT_FDROOT, fd));
ASSERT_OK_POSITIVE(fds_inode_and_mount_same(fd, fd));
ASSERT_OK_POSITIVE(fds_inode_and_mount_same(XAT_FDROOT, XAT_FDROOT));
ASSERT_OK_POSITIVE(fds_inode_and_mount_same(fd, XAT_FDROOT));
ASSERT_OK_POSITIVE(fds_inode_and_mount_same(XAT_FDROOT, fd));
ASSERT_OK_POSITIVE(dir_fd_is_root(XAT_FDROOT));
ASSERT_OK_POSITIVE(dir_fd_is_root(fd));
+7 -3
View File
@@ -586,9 +586,12 @@ static int opendir_and_stat(
return 0;
}
r = xstatx_full(dirfd(d), /* path = */ NULL, AT_EMPTY_PATH,
r = xstatx_full(dirfd(d),
/* path= */ NULL,
AT_EMPTY_PATH,
/* xstatx_flags= */ 0,
STATX_MODE|STATX_INO|STATX_ATIME|STATX_MTIME,
/* optional_mask = */ 0,
/* optional_mask= */ 0,
STATX_ATTR_MOUNT_ROOT,
&sx);
if (r < 0)
@@ -687,6 +690,7 @@ static int dir_cleanup(
struct statx sx;
r = xstatx_full(dirfd(d), de->d_name,
AT_SYMLINK_NOFOLLOW|AT_NO_AUTOMOUNT,
/* xstatx_flags= */ 0,
STATX_TYPE|STATX_MODE|STATX_UID,
STATX_ATIME|STATX_MTIME|STATX_CTIME|STATX_BTIME,
STATX_ATTR_MOUNT_ROOT,
@@ -2633,7 +2637,7 @@ static int rm_if_wrong_type_safe(
}
/* Fail before removing anything if this is an unsafe transition. */
if (follow_links && unsafe_transition(parent_st, &st)) {
if (follow_links && stat_unsafe_transition(parent_st, &st)) {
(void) fd_get_path(parent_fd, &parent_name);
return log_error_errno(SYNTHETIC_ERRNO(ENOLINK),
"Unsafe transition from \"%s\" to \"%s\".", parent_name ?: "...", name);