]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
chase: add explicit root_fd parameter to chaseat() and drop CHASE_AT_RESOLVE_IN_ROOT
authorDaan De Meyer <daan@amutable.com>
Wed, 15 Apr 2026 22:51:46 +0000 (22:51 +0000)
committerDaan De Meyer <daan@amutable.com>
Mon, 20 Apr 2026 21:35:31 +0000 (23:35 +0200)
Split the single directory fd that chaseat() used to take into two separate
fds: a root_fd that sets the chroot boundary (symlinks may not escape it,
absolute symlinks resolve relative to it), and a dir_fd that path resolution
starts from. This makes the chroot semantics of chaseat() explicit at every
call site instead of encoding them in the CHASE_AT_RESOLVE_IN_ROOT flag,
which is removed. It also decouples the starting directory from the root
boundary, so callers can descend from any inode inside the tree without
having to reopen the root separately.

XAT_FDROOT passed as root_fd means "no containment" (host root); as dir_fd
it means "start at root_fd". For a smoother transition, AT_FDCWD is also
accepted as root_fd and treated as XAT_FDROOT. When root_fd points to a
directory that is actually the host root, it is normalized to XAT_FDROOT
up front so the existing shortcut path can kick in.

Absolute paths returned by chaseat() are now relative to root_fd, and
relative paths are relative to dir_fd. The result is absolute only when
there is no chroot boundary (root_fd is XAT_FDROOT), or when an absolute
symlink made resolution jump out of the dir_fd subtree; otherwise callers
get a relative path they can feed straight back into an openat()-style
call against dir_fd. Specifically, when dir_fd == root_fd and we're not
operating on the host's root directory, we return a relative path even if
we received an absolute path or resolved an absolute symlink to allow
passing the path directly to openat() style functions. We do this to not
have to go modify every caller of chaseat() to make sure they deal properly
with any absolute paths they might receive. Only when root_fd != dir_fd do
we have to return an absolute path to indicate that the path is relative to
root_fd and not dir_fd.

The shortcut that skips the per-component walk is reworked around a new
chase_xopenat() helper that funnels CHASE_NOFOLLOW, CHASE_MUST_BE_* and
CHASE_TRIGGER_AUTOFS through xopenat_full()'s O_NOFOLLOW, O_DIRECTORY,
XO_REGULAR, XO_SOCKET and XO_TRIGGER_AUTOMOUNT flags. As a result these
flags no longer force us off the shortcut and can be dropped from
CHASE_NO_SHORTCUT_MASK, and the old openat_opath_with_automount() helper
goes away. A CHASE_MUST_BE_ANY alias is introduced for shortcut callers
(stat/access paths) that don't go through xopenat_full() and still need
to bail on those flags locally.

All *_and_* helpers built on top of chaseat() (chase_and_openat,
chase_and_opendirat, chase_and_statat, chase_and_accessat,
chase_and_fopenat_unlocked, chase_and_unlinkat, chase_and_open_parent_at)
gain the same root_fd parameter, and every call site in the tree is
ported to the new signature.

chase_and_open() is also fixed to correctly handle CHASE_EXTRACT_FILENAME
without CHASE_PARENT.

28 files changed:
src/basic/chase.c
src/basic/chase.h
src/basic/conf-files.c
src/basic/fileio.c
src/basic/mkdir.c
src/basic/os-util.c
src/basic/user-util.c
src/bootctl/bootctl-install.c
src/core/exec-invoke.c
src/core/service.c
src/firstboot/firstboot.c
src/kernel-install/kernel-install.c
src/libsystemd/sd-id128/sd-id128.c
src/mountfsd/mountwork.c
src/portable/portable.c
src/shared/boot-entry.c
src/shared/bootspec.c
src/shared/btrfs-util.c
src/shared/conf-parser.c
src/shared/discover-image.c
src/shared/find-esp.c
src/shared/mstack.c
src/shared/tar-util.c
src/shared/tests.h
src/shared/vpick.c
src/sysext/sysext.c
src/test/test-chase.c
src/tpm2-setup/tpm2-swtpm.c

index 5abb4bc4307bb2d67f4976a8211c2b4cf0e6d312..50ab4e3c47495ccd0e38b3401cc0d3c61c90ecaa 100644 (file)
@@ -1,7 +1,6 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
 #include <linux/magic.h>
-#include <sys/mount.h>
 #include <unistd.h>
 
 #include "alloc-util.h"
 #include "strv.h"
 #include "user-util.h"
 
+/* Flags that prevent us from taking any of the early shortcuts: either they change the path resolution
+ * semantics (e.g. CHASE_NONEXISTENT, CHASE_PARENT, CHASE_STEP) or ask for per-component validation that a
+ * single open() cannot provide (e.g. CHASE_SAFE, CHASE_NO_AUTOFS, CHASE_PROHIBIT_SYMLINKS).
+ *
+ * Notably, the following are *not* listed here:
+ *   - CHASE_TRIGGER_AUTOFS: plain open() already triggers automounts, and O_PATH shortcuts can use
+ *     XO_TRIGGER_AUTOMOUNT to tell xopenat_full() to use open_tree() instead.
+ *   - CHASE_MUST_BE_{DIRECTORY,REGULAR,SOCKET}: xopenat_full() can enforce these via O_DIRECTORY,
+ *     XO_REGULAR and XO_SOCKET. Shortcut callers that don't go through xopenat_full() (stat/access
+ *     paths) must include CHASE_MUST_BE_ANY in their local mask to still bail on these. */
 #define CHASE_NO_SHORTCUT_MASK                          \
         (CHASE_NONEXISTENT |                            \
          CHASE_NO_AUTOFS |                              \
-         CHASE_TRIGGER_AUTOFS |                         \
          CHASE_SAFE |                                   \
          CHASE_STEP |                                   \
          CHASE_PROHIBIT_SYMLINKS |                      \
          CHASE_PARENT |                                 \
-         CHASE_MKDIR_0755 |                             \
-         CHASE_MUST_BE_DIRECTORY |                      \
-         CHASE_MUST_BE_REGULAR |                        \
-         CHASE_MUST_BE_SOCKET)
+         CHASE_MKDIR_0755)
+
+#define CHASE_MUST_BE_ANY \
+        (CHASE_MUST_BE_DIRECTORY|CHASE_MUST_BE_REGULAR|CHASE_MUST_BE_SOCKET)
+
+static int chase_statx(int fd, struct statx *ret) {
+        return xstatx_full(fd,
+                        /* path= */ NULL,
+                        /* statx_flags= */ 0,
+                        XSTATX_MNT_ID_BEST,
+                        STATX_TYPE|STATX_UID|STATX_INO,
+                        /* optional_mask= */ 0,
+                        /* mandatory_attributes= */ 0,
+                        ret);
+}
+
+static int chase_xopenat(int dir_fd, const char *path, ChaseFlags chase_flags, int open_flags, XOpenFlags xopen_flags) {
+        /* Wrapper around xopenat_full() that translates CHASE_NOFOLLOW, CHASE_MUST_BE_* and
+         * CHASE_TRIGGER_AUTOFS into their xopenat_full() counterparts. Used by shortcuts that want to open
+         * the final target of a chase operation: they all want O_NOFOLLOW honoured, MUST_BE_* verified on
+         * the opened inode, and automounts triggered if requested. */
+
+        if (FLAGS_SET(chase_flags, CHASE_NOFOLLOW))
+                open_flags |= O_NOFOLLOW;
+        if (FLAGS_SET(chase_flags, CHASE_MUST_BE_DIRECTORY))
+                open_flags |= O_DIRECTORY;
+        if (FLAGS_SET(chase_flags, CHASE_MUST_BE_REGULAR))
+                xopen_flags |= XO_REGULAR;
+        if (FLAGS_SET(chase_flags, CHASE_MUST_BE_SOCKET))
+                xopen_flags |= XO_SOCKET;
+        /* Only needed for O_PATH since plain open() already triggers automounts */
+        if (FLAGS_SET(chase_flags, CHASE_TRIGGER_AUTOFS) && FLAGS_SET(open_flags, O_PATH))
+                xopen_flags |= XO_TRIGGER_AUTOMOUNT;
+
+        return xopenat_full(dir_fd, path, open_flags, xopen_flags, MODE_INVALID);
+}
 
 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
@@ -108,95 +148,40 @@ static int log_prohibited_symlink(int fd, ChaseFlags flags) {
                                  strna(n1));
 }
 
-static int openat_opath_with_automount(int dir_fd, const char *path, bool automount) {
-        static bool can_open_tree = true;
-        int r;
-
-        /* Pin an inode via O_PATH semantics. Sounds pretty obvious to do this, right? You just do open()
-         * with O_PATH, and there you go. But uh, it's not that easy. open() via O_PATH does not trigger
-         * automounts, but we may want that when CHASE_TRIGGER_AUTOFS is set. But thankfully there's
-         * a way out: the newer open_tree() call, when specified without OPEN_TREE_CLONE actually is fully
-         * equivalent to open() with O_PATH – except for one thing: it triggers automounts.
-         *
-         * As it turns out some sandboxes prohibit open_tree(), and return EPERM or ENOSYS if we call it.
-         * But since autofs does not work inside of mount namespace anyway, let's simply handle this
-         * as gracefully as we can, and fall back to classic openat() if we see EPERM/ENOSYS. */
-
-        assert(dir_fd >= 0 || dir_fd == AT_FDCWD);
-        assert(path);
-
-        if (automount && can_open_tree) {
-                r = RET_NERRNO(open_tree(dir_fd, path, AT_SYMLINK_NOFOLLOW|OPEN_TREE_CLOEXEC));
-                if (r >= 0 || (r != -EPERM && !ERRNO_IS_NEG_NOT_SUPPORTED(r)))
-                        return r;
-
-                can_open_tree = false;
-        }
-
-        return RET_NERRNO(openat(dir_fd, path, O_PATH|O_NOFOLLOW|O_CLOEXEC));
-}
-
-static int chaseat_needs_absolute(int dir_fd, const char *path) {
-        if (dir_fd < 0)
-                return path_is_absolute(path);
-
-        return dir_fd_is_root(dir_fd);
-}
-
-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;
-        bool exists = true, append_trail_slash = false;
-        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 chaseat(int root_fd, int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int *ret_fd) {
         int r;
 
         assert(!FLAGS_SET(flags, CHASE_PREFIX_ROOT));
         assert(!FLAGS_SET(flags, CHASE_STEP|CHASE_EXTRACT_FILENAME));
         assert(!FLAGS_SET(flags, CHASE_NO_AUTOFS|CHASE_TRIGGER_AUTOFS));
         assert(dir_fd >= 0 || IN_SET(dir_fd, AT_FDCWD, XAT_FDROOT));
+        assert(root_fd >= 0 || IN_SET(root_fd, AT_FDCWD, XAT_FDROOT));
+        /* AT_FDCWD for dir_fd is only allowed when there is no chroot boundary: otherwise the current
+         * working directory might live outside root_fd's subtree. */
+        assert(dir_fd != AT_FDCWD || IN_SET(root_fd, AT_FDCWD, XAT_FDROOT));
 
         if (FLAGS_SET(flags, CHASE_STEP))
                 assert(!ret_fd);
 
-        if (isempty(path))
-                path = ".";
-
-        /* This function resolves symlinks of the path relative to the given directory file descriptor. If
-         * CHASE_AT_RESOLVE_IN_ROOT is specified and a directory file descriptor is provided, symlinks
-         * are resolved relative to the given directory file descriptor. Otherwise, they are resolved
-         * relative to the root directory of the host.
+        /* This function resolves symlinks of the path relative to the given directory file descriptor.
+         * The root directory file descriptor sets the chroot boundary: symlinks may not escape it, and
+         * absolute symlinks encountered during resolution are resolved relative to it. When the root fd is
+         * XAT_FDROOT, symlinks are resolved relative to the host's root directory with no containment.
          *
-         * Note that when a positive directory file descriptor is provided and CHASE_AT_RESOLVE_IN_ROOT is
-         * specified and we find an absolute symlink, it is resolved relative to given directory file
-         * descriptor and not the root of the host. Also, when following relative symlinks, this functions
-         * ensures they cannot be used to "escape" the given directory file descriptor. If a positive
-         * directory file descriptor is provided, the "path" parameter is always interpreted relative to the
-         * given directory file descriptor, even if it is absolute. If the given directory file descriptor is
-         * AT_FDCWD and "path" is absolute, it is interpreted relative to the root directory of the host.
+         * The given path is always resolved starting at dir_fd, regardless of whether it is absolute or
+         * relative. The leading slashes of an absolute path are ignored. The only exceptions are
+         * dir_fd == XAT_FDROOT (which starts resolution at root_fd) and dir_fd == AT_FDCWD with an absolute
+         * path (which starts resolution at "/" rather than the current working directory).
          *
-         * When "dir_fd" points to a non-root directory and CHASE_AT_RESOLVE_IN_ROOT is set, this function
-         * always returns a relative path in "ret_path", even if "path" is an absolute path, because openat()
-         * like functions generally ignore the directory fd if they are provided with an absolute path. When
-         * CHASE_AT_RESOLVE_IN_ROOT is not set, then this returns relative path to the specified file
-         * descriptor if all resolved symlinks are relative, otherwise absolute path will be returned. When
-         * "dir_fd" is AT_FDCWD and "path" is an absolute path, we return an absolute path in "ret_path"
-         * because otherwise, if the caller passes the returned relative path to another openat() like
-         * function, it would be resolved relative to the current working directory instead of to "/".
+         * Note that we do not verify that dir_fd actually points to a descendant of root_fd. If dir_fd
+         * lies outside the root_fd subtree, ".." traversal and absolute symlinks may still be clamped to
+         * root_fd, leading to surprising results. Callers must ensure the relationship themselves.
          *
-         * Summary about the result path:
-         * - "dir_fd" points to the root directory
-         *    → result will be absolute
-         * - "dir_fd" points to a non-root directory, and CHASE_AT_RESOLVE_IN_ROOT is set
-         *    → relative
-         * - "dir_fd" points to a non-root directory, and CHASE_AT_RESOLVE_IN_ROOT is not set
-         *    → relative when all resolved symlinks are relative, otherwise absolute
-         * - "dir_fd" is AT_FDCWD, and "path" is absolute
-         *    → absolute
-         * - "dir_fd" is AT_FDCWD, and "path" is relative
-         *    → relative when all resolved symlinks are relative, otherwise absolute
+         * Absolute paths returned by this function are relative to the given root file descriptor. Relative
+         * paths returned by this function are relative to the given directory file descriptor. The result is
+         * absolute when root_fd is XAT_FDROOT (i.e. there is no chroot boundary, so openat()-like callers
+         * need an absolute path to reach the host inode), or when an absolute symlink made us jump to a
+         * different subtree than the one dir_fd points into. Otherwise the result is relative.
          *
          * Algorithmically this operates on two path buffers: "done" are the components of the path we
          * already processed and resolved symlinks, "." and ".." of. "todo" are the components of the path we
@@ -204,11 +189,6 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
          * its special meaning each time. We always keep an O_PATH fd to the component we are currently
          * processing, thus keeping lookup races to a minimum.
          *
-         * Suggested usage: whenever you want to canonicalize a path, use this function. Pass the absolute
-         * path you got as-is: fully qualified and relative to your host's root. Optionally, specify the
-         * "dir_fd" parameter to tell this function what to do when encountering a symlink with an absolute
-         * path as directory: resolve it relative to the given directory file descriptor.
-         *
          * There are five ways to invoke this function:
          *
          * 1. Without CHASE_STEP or ret_fd: in this case the path is resolved and the normalized path is
@@ -239,127 +219,116 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
          *    the mount point is emitted. CHASE_WARN cannot be used in PID 1.
          */
 
-        _cleanup_close_ int _dir_fd = -EBADF;
+        /* We treat AT_FDCWD as XAT_FDROOT for a more seamless migration for all callers of chaseat() before
+         * it was reworked to support separate root_fd and dir_fd arguments. */
+        if (root_fd == AT_FDCWD)
+                root_fd = XAT_FDROOT;
+        else {
+                r = dir_fd_is_root(root_fd);
+                if (r < 0)
+                        return r;
+                if (r > 0)
+                        root_fd = XAT_FDROOT;
+        }
+
+        /* If dir_fd points to the host's root directory and there is no chroot boundary, normalize it
+         * to XAT_FDROOT so the shortcut path can kick in. */
         r = dir_fd_is_root(dir_fd);
         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 &&
-                    (flags & (CHASE_STEP|CHASE_NO_AUTOFS|CHASE_NONEXISTENT|CHASE_SAFE|CHASE_WARN|CHASE_PROHIBIT_SYMLINKS|CHASE_PARENT|CHASE_MKDIR_0755)) == 0) {
-                        _cleanup_free_ char *slash_path = NULL;
-
-                        if (!path_is_absolute(path)) {
-                                slash_path = strjoin("/", path);
-                                if (!slash_path)
-                                        return -ENOMEM;
-                        }
+        if (r > 0 && root_fd == XAT_FDROOT)
+                dir_fd = XAT_FDROOT;
 
-                        /* We use open_tree() rather than regular open() here, because it gives us direct
-                         * control over automount behaviour, and otherwise is equivalent to open() with
-                         * O_PATH */
-                        fd = open_tree(-EBADF, slash_path ?: path, OPEN_TREE_CLOEXEC|(FLAGS_SET(flags, CHASE_TRIGGER_AUTOFS) ? 0 : AT_NO_AUTOMOUNT));
-                        if (fd < 0)
-                                return -errno;
+        /* dir_fd == XAT_FDROOT means "start at root_fd". An absolute path is always resolved relative to
+         * root_fd, regardless of what dir_fd points to. */
+        if (dir_fd == XAT_FDROOT || path_is_absolute(path))
+                dir_fd = root_fd;
 
-                        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 (isempty(path))
+                path = ".";
 
-                        exists = true;
-                        goto success;
-                }
+        bool append_trail_slash = false;
+        if (ENDSWITH_SET(path, "/", "/.")) {
+                flags |= CHASE_MUST_BE_DIRECTORY;
+                if (FLAGS_SET(flags, CHASE_TRAIL_SLASH))
+                        append_trail_slash = true;
+        } else if (dot_or_dot_dot(path) || endswith(path, "/.."))
+                flags |= CHASE_MUST_BE_DIRECTORY;
 
-                _dir_fd = open("/", O_DIRECTORY|O_RDONLY|O_CLOEXEC);
-                if (_dir_fd < 0)
-                        return -errno;
+        if (FLAGS_SET(flags, CHASE_PARENT))
+                flags |= CHASE_MUST_BE_DIRECTORY;
 
-                dir_fd = _dir_fd;
-                flags &= ~CHASE_AT_RESOLVE_IN_ROOT;
-        }
+        /* If multiple flags are set now, fail immediately */
+        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;
+
+        if (root_fd == XAT_FDROOT && !ret_path && (flags & CHASE_NO_SHORTCUT_MASK) == 0) {
+                /* Shortcut the common case where we don't have a real root boundary and no fancy features
+                 * are requested: open the target directly via xopenat_full() which applies any MUST_BE_*
+                 * verification and automount triggering for us. */
 
-        if (!ret_path && ret_fd && (flags & (CHASE_AT_RESOLVE_IN_ROOT|CHASE_NO_SHORTCUT_MASK)) == 0) {
-                /* Shortcut the ret_fd case if the caller isn't interested in the actual path and has no root
-                 * set and doesn't care about any of the other special features we provide either. */
-                r = openat(dir_fd, path, O_PATH|O_CLOEXEC|(FLAGS_SET(flags, CHASE_NOFOLLOW) ? O_NOFOLLOW : 0));
+                r = chase_xopenat(dir_fd, path, flags, O_PATH|O_CLOEXEC, /* xopen_flags= */ 0);
                 if (r < 0)
-                        return -errno;
+                        return r;
 
-                *ret_fd = r;
-                return 0;
-        }
+                if (ret_fd)
+                        *ret_fd = r;
+                else
+                        safe_close(r);
 
-        buffer = strdup(path);
-        if (!buffer)
-                return -ENOMEM;
+                return 1;
+        }
 
-        /* If we receive an absolute path together with AT_FDCWD, we need to return an absolute path, because
-         * a relative path would be interpreted relative to the current working directory. Also, let's make
-         * the result absolute when the file descriptor of the root directory is specified. */
-        r = chaseat_needs_absolute(dir_fd, path);
-        if (r < 0)
-                return r;
+        /* Decide whether to return an absolute or relative path.
+         *
+         * We return an absolute path only when there is no chroot boundary (root_fd == XAT_FDROOT)
+         * and resolution starts from root — i.e. either dir_fd was XAT_FDROOT or path is absolute,
+         * both of which caused dir_fd = root_fd above. In every other case we return a relative
+         * path so the result keeps working when fed to an openat()-style call against dir_fd,
+         * which would ignore dir_fd if handed an absolute path.
+         *
+         * When root_fd != XAT_FDROOT and an absolute symlink later causes resolution to escape
+         * dir_fd, the loop below rebases onto root_fd and switches to an absolute result at that
+         * point — it is not handled here.
+         */
+        bool need_absolute = (root_fd == XAT_FDROOT || dir_fd != root_fd) && (dir_fd == XAT_FDROOT || path_is_absolute(path));
 
-        need_absolute = r;
+        _cleanup_free_ char *done = NULL;
         if (need_absolute) {
                 done = strdup("/");
                 if (!done)
                         return -ENOMEM;
         }
 
-        /* If a positive directory file descriptor is provided, always resolve the given path relative to it,
-         * regardless of whether it is absolute or not. If we get AT_FDCWD, follow regular openat()
-         * semantics, if the path is relative, resolve against the current working directory. Otherwise,
-         * resolve against root. */
-        fd = openat(dir_fd, done ?: ".", O_CLOEXEC|O_DIRECTORY|O_PATH);
+        _cleanup_close_ int fd = xopenat(dir_fd, NULL, O_CLOEXEC|O_DIRECTORY|O_PATH);
         if (fd < 0)
-                return -errno;
+                return fd;
 
-        r = xstatx_full(fd,
-                        /* path= */ NULL,
-                        /* statx_flags= */ 0,
-                        XSTATX_MNT_ID_BEST,
-                        mask,
-                        /* optional_mask= */ 0,
-                        /* mandatory_attributes= */ 0,
-                        &stx);
+        struct statx stx;
+        r = chase_statx(fd, &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
-         * whether to resolve symlinks in it or not. */
-        if (dir_fd >= 0 && FLAGS_SET(flags, CHASE_AT_RESOLVE_IN_ROOT))
-                root_fd = openat(dir_fd, ".", O_CLOEXEC|O_DIRECTORY|O_PATH);
-        else
-                root_fd = open("/", O_CLOEXEC|O_DIRECTORY|O_PATH);
-        if (root_fd < 0)
-                return -errno;
 
-        if (ENDSWITH_SET(buffer, "/", "/.")) {
-                flags |= CHASE_MUST_BE_DIRECTORY;
-                if (FLAGS_SET(flags, CHASE_TRAIL_SLASH))
-                        append_trail_slash = true;
-        } else if (dot_or_dot_dot(buffer) || endswith(buffer, "/.."))
-                flags |= CHASE_MUST_BE_DIRECTORY;
-
-        if (FLAGS_SET(flags, CHASE_PARENT))
-                flags |= CHASE_MUST_BE_DIRECTORY;
+        /* Remember stat data of the root, so that we can recognize it later during .. handling. Only
+         * needed when there is an actual chroot boundary — with root_fd == XAT_FDROOT the boundary
+         * check in the .. loop below is skipped and root_stx is never consulted. */
+        struct statx root_stx;
+        if (root_fd != XAT_FDROOT) {
+                if (root_fd == dir_fd)
+                        root_stx = stx;
+                else {
+                        r = chase_statx(root_fd, &root_stx);
+                        if (r < 0)
+                                return r;
+                }
+        }
 
-        /* If multiple flags are set now, fail immediately */
-        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;
+        _cleanup_free_ char *buffer = strdup(path);
+        if (!buffer)
+                return -ENOMEM;
 
-        todo = buffer;
+        const char *todo = buffer;
+        bool exists = true;
         for (unsigned n_steps = 0;; n_steps++) {
                 _cleanup_free_ char *first = NULL;
                 _cleanup_close_ int child = -EBADF;
@@ -392,9 +361,13 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
                          * 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);
+                         * we actually are.
+                         *
+                         * The path-based fast path is only valid when the caller started at the root fd:
+                         * otherwise 'done' being empty just means we haven't descended past the starting
+                         * dir_fd, not that we're at the chroot boundary. */
+                        if (root_fd != XAT_FDROOT) {
+                                bool is_root = root_fd == dir_fd && empty_or_root(done);
                                 if (!is_root && statx_inode_same(&stx, &root_stx)) {
                                         r = statx_mount_same(&stx, &root_stx);
                                         if (r < 0)
@@ -413,14 +386,7 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
                         if (fd_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);
+                        r = chase_statx(fd_parent, &stx_parent);
                         if (r < 0)
                                 return r;
 
@@ -447,18 +413,19 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
                                 assert(!need_absolute);
                                 done = mfree(done);
                         } else if (r == -EADDRNOTAVAIL) {
-                                /* 'done' is "/". This branch should be already handled in the above. */
-                                assert(!FLAGS_SET(flags, CHASE_AT_RESOLVE_IN_ROOT));
+                                /* 'done' is "/". This branch should already be handled above via the
+                                 * is_root check. */
                                 assert_not_reached();
                         } else if (r == -EINVAL) {
-                                /* 'done' is an empty string, ends with '..', or an invalid path. */
+                                /* 'done' is empty (we haven't descended past the starting dir_fd yet), or
+                                 * ends with '..'. In both cases we're traversing above the starting point
+                                 * (valid when root_fd is XAT_FDROOT, or when dir_fd was below root_fd to
+                                 * start with), so record another '..' in 'done'. */
                                 assert(!need_absolute);
-                                assert(!FLAGS_SET(flags, CHASE_AT_RESOLVE_IN_ROOT));
 
-                                if (!path_is_valid(done))
+                                if (!isempty(done) && !path_is_valid(done))
                                         return -EINVAL;
 
-                                /* If we're at the top of "dir_fd", start appending ".." to "done". */
                                 if (!path_extend(&done, ".."))
                                         return -ENOMEM;
                         } else
@@ -486,14 +453,7 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
                                 if (fd_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);
+                                r = chase_statx(fd_grandparent, &stx_grandparent);
                                 if (r < 0)
                                         return r;
 
@@ -517,7 +477,10 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
                 }
 
                 /* Otherwise let's pin it by file descriptor, via O_PATH. */
-                child = r = openat_opath_with_automount(fd, first, /* automount= */ FLAGS_SET(flags, CHASE_TRIGGER_AUTOFS));
+                child = r = xopenat_full(fd, first,
+                                         O_PATH|O_NOFOLLOW|O_CLOEXEC,
+                                         FLAGS_SET(flags, CHASE_TRIGGER_AUTOFS) ? XO_TRIGGER_AUTOMOUNT : 0,
+                                         MODE_INVALID);
                 if (r < 0) {
                         if (r != -ENOENT)
                                 return r;
@@ -546,15 +509,7 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
                                 return r;
                 }
 
-                /* ... and then check what it actually is. */
-                r = xstatx_full(child,
-                                /* path= */ NULL,
-                                /* statx_flags= */ 0,
-                                XSTATX_MNT_ID_BEST,
-                                mask,
-                                /* optional_mask= */ 0,
-                                /* mandatory_attributes= */ 0,
-                                &stx_child);
+                r = chase_statx(child, &stx_child);
                 if (r < 0)
                         return r;
 
@@ -592,14 +547,7 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
                                 if (fd < 0)
                                         return fd;
 
-                                r = xstatx_full(fd,
-                                                /* path= */ NULL,
-                                                /* statx_flags= */ 0,
-                                                XSTATX_MNT_ID_BEST,
-                                                mask,
-                                                /* optional_mask= */ 0,
-                                                /* mandatory_attributes= */ 0,
-                                                &stx);
+                                r = chase_statx(fd, &stx);
                                 if (r < 0)
                                         return r;
 
@@ -611,9 +559,7 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
                                                 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. */
-                                if (!FLAGS_SET(flags, CHASE_AT_RESOLVE_IN_ROOT))
+                                if (dir_fd != root_fd)
                                         need_absolute = true;
 
                                 r = free_and_strdup(&done, need_absolute ? "/" : NULL);
@@ -647,7 +593,6 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
                 close_and_replace(fd, child);
         }
 
-success:
         if (exists) {
                 if (FLAGS_SET(flags, CHASE_MUST_BE_DIRECTORY)) {
                         r = statx_verify_directory(&stx);
@@ -755,14 +700,9 @@ int chase(const char *path, const char *root, ChaseFlags flags, char **ret_path,
                 return r;
 
         /* A root directory of "/" or "" is identical to "/". */
-        if (empty_or_root(root)) {
+        if (empty_or_root(root))
                 root = "/";
-
-                /* When the root directory is "/", we will drop CHASE_AT_RESOLVE_IN_ROOT in chaseat(),
-                 * hence below is not necessary, but let's shortcut. */
-                flags &= ~CHASE_AT_RESOLVE_IN_ROOT;
-
-        } else {
+        else {
                 r = path_make_absolute_cwd(root, &root_abs);
                 if (r < 0)
                         return r;
@@ -779,8 +719,6 @@ int chase(const char *path, const char *root, ChaseFlags flags, char **ret_path,
                         if (!absolute)
                                 return -ENOMEM;
                 }
-
-                flags |= CHASE_AT_RESOLVE_IN_ROOT;
         }
 
         if (!absolute) {
@@ -804,7 +742,7 @@ int chase(const char *path, const char *root, ChaseFlags flags, char **ret_path,
                         return -errno;
         }
 
-        r = chaseat(fd, path, flags & ~CHASE_PREFIX_ROOT, ret_path ? &p : NULL, ret_fd ? &pfd : NULL);
+        r = chaseat(fd, fd, path, flags & ~CHASE_PREFIX_ROOT, ret_path ? &p : NULL, ret_fd ? &pfd : NULL);
         if (r < 0)
                 return r;
 
@@ -927,36 +865,34 @@ int chase_and_open(
 
         _cleanup_close_ int path_fd = -EBADF;
         _cleanup_free_ char *p = NULL, *fname = NULL;
+        const char *open_name = NULL;
         int r;
 
         assert(!(chase_flags & (CHASE_NONEXISTENT|CHASE_STEP)));
 
-        XOpenFlags xopen_flags = 0;
-        if (FLAGS_SET(chase_flags, CHASE_MUST_BE_DIRECTORY))
-                open_flags |= O_DIRECTORY;
-        if (FLAGS_SET(chase_flags, CHASE_MUST_BE_REGULAR))
-                xopen_flags |= XO_REGULAR;
-
         if (empty_or_root(root) && !ret_path && (chase_flags & CHASE_NO_SHORTCUT_MASK) == 0)
                 /* Shortcut this call if none of the special features of this call are requested */
-                return xopenat_full(AT_FDCWD, path,
-                                    open_flags | (FLAGS_SET(chase_flags, CHASE_NOFOLLOW) ? O_NOFOLLOW : 0),
-                                    xopen_flags,
-                                    MODE_INVALID);
+                return chase_xopenat(AT_FDCWD, path, chase_flags, open_flags, /* xopen_flags= */ 0);
 
         r = chase(path, root, (CHASE_PARENT|chase_flags)&~CHASE_MUST_BE_REGULAR, &p, &path_fd);
         if (r < 0)
                 return r;
         assert(path_fd >= 0);
 
-        if (!FLAGS_SET(chase_flags, CHASE_PARENT) &&
-            !FLAGS_SET(chase_flags, CHASE_EXTRACT_FILENAME)) {
-                r = chase_extract_filename(p, root, &fname);
-                if (r < 0)
-                        return r;
+        if (!FLAGS_SET(chase_flags, CHASE_PARENT)) {
+                if (FLAGS_SET(chase_flags, CHASE_EXTRACT_FILENAME))
+                        /* chase() with CHASE_EXTRACT_FILENAME already returns just the filename in
+                         * p — use it directly without redundant extraction. */
+                        open_name = p;
+                else {
+                        r = chase_extract_filename(p, root, &fname);
+                        if (r < 0)
+                                return r;
+                        open_name = fname;
+                }
         }
 
-        r = xopenat_full(path_fd, strempty(fname), open_flags|O_NOFOLLOW, xopen_flags, MODE_INVALID);
+        r = chase_xopenat(path_fd, strempty(open_name), chase_flags, open_flags|O_NOFOLLOW, /* xopen_flags= */ 0);
         if (r < 0)
                 return r;
 
@@ -1010,8 +946,10 @@ int chase_and_stat(const char *path, const char *root, ChaseFlags chase_flags, c
         assert(!(chase_flags & (CHASE_NONEXISTENT|CHASE_STEP)));
         assert(ret_stat);
 
-        if (empty_or_root(root) && !ret_path && (chase_flags & CHASE_NO_SHORTCUT_MASK) == 0)
-                /* Shortcut this call if none of the special features of this call are requested */
+        if (empty_or_root(root) && !ret_path && (chase_flags & (CHASE_NO_SHORTCUT_MASK|CHASE_MUST_BE_ANY)) == 0)
+                /* Shortcut this call if none of the special features of this call are requested. We can't
+                 * take the shortcut if CHASE_MUST_BE_* is set because fstatat() alone does not verify the
+                 * inode type. */
                 return RET_NERRNO(fstatat(AT_FDCWD, path, ret_stat,
                                           FLAGS_SET(chase_flags, CHASE_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0));
 
@@ -1037,8 +975,8 @@ int chase_and_access(const char *path, const char *root, ChaseFlags chase_flags,
         assert(path);
         assert(!(chase_flags & (CHASE_NONEXISTENT|CHASE_STEP)));
 
-        if (empty_or_root(root) && !ret_path && (chase_flags & CHASE_NO_SHORTCUT_MASK) == 0)
-                /* Shortcut this call if none of the special features of this call are requested */
+        if (empty_or_root(root) && !ret_path && (chase_flags & (CHASE_NO_SHORTCUT_MASK|CHASE_MUST_BE_ANY)) == 0)
+                /* Shortcut this call if none of the special features of this call are requested. */
                 return RET_NERRNO(faccessat(AT_FDCWD, path, access_mode,
                                             FLAGS_SET(chase_flags, CHASE_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0));
 
@@ -1130,6 +1068,7 @@ int chase_and_open_parent(const char *path, const char *root, ChaseFlags chase_f
 }
 
 int chase_and_openat(
+                int root_fd,
                 int dir_fd,
                 const char *path,
                 ChaseFlags chase_flags,
@@ -1138,39 +1077,33 @@ int chase_and_openat(
 
         _cleanup_close_ int path_fd = -EBADF;
         _cleanup_free_ char *p = NULL, *fname = NULL;
+        const char *open_name = NULL;
         int r;
 
         assert(!(chase_flags & (CHASE_NONEXISTENT|CHASE_STEP|CHASE_MUST_BE_SOCKET)));
 
-        XOpenFlags xopen_flags = 0;
-        if (FLAGS_SET(chase_flags, CHASE_MUST_BE_DIRECTORY))
-                open_flags |= O_DIRECTORY;
-        if (FLAGS_SET(chase_flags, CHASE_MUST_BE_REGULAR))
-                xopen_flags |= XO_REGULAR;
-
-        if (dir_fd == AT_FDCWD && !ret_path && (chase_flags & CHASE_NO_SHORTCUT_MASK) == 0)
+        if (root_fd == XAT_FDROOT && dir_fd == AT_FDCWD && !ret_path && (chase_flags & CHASE_NO_SHORTCUT_MASK) == 0)
                 /* Shortcut this call if none of the special features of this call are requested */
-                return xopenat_full(dir_fd, path,
-                                    open_flags | (FLAGS_SET(chase_flags, CHASE_NOFOLLOW) ? O_NOFOLLOW : 0),
-                                    xopen_flags,
-                                    MODE_INVALID);
+                return chase_xopenat(dir_fd, path, chase_flags, open_flags, /* xopen_flags= */ 0);
 
-        r = chaseat(dir_fd, path, (chase_flags|CHASE_PARENT)&~CHASE_MUST_BE_REGULAR, &p, &path_fd);
+        r = chaseat(root_fd, dir_fd, path, (chase_flags|CHASE_PARENT)&~CHASE_MUST_BE_REGULAR, &p, &path_fd);
         if (r < 0)
                 return r;
 
         if (!FLAGS_SET(chase_flags, CHASE_PARENT)) {
-                r = path_extract_filename(p, &fname);
-                if (r < 0 && r != -EADDRNOTAVAIL)
-                        return r;
+                if (FLAGS_SET(chase_flags, CHASE_EXTRACT_FILENAME))
+                        /* chaseat() with CHASE_EXTRACT_FILENAME already returns just the filename in
+                         * p — use it directly without redundant extraction. */
+                        open_name = p;
+                else {
+                        r = path_extract_filename(p, &fname);
+                        if (r < 0 && r != -EADDRNOTAVAIL)
+                                return r;
+                        open_name = fname;
+                }
         }
 
-        r = xopenat_full(
-                        path_fd,
-                        strempty(fname),
-                        open_flags|O_NOFOLLOW,
-                        xopen_flags,
-                        MODE_INVALID);
+        r = chase_xopenat(path_fd, strempty(open_name), chase_flags, open_flags|O_NOFOLLOW, /* xopen_flags= */ 0);
         if (r < 0)
                 return r;
 
@@ -1180,7 +1113,7 @@ int chase_and_openat(
         return r;
 }
 
-int chase_and_opendirat(int dir_fd, const char *path, ChaseFlags chase_flags, char **ret_path, DIR **ret_dir) {
+int chase_and_opendirat(int root_fd, int dir_fd, const char *path, ChaseFlags chase_flags, char **ret_path, DIR **ret_dir) {
         _cleanup_close_ int path_fd = -EBADF;
         _cleanup_free_ char *p = NULL;
         DIR *d;
@@ -1189,7 +1122,7 @@ int chase_and_opendirat(int dir_fd, const char *path, ChaseFlags chase_flags, ch
         assert(!(chase_flags & (CHASE_NONEXISTENT|CHASE_STEP|CHASE_MUST_BE_REGULAR|CHASE_MUST_BE_SOCKET)));
         assert(ret_dir);
 
-        if (dir_fd == AT_FDCWD && !ret_path && (chase_flags & CHASE_NO_SHORTCUT_MASK) == 0) {
+        if (root_fd == XAT_FDROOT && dir_fd == AT_FDCWD && !ret_path && (chase_flags & CHASE_NO_SHORTCUT_MASK) == 0) {
                 /* Shortcut this call if none of the special features of this call are requested */
                 d = opendir(path);
                 if (!d)
@@ -1199,7 +1132,7 @@ int chase_and_opendirat(int dir_fd, const char *path, ChaseFlags chase_flags, ch
                 return 0;
         }
 
-        r = chaseat(dir_fd, path, chase_flags, ret_path ? &p : NULL, &path_fd);
+        r = chaseat(root_fd, dir_fd, path, chase_flags, ret_path ? &p : NULL, &path_fd);
         if (r < 0)
                 return r;
         assert(path_fd >= 0);
@@ -1215,7 +1148,7 @@ int chase_and_opendirat(int dir_fd, const char *path, ChaseFlags chase_flags, ch
         return 0;
 }
 
-int chase_and_statat(int dir_fd, const char *path, ChaseFlags chase_flags, char **ret_path, struct stat *ret_stat) {
+int chase_and_statat(int root_fd, int dir_fd, const char *path, ChaseFlags chase_flags, char **ret_path, struct stat *ret_stat) {
         _cleanup_close_ int path_fd = -EBADF;
         _cleanup_free_ char *p = NULL;
         int r;
@@ -1224,12 +1157,14 @@ int chase_and_statat(int dir_fd, const char *path, ChaseFlags chase_flags, char
         assert(!(chase_flags & (CHASE_NONEXISTENT|CHASE_STEP)));
         assert(ret_stat);
 
-        if (dir_fd == AT_FDCWD && !ret_path && (chase_flags & CHASE_NO_SHORTCUT_MASK) == 0)
-                /* Shortcut this call if none of the special features of this call are requested */
+        if (root_fd == XAT_FDROOT && dir_fd == AT_FDCWD && !ret_path && (chase_flags & (CHASE_NO_SHORTCUT_MASK|CHASE_MUST_BE_ANY)) == 0)
+                /* Shortcut this call if none of the special features of this call are requested. We can't
+                 * take the shortcut if CHASE_MUST_BE_* is set because fstatat() alone does not verify the
+                 * inode type. */
                 return RET_NERRNO(fstatat(AT_FDCWD, path, ret_stat,
                                           FLAGS_SET(chase_flags, CHASE_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0));
 
-        r = chaseat(dir_fd, path, chase_flags, ret_path ? &p : NULL, &path_fd);
+        r = chaseat(root_fd, dir_fd, path, chase_flags, ret_path ? &p : NULL, &path_fd);
         if (r < 0)
                 return r;
         assert(path_fd >= 0);
@@ -1243,7 +1178,7 @@ int chase_and_statat(int dir_fd, const char *path, ChaseFlags chase_flags, char
         return 0;
 }
 
-int chase_and_accessat(int dir_fd, const char *path, ChaseFlags chase_flags, int access_mode, char **ret_path) {
+int chase_and_accessat(int root_fd, int dir_fd, const char *path, ChaseFlags chase_flags, int access_mode, char **ret_path) {
         _cleanup_close_ int path_fd = -EBADF;
         _cleanup_free_ char *p = NULL;
         int r;
@@ -1251,12 +1186,12 @@ int chase_and_accessat(int dir_fd, const char *path, ChaseFlags chase_flags, int
         assert(path);
         assert(!(chase_flags & (CHASE_NONEXISTENT|CHASE_STEP)));
 
-        if (dir_fd == AT_FDCWD && !ret_path && (chase_flags & CHASE_NO_SHORTCUT_MASK) == 0)
-                /* Shortcut this call if none of the special features of this call are requested */
+        if (root_fd == XAT_FDROOT && dir_fd == AT_FDCWD && !ret_path && (chase_flags & (CHASE_NO_SHORTCUT_MASK|CHASE_MUST_BE_ANY)) == 0)
+                /* Shortcut this call if none of the special features of this call are requested. */
                 return RET_NERRNO(faccessat(AT_FDCWD, path, access_mode,
                                             FLAGS_SET(chase_flags, CHASE_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0));
 
-        r = chaseat(dir_fd, path, chase_flags, ret_path ? &p : NULL, &path_fd);
+        r = chaseat(root_fd, dir_fd, path, chase_flags, ret_path ? &p : NULL, &path_fd);
         if (r < 0)
                 return r;
         assert(path_fd >= 0);
@@ -1272,6 +1207,7 @@ int chase_and_accessat(int dir_fd, const char *path, ChaseFlags chase_flags, int
 }
 
 int chase_and_fopenat_unlocked(
+                int root_fd,
                 int dir_fd,
                 const char *path,
                 ChaseFlags chase_flags,
@@ -1292,7 +1228,7 @@ int chase_and_fopenat_unlocked(
         if (mode_flags < 0)
                 return mode_flags;
 
-        fd = chase_and_openat(dir_fd, path, chase_flags, mode_flags, ret_path ? &final_path : NULL);
+        fd = chase_and_openat(root_fd, dir_fd, path, chase_flags, mode_flags, ret_path ? &final_path : NULL);
         if (fd < 0)
                 return fd;
 
@@ -1306,7 +1242,7 @@ int chase_and_fopenat_unlocked(
         return 0;
 }
 
-int chase_and_unlinkat(int dir_fd, const char *path, ChaseFlags chase_flags, int unlink_flags, char **ret_path) {
+int chase_and_unlinkat(int root_fd, int dir_fd, const char *path, ChaseFlags chase_flags, int unlink_flags, char **ret_path) {
         _cleanup_free_ char *p = NULL, *fname = NULL;
         _cleanup_close_ int fd = -EBADF;
         int r;
@@ -1314,7 +1250,7 @@ int chase_and_unlinkat(int dir_fd, const char *path, ChaseFlags chase_flags, int
         assert(path);
         assert(!(chase_flags & (CHASE_NONEXISTENT|CHASE_STEP|CHASE_PARENT|CHASE_MUST_BE_SOCKET|CHASE_MUST_BE_REGULAR|CHASE_MUST_BE_DIRECTORY|CHASE_EXTRACT_FILENAME|CHASE_MKDIR_0755)));
 
-        fd = chase_and_openat(dir_fd, path, chase_flags|CHASE_PARENT|CHASE_NOFOLLOW, O_PATH|O_DIRECTORY|O_CLOEXEC, &p);
+        fd = chase_and_openat(root_fd, dir_fd, path, chase_flags|CHASE_PARENT|CHASE_NOFOLLOW, O_PATH|O_DIRECTORY|O_CLOEXEC, &p);
         if (fd < 0)
                 return fd;
 
@@ -1331,12 +1267,12 @@ int chase_and_unlinkat(int dir_fd, const char *path, ChaseFlags chase_flags, int
         return 0;
 }
 
-int chase_and_open_parent_at(int dir_fd, const char *path, ChaseFlags chase_flags, char **ret_filename) {
+int chase_and_open_parent_at(int root_fd, int dir_fd, const char *path, ChaseFlags chase_flags, char **ret_filename) {
         int pfd, r;
 
         assert(!(chase_flags & (CHASE_NONEXISTENT|CHASE_STEP)));
 
-        r = chaseat(dir_fd, path, CHASE_PARENT|CHASE_EXTRACT_FILENAME|chase_flags, ret_filename, &pfd);
+        r = chaseat(root_fd, dir_fd, path, CHASE_PARENT|CHASE_EXTRACT_FILENAME|chase_flags, ret_filename, &pfd);
         if (r < 0)
                 return r;
 
index d779658ba15f856cef92d55b3a3d7213da50a04a..afb50fa61ec7c2527c00cde6ab44937b3e0ccd19 100644 (file)
@@ -15,21 +15,19 @@ typedef enum ChaseFlags {
                                              * right-most component refers to symlink, return O_PATH fd of the symlink. */
         CHASE_WARN               = 1 << 8,  /* Emit an appropriate warning when an error is encountered.
                                              * Note: this may do an NSS lookup, hence this flag cannot be used in PID 1. */
-        CHASE_AT_RESOLVE_IN_ROOT = 1 << 9,  /* Same as openat2()'s RESOLVE_IN_ROOT flag, symlinks are resolved
-                                             * relative to the given directory fd instead of root. */
-        CHASE_PROHIBIT_SYMLINKS  = 1 << 10, /* Refuse all symlinks */
-        CHASE_PARENT             = 1 << 11, /* Chase the parent directory of the given path. Note that the
+        CHASE_PROHIBIT_SYMLINKS  = 1 << 9,  /* Refuse all symlinks */
+        CHASE_PARENT             = 1 << 10, /* Chase the parent directory of the given path. Note that the
                                              * full path is still stored in ret_path and only the returned
                                              * file descriptor will point to the parent directory. Note that
                                              * the result path is the root or '.', then the file descriptor
                                              * also points to the result path even if this flag is set.
                                              * When this specified, chase() will succeed with 1 even if the
                                              * file points to the last path component does not exist. */
-        CHASE_MKDIR_0755         = 1 << 12, /* Create any missing directories in the given path. */
-        CHASE_EXTRACT_FILENAME   = 1 << 13, /* Only return the last component of the resolved path */
-        CHASE_MUST_BE_DIRECTORY  = 1 << 14, /* Fail if returned inode fd is not a dir */
-        CHASE_MUST_BE_REGULAR    = 1 << 15, /* Fail if returned inode fd is not a regular file */
-        CHASE_MUST_BE_SOCKET     = 1 << 16, /* Fail if returned inode fd is not a socket */
+        CHASE_MKDIR_0755         = 1 << 11, /* Create any missing directories in the given path. */
+        CHASE_EXTRACT_FILENAME   = 1 << 12, /* Only return the last component of the resolved path */
+        CHASE_MUST_BE_DIRECTORY  = 1 << 13, /* Fail if returned inode fd is not a dir */
+        CHASE_MUST_BE_REGULAR    = 1 << 14, /* Fail if returned inode fd is not a regular file */
+        CHASE_MUST_BE_SOCKET     = 1 << 15, /* Fail if returned inode fd is not a socket */
 } ChaseFlags;
 
 int statx_unsafe_transition(const struct statx *a, const struct statx *b);
@@ -51,12 +49,12 @@ int chase_and_fopen_unlocked(const char *path, const char *root, ChaseFlags chas
 int chase_and_unlink(const char *path, const char *root, ChaseFlags chase_flags, int unlink_flags, char **ret_path);
 int chase_and_open_parent(const char *path, const char *root, ChaseFlags chase_flags, char **ret_filename);
 
-int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int *ret_fd);
+int chaseat(int root_fd, int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int *ret_fd);
 
-int chase_and_openat(int dir_fd, const char *path, ChaseFlags chase_flags, int open_flags, char **ret_path);
-int chase_and_opendirat(int dir_fd, const char *path, ChaseFlags chase_flags, char **ret_path, DIR **ret_dir);
-int chase_and_statat(int dir_fd, const char *path, ChaseFlags chase_flags, char **ret_path, struct stat *ret_stat);
-int chase_and_accessat(int dir_fd, const char *path, ChaseFlags chase_flags, int access_mode, char **ret_path);
-int chase_and_fopenat_unlocked(int dir_fd, const char *path, ChaseFlags chase_flags, const char *open_flags, char **ret_path, FILE **ret_file);
-int chase_and_unlinkat(int dir_fd, const char *path, ChaseFlags chase_flags, int unlink_flags, char **ret_path);
-int chase_and_open_parent_at(int dir_fd, const char *path, ChaseFlags chase_flags, char **ret_filename);
+int chase_and_openat(int root_fd, int dir_fd, const char *path, ChaseFlags chase_flags, int open_flags, char **ret_path);
+int chase_and_opendirat(int root_fd, int dir_fd, const char *path, ChaseFlags chase_flags, char **ret_path, DIR **ret_dir);
+int chase_and_statat(int root_fd, int dir_fd, const char *path, ChaseFlags chase_flags, char **ret_path, struct stat *ret_stat);
+int chase_and_accessat(int root_fd, int dir_fd, const char *path, ChaseFlags chase_flags, int access_mode, char **ret_path);
+int chase_and_fopenat_unlocked(int root_fd, int dir_fd, const char *path, ChaseFlags chase_flags, const char *open_flags, char **ret_path, FILE **ret_file);
+int chase_and_unlinkat(int root_fd, int dir_fd, const char *path, ChaseFlags chase_flags, int unlink_flags, char **ret_path);
+int chase_and_open_parent_at(int root_fd, int dir_fd, const char *path, ChaseFlags chase_flags, char **ret_filename);
index 4db486d8ada7e77f7f7374201952464698cc9747..d4ff194d4cce45aaf8dd293353f97f2d1d483819 100644 (file)
@@ -128,7 +128,7 @@ static bool conf_files_need_stat(ConfFilesFlags flags) {
 }
 
 static ChaseFlags conf_files_chase_flags(ConfFilesFlags flags) {
-        ChaseFlags chase_flags = CHASE_AT_RESOLVE_IN_ROOT;
+        ChaseFlags chase_flags = 0;
 
         if (!conf_files_need_stat(flags) || FLAGS_SET(flags, CONF_FILES_FILTER_MASKED_BY_SYMLINK))
                 /* Even if no verification is requested, let's unconditionally call chaseat(),
@@ -164,7 +164,7 @@ static int conf_file_chase_and_verify(
 
         root = empty_to_root(root);
 
-        r = chaseat(rfd, path, conf_files_chase_flags(flags), &resolved_path, &fd);
+        r = chaseat(rfd, rfd, path, conf_files_chase_flags(flags), &resolved_path, &fd);
         if (r < 0)
                 return log_full_errno(log_level, r, "Failed to chase '%s%s': %m",
                                       root, skip_leading_slash(original_path));
@@ -306,7 +306,7 @@ int conf_file_new_at(
         if (r < 0 && r != -EDESTADDRREQ)
                 return log_full_errno(log_level, r, "Failed to extract directory from '%s': %m", path);
         if (r >= 0) {
-                r = chaseat(rfd, dirpath,
+                r = chaseat(rfd, rfd, dirpath,
                             CHASE_MUST_BE_DIRECTORY | conf_files_chase_flags(flags),
                             &resolved_dirpath, /* ret_fd= */ NULL);
                 if (r < 0)
@@ -637,7 +637,7 @@ static int conf_files_list_impl(
                 _cleanup_closedir_ DIR *dir = NULL;
                 _cleanup_free_ char *path = NULL;
 
-                r = chase_and_opendirat(rfd, *p, CHASE_AT_RESOLVE_IN_ROOT, &path, &dir);
+                r = chase_and_opendirat(rfd, rfd, *p, 0, &path, &dir);
                 if (r < 0) {
                         if (r != -ENOENT)
                                 log_full_errno(conf_files_log_level(flags), r,
index 2dfa37f20bcc7deb8a5e583d27702a3042f233f7..661667a6b2a1ebfb1f7f8726a62bf7c8e0b6a50c 100644 (file)
@@ -1684,7 +1684,8 @@ int write_data_file_atomic_at(
         _cleanup_close_ int mfd = -EBADF;
         if (dn) {
                 /* If there's a directory component, readjust our position */
-                r = chaseat(dir_fd,
+                r = chaseat(XAT_FDROOT,
+                            dir_fd,
                             dn,
                             FLAGS_SET(flags, WRITE_DATA_FILE_MKDIR_0755) ? CHASE_MKDIR_0755 : 0,
                             /* ret_path= */ NULL,
index 837880baa2ec21f0fd0b2b9c923ad138cee4f075..7b353542e342ff64746562c988ee17fc9a5fa9e9 100644 (file)
@@ -46,7 +46,7 @@ int mkdirat_safe_internal(
         if ((flags & MKDIR_FOLLOW_SYMLINK) && S_ISLNK(st.st_mode)) {
                 _cleanup_free_ char *p = NULL;
 
-                r = chaseat(dir_fd, path, CHASE_NONEXISTENT, &p, NULL);
+                r = chaseat(XAT_FDROOT, dir_fd, path, CHASE_NONEXISTENT, &p, NULL);
                 if (r < 0)
                         return r;
                 if (r == 0)
index 66bab1bcee9d24bb4f1f567cf9ddfe00b29928ce..06b476f1344a8bdcdddc082bd97f2e78085978f9 100644 (file)
@@ -172,10 +172,10 @@ int open_os_release_at(int rfd, char **ret_path, int *ret_fd) {
 
         e = secure_getenv("SYSTEMD_OS_RELEASE");
         if (e)
-                return chaseat(rfd, e, CHASE_AT_RESOLVE_IN_ROOT, ret_path, ret_fd);
+                return chaseat(rfd, rfd, e, /* flags= */ 0, ret_path, ret_fd);
 
         FOREACH_STRING(path, "/etc/os-release", "/usr/lib/os-release") {
-                r = chaseat(rfd, path, CHASE_AT_RESOLVE_IN_ROOT, ret_path, ret_fd);
+                r = chaseat(rfd, rfd, path, /* flags= */ 0, ret_path, ret_fd);
                 if (r != -ENOENT)
                         return r;
         }
@@ -238,7 +238,7 @@ int open_extension_release_at(
                 return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "The extension name %s is invalid.", extension);
 
         p = strjoina(image_class_release_info[image_class].release_file_path_prefix, extension);
-        r = chaseat(rfd, p, CHASE_AT_RESOLVE_IN_ROOT, ret_path, ret_fd);
+        r = chaseat(rfd, rfd, p, /* flags= */ 0, ret_path, ret_fd);
         log_full_errno_zerook(LOG_DEBUG, MIN(r, 0), "Checking for %s: %m", p);
         if (r != -ENOENT)
                 return r;
@@ -249,7 +249,7 @@ int open_extension_release_at(
          * xattr is checked to ensure the author of the image considers it OK if names do not match. */
 
         p = image_class_release_info[image_class].release_file_directory;
-        r = chase_and_opendirat(rfd, p, CHASE_AT_RESOLVE_IN_ROOT, &dir_path, &dir);
+        r = chase_and_opendirat(rfd, rfd, p, /* chase_flags= */ 0, &dir_path, &dir);
         if (r < 0)
                 return log_debug_errno(r, "Cannot open %s, ignoring: %m", p);
 
index 93a3852879b298be952cc38c1cb250844d3b9be7..b735d27474272d1a1bbc384b5c7a2f2c95e6c850 100644 (file)
@@ -185,7 +185,7 @@ const char* default_root_shell_at(int rfd) {
 
         assert(rfd >= 0 || rfd == AT_FDCWD);
 
-        int r = chaseat(rfd, DEFAULT_USER_SHELL, CHASE_AT_RESOLVE_IN_ROOT, NULL, NULL);
+        int r = chaseat(rfd, rfd, DEFAULT_USER_SHELL, /* flags= */ 0, NULL, NULL);
         if (r < 0 && r != -ENOENT)
                 log_debug_errno(r, "Failed to look up shell '%s': %m", DEFAULT_USER_SHELL);
         if (r > 0)
index 96bc9213cf6b24b17a5f9f49b1576d85df460573..a8ac742b9a760a3cab9e3d44eeeebc7b41d6f82d 100644 (file)
@@ -276,9 +276,10 @@ static int load_etc_machine_info(InstallContext *c) {
 
         _cleanup_close_ int fd =
                 chase_and_openat(
+                                c->root_fd,
                                 c->root_fd,
                                 "/etc/machine-info",
-                                CHASE_AT_RESOLVE_IN_ROOT|CHASE_MUST_BE_REGULAR,
+                                CHASE_MUST_BE_REGULAR,
                                 O_RDONLY|O_CLOEXEC,
                                 /* ret_path= */ NULL);
         if (fd == -ENOENT)
@@ -392,8 +393,9 @@ static int settle_make_entry_directory(InstallContext *c) {
 
                                 _cleanup_close_ int fd = -EBADF;
                                 r = chaseat(c->root_fd,
+                                            c->root_fd,
                                             "/etc/machine-id",
-                                            CHASE_AT_RESOLVE_IN_ROOT|CHASE_MUST_BE_REGULAR,
+                                            CHASE_MUST_BE_REGULAR,
                                             /* ret_path= */ NULL,
                                             &fd);
                                 if (r < 0)
@@ -546,8 +548,9 @@ static int mkdir_one(const char *root, int root_fd, const char *path) {
                 return log_oom();
 
         r = chaseat(root_fd,
+                    root_fd,
                     path,
-                    CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY,
+                    CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY,
                     /* ret_path= */ NULL,
                     /* ret_fd= */ NULL);
         if (r < 0)
@@ -611,9 +614,10 @@ static int update_efi_boot_binaries(
 
         _cleanup_closedir_ DIR *d = NULL;
         r = chase_and_opendirat(
+                        c->esp_fd,
                         c->esp_fd,
                         "/EFI/BOOT",
-                        CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MUST_BE_DIRECTORY,
+                        CHASE_PROHIBIT_SYMLINKS|CHASE_MUST_BE_DIRECTORY,
                         /* ret_path= */ NULL,
                         &d);
         if (r == -ENOENT)
@@ -679,9 +683,10 @@ static int copy_one_file(
         _cleanup_close_ int source_fd = -EBADF;
         if (IN_SET(c->install_source, INSTALL_SOURCE_AUTO, INSTALL_SOURCE_IMAGE)) {
                 source_fd = chase_and_openat(
+                                c->root_fd,
                                 c->root_fd,
                                 sp,
-                                CHASE_AT_RESOLVE_IN_ROOT|CHASE_MUST_BE_REGULAR,
+                                CHASE_MUST_BE_REGULAR,
                                 O_RDONLY|O_CLOEXEC,
                                 &source_path);
                 if (source_fd < 0 && (source_fd != -ENOENT || c->install_source != INSTALL_SOURCE_AUTO))
@@ -709,8 +714,9 @@ static int copy_one_file(
 
         _cleanup_close_ int dest_parent_fd = -EBADF;
         r = chaseat(c->esp_fd,
+                    c->esp_fd,
                     "/EFI/systemd",
-                    CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY,
+                    CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY,
                     /* ret_path= */ NULL,
                     &dest_parent_fd);
         if (r < 0)
@@ -740,8 +746,9 @@ static int copy_one_file(
 
                 _cleanup_close_ int default_dest_parent_fd = -EBADF;
                 r = chaseat(c->esp_fd,
+                            c->esp_fd,
                             "/EFI/BOOT",
-                            CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY,
+                            CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY,
                             /* ret_path= */ NULL,
                             &default_dest_parent_fd);
                 if (r < 0)
@@ -778,9 +785,10 @@ static int install_binaries(
         _cleanup_closedir_ DIR *d = NULL;
         if (IN_SET(c->install_source, INSTALL_SOURCE_AUTO, INSTALL_SOURCE_IMAGE)) {
                 r = chase_and_opendirat(
+                                c->root_fd,
                                 c->root_fd,
                                 BOOTLIBDIR,
-                                CHASE_AT_RESOLVE_IN_ROOT|CHASE_MUST_BE_DIRECTORY,
+                                CHASE_MUST_BE_DIRECTORY,
                                 &source_path,
                                 &d);
                 if (r < 0 && (r != -ENOENT || c->install_source != INSTALL_SOURCE_AUTO))
@@ -845,8 +853,9 @@ static int install_loader_config(InstallContext *c) {
 
         _cleanup_close_ int loader_dir_fd = -EBADF;
         r = chaseat(c->esp_fd,
+                    c->esp_fd,
                     "loader",
-                    CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY,
+                    CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY,
                     /* ret_path= */ NULL,
                     &loader_dir_fd);
         if (r < 0)
@@ -899,8 +908,9 @@ static int install_loader_specification(InstallContext *c) {
 
         _cleanup_close_ int loader_dir_fd = -EBADF;
         r = chaseat(dollar_boot_fd,
+                    dollar_boot_fd,
                     "loader",
-                    CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY,
+                    CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY,
                     /* ret_path= */ NULL,
                     &loader_dir_fd);
         if (r < 0)
@@ -973,8 +983,9 @@ static int install_entry_token(InstallContext *c) {
 
         _cleanup_close_ int dfd = -EBADF;
         r = chaseat(c->root_fd,
+                    c->root_fd,
                     confdir,
-                    CHASE_AT_RESOLVE_IN_ROOT|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY,
+                    CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY,
                     /* ret_path= */ NULL,
                     &dfd);
         if (r < 0)
@@ -1040,8 +1051,9 @@ static int install_secure_boot_auto_enroll(InstallContext *c) {
 
         _cleanup_close_ int keys_fd = -EBADF;
         r = chaseat(c->esp_fd,
+                    c->esp_fd,
                     "loader/keys/auto",
-                    CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY,
+                    CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY,
                     /* ret_path= */ NULL,
                     &keys_fd);
         if (r < 0)
@@ -1352,9 +1364,10 @@ static int install_variables(
                 return log_oom();
 
         r = chase_and_accessat(
+                        c->esp_fd,
                         c->esp_fd,
                         path,
-                        CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MUST_BE_REGULAR,
+                        CHASE_PROHIBIT_SYMLINKS|CHASE_MUST_BE_REGULAR,
                         F_OK,
                         /* ret_path= */ NULL);
         if (r == -ENOENT)
@@ -1436,9 +1449,10 @@ static int are_we_installed(InstallContext *c) {
                 return c->esp_fd;
 
         _cleanup_close_ int fd = chase_and_openat(
+                        c->esp_fd,
                         c->esp_fd,
                         "/EFI/systemd",
-                        CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MUST_BE_DIRECTORY,
+                        CHASE_PROHIBIT_SYMLINKS|CHASE_MUST_BE_DIRECTORY,
                         O_RDONLY|O_CLOEXEC|O_DIRECTORY,
                         /* ret_path= */ NULL);
         if (fd == -ENOENT)
@@ -1655,9 +1669,10 @@ static int remove_boot_efi(InstallContext *c) {
         _cleanup_closedir_ DIR *d = NULL;
         _cleanup_free_ char *p = NULL;
         r = chase_and_opendirat(
+                        c->esp_fd,
                         c->esp_fd,
                         "/EFI/BOOT",
-                        CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MUST_BE_DIRECTORY,
+                        CHASE_PROHIBIT_SYMLINKS|CHASE_MUST_BE_DIRECTORY,
                         &p,
                         &d);
         if (r == -ENOENT)
@@ -1725,9 +1740,10 @@ static int unlink_inode(const char *root, int root_fd, const char *path, mode_t
                 return log_oom();
 
         r = chase_and_unlinkat(
+                        root_fd,
                         root_fd,
                         path,
-                        CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS,
+                        CHASE_PROHIBIT_SYMLINKS,
                         S_ISDIR(type) ? AT_REMOVEDIR : 0,
                         /* ret_path= */ NULL);
         if (r < 0) {
@@ -1773,8 +1789,9 @@ static int remove_binaries(InstallContext *c) {
 
         _cleanup_close_ int efi_fd = -EBADF;
         r = chaseat(c->esp_fd,
+                    c->esp_fd,
                     "EFI",
-                    CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MUST_BE_DIRECTORY,
+                    CHASE_PROHIBIT_SYMLINKS|CHASE_MUST_BE_DIRECTORY,
                     /* ret_path= */ NULL,
                     &efi_fd);
         if (r < 0) {
index 2138367218ba94a12ff15abdc66f3a16958be676..c663532e00e09fafb43878cce83f1af4207148ce 100644 (file)
@@ -4152,7 +4152,7 @@ static int apply_working_directory(
 
                 r = chase(wd,
                           runtime->ephemeral_copy ?: context->root_directory,
-                          CHASE_PREFIX_ROOT|CHASE_AT_RESOLVE_IN_ROOT|CHASE_TRIGGER_AUTOFS,
+                          CHASE_PREFIX_ROOT|CHASE_TRIGGER_AUTOFS,
                           /* ret_path= */ NULL,
                           &dfd);
                 if (r >= 0)
index 63e659942188f36ac8a30e632a47b2c1b99ae18f..aa00413dc304a623588b5cfa31e750dd428a95bf 100644 (file)
@@ -5911,7 +5911,7 @@ int service_determine_exec_selinux_label(Service *s, char **ret) {
 
         _cleanup_free_ char *path = NULL;
         if (s->exec_context.root_directory_as_fd)
-                r = chaseat(s->root_directory_fd, c->path, CHASE_AT_RESOLVE_IN_ROOT|CHASE_TRIGGER_AUTOFS, &path, NULL);
+                r = chaseat(s->root_directory_fd, s->root_directory_fd, c->path, CHASE_TRIGGER_AUTOFS, &path, NULL);
         else
                 r = chase(c->path, s->exec_context.root_directory, CHASE_PREFIX_ROOT|CHASE_TRIGGER_AUTOFS, &path, NULL);
         if (r < 0) {
index ea4dd5f184038f3b3030ec88c1f9cda5d1096226..3006d93407255cf9d543ff646631bd7971fc9d33 100644 (file)
@@ -339,8 +339,8 @@ static int process_locale(int rfd, sd_varlink **mute_console_link) {
 
         assert(rfd >= 0);
 
-        pfd = chase_and_open_parent_at(rfd, etc_locale_conf(),
-                                       CHASE_AT_RESOLVE_IN_ROOT|CHASE_MKDIR_0755|CHASE_WARN|CHASE_NOFOLLOW,
+        pfd = chase_and_open_parent_at(rfd, rfd, etc_locale_conf(),
+                                       CHASE_MKDIR_0755|CHASE_WARN|CHASE_NOFOLLOW,
                                        &f);
         if (pfd < 0)
                 return log_error_errno(pfd, "Failed to chase /etc/locale.conf: %m");
@@ -474,8 +474,8 @@ static int process_keymap(int rfd, sd_varlink **mute_console_link) {
 
         assert(rfd >= 0);
 
-        pfd = chase_and_open_parent_at(rfd, etc_vconsole_conf(),
-                                       CHASE_AT_RESOLVE_IN_ROOT|CHASE_MKDIR_0755|CHASE_WARN|CHASE_NOFOLLOW,
+        pfd = chase_and_open_parent_at(rfd, rfd, etc_vconsole_conf(),
+                                       CHASE_MKDIR_0755|CHASE_WARN|CHASE_NOFOLLOW,
                                        &f);
         if (pfd < 0)
                 return log_error_errno(pfd, "Failed to chase /etc/vconsole.conf: %m");
@@ -590,8 +590,8 @@ static int process_timezone(int rfd, sd_varlink **mute_console_link) {
 
         assert(rfd >= 0);
 
-        pfd = chase_and_open_parent_at(rfd, etc_localtime(),
-                                       CHASE_AT_RESOLVE_IN_ROOT|CHASE_MKDIR_0755|CHASE_WARN|CHASE_NOFOLLOW,
+        pfd = chase_and_open_parent_at(rfd, rfd, etc_localtime(),
+                                       CHASE_MKDIR_0755|CHASE_WARN|CHASE_NOFOLLOW,
                                        &f);
         if (pfd < 0)
                 return log_error_errno(pfd, "Failed to chase /etc/localtime: %m");
@@ -703,9 +703,7 @@ static int process_hostname(int rfd, sd_varlink **mute_console_link) {
 
         assert(rfd >= 0);
 
-        pfd = chase_and_open_parent_at(rfd, etc_hostname(),
-                                       CHASE_AT_RESOLVE_IN_ROOT|CHASE_MKDIR_0755|CHASE_WARN,
-                                       &f);
+        pfd = chase_and_open_parent_at(rfd, rfd, etc_hostname(), CHASE_MKDIR_0755|CHASE_WARN, &f);
         if (pfd < 0)
                 return log_error_errno(pfd, "Failed to chase /etc/hostname: %m");
 
@@ -738,8 +736,8 @@ static int process_machine_id(int rfd) {
 
         assert(rfd >= 0);
 
-        pfd = chase_and_open_parent_at(rfd, "/etc/machine-id",
-                                       CHASE_AT_RESOLVE_IN_ROOT|CHASE_MKDIR_0755|CHASE_WARN|CHASE_NOFOLLOW,
+        pfd = chase_and_open_parent_at(rfd, rfd, "/etc/machine-id",
+                                       CHASE_MKDIR_0755|CHASE_WARN|CHASE_NOFOLLOW,
                                        &f);
         if (pfd < 0)
                 return log_error_errno(pfd, "Failed to chase /etc/machine-id: %m");
@@ -848,7 +846,7 @@ static int find_shell(int rfd, const char *path) {
         if (!valid_shell(path))
                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "%s is not a valid shell", path);
 
-        r = chaseat(rfd, path, CHASE_AT_RESOLVE_IN_ROOT, NULL, NULL);
+        r = chaseat(rfd, rfd, path, /* flags= */ 0, /* ret_path= */ NULL, /* ret_fd= */ NULL);
         if (r < 0)
                 return log_error_errno(r, "Failed to resolve shell %s: %m", path);
 
@@ -1052,8 +1050,8 @@ static int process_root_account(int rfd, sd_varlink **mute_console_link) {
 
         assert(rfd >= 0);
 
-        pfd = chase_and_open_parent_at(rfd, "/etc/passwd",
-                                       CHASE_AT_RESOLVE_IN_ROOT|CHASE_MKDIR_0755|CHASE_WARN|CHASE_NOFOLLOW,
+        pfd = chase_and_open_parent_at(rfd, rfd, "/etc/passwd",
+                                       CHASE_MKDIR_0755|CHASE_WARN|CHASE_NOFOLLOW,
                                        NULL);
         if (pfd < 0)
                 return log_error_errno(pfd, "Failed to chase /etc/passwd: %m");
@@ -1169,8 +1167,8 @@ static int process_kernel_cmdline(int rfd) {
 
         assert(rfd >= 0);
 
-        pfd = chase_and_open_parent_at(rfd, "/etc/kernel/cmdline",
-                                       CHASE_AT_RESOLVE_IN_ROOT|CHASE_MKDIR_0755|CHASE_WARN|CHASE_NOFOLLOW,
+        pfd = chase_and_open_parent_at(rfd, rfd, "/etc/kernel/cmdline",
+                                       CHASE_MKDIR_0755|CHASE_WARN|CHASE_NOFOLLOW,
                                        &f);
         if (pfd < 0)
                 return log_error_errno(pfd, "Failed to chase /etc/kernel/cmdline: %m");
@@ -1202,7 +1200,7 @@ static int reset_one(int rfd, const char *path) {
         assert(rfd >= 0);
         assert(path);
 
-        pfd = chase_and_open_parent_at(rfd, path, CHASE_AT_RESOLVE_IN_ROOT|CHASE_WARN|CHASE_NOFOLLOW, &f);
+        pfd = chase_and_open_parent_at(rfd, rfd, path, CHASE_WARN|CHASE_NOFOLLOW, &f);
         if (pfd == -ENOENT)
                 return 0;
         if (pfd < 0)
index aeded46c22d9fe483337226f4763e4234db7e63c..331923e9e5578fb09033c112d6dfb33aef99e36f 100644 (file)
@@ -348,7 +348,7 @@ static int context_set_path(Context *c, const char *s, const char *source, const
                 return 0;
 
         if (c->rfd >= 0) {
-                r = chaseat(c->rfd, s, CHASE_AT_RESOLVE_IN_ROOT, &p, /* ret_fd= */ NULL);
+                r = chaseat(c->rfd, c->rfd, s, /* flags= */ 0, &p, /* ret_fd= */ NULL);
                 if (r < 0)
                         return log_warning_errno(r, "Failed to chase path %s for %s specified via %s, ignoring: %m",
                                                  s, name, source);
@@ -396,7 +396,7 @@ static int context_set_path_strv(Context *c, char* const* strv, const char *sour
                 char *p;
 
                 if (c->rfd >= 0) {
-                        r = chaseat(c->rfd, *s, CHASE_AT_RESOLVE_IN_ROOT, &p, /* ret_fd= */ NULL);
+                        r = chaseat(c->rfd, c->rfd, *s, /* flags= */ 0, &p, /* ret_fd= */ NULL);
                         if (r < 0)
                                 return log_warning_errno(r, "Failed to chase path %s for %s specified via %s: %m",
                                                          *s, name, source);
@@ -503,7 +503,7 @@ static int context_load_machine_info(Context *c) {
                 return 0;
         }
 
-        r = chase_and_fopenat_unlocked(c->rfd, path, CHASE_AT_RESOLVE_IN_ROOT, "re", NULL, &f);
+        r = chase_and_fopenat_unlocked(c->rfd, c->rfd, path, /* chase_flags= */ 0, "re", NULL, &f);
         if (r == -ENOENT)
                 return 0;
         if (r < 0)
@@ -631,7 +631,7 @@ static int context_ensure_boot_root(Context *c) {
 
         /* If all else fails, use /boot. */
         if (c->rfd >= 0) {
-                r = chaseat(c->rfd, "/boot", CHASE_AT_RESOLVE_IN_ROOT, &c->boot_root, /* ret_fd= */ NULL);
+                r = chaseat(c->rfd, c->rfd, "/boot", 0, &c->boot_root, /* ret_fd= */ NULL);
                 if (r < 0)
                         return log_error_errno(r, "Failed to chase '/boot/': %m");
         } else {
@@ -794,7 +794,7 @@ static int context_ensure_layout(Context *c) {
                 return log_oom();
 
         _cleanup_fclose_ FILE *f = NULL;
-        r = chase_and_fopenat_unlocked(c->rfd, srel_path, CHASE_AT_RESOLVE_IN_ROOT|CHASE_MUST_BE_REGULAR, "re", /* ret_path= */ NULL, &f);
+        r = chase_and_fopenat_unlocked(c->rfd, c->rfd, srel_path, CHASE_MUST_BE_REGULAR, "re", /* ret_path= */ NULL, &f);
         if (r < 0) {
                 if (r != -ENOENT)
                         return log_error_errno(r, "Failed to open '%s': %m", srel_path);
@@ -824,7 +824,7 @@ static int context_ensure_layout(Context *c) {
         if (!entry_token_path)
                 return log_oom();
 
-        r = chaseat(c->rfd, entry_token_path, CHASE_AT_RESOLVE_IN_ROOT|CHASE_MUST_BE_DIRECTORY, /* ret_path= */ NULL, /* ret_fd= */ NULL);
+        r = chaseat(c->rfd, c->rfd, entry_token_path, CHASE_MUST_BE_DIRECTORY, /* ret_path= */ NULL, /* ret_fd= */ NULL);
         if (r < 0) {
                 if (!IN_SET(r, -ENOENT, -ENOTDIR))
                         return log_error_errno(r, "Failed to check if '%s' exists and is a directory: %m", entry_token_path);
@@ -916,7 +916,7 @@ static int context_make_entry_dir(Context *c) {
                 return 0;
 
         log_debug("mkdir -p %s", c->entry_dir);
-        fd = chase_and_openat(c->rfd, c->entry_dir, CHASE_AT_RESOLVE_IN_ROOT | CHASE_MKDIR_0755,
+        fd = chase_and_openat(c->rfd, c->rfd, c->entry_dir, CHASE_MKDIR_0755,
                               O_CLOEXEC | O_CREAT | O_DIRECTORY | O_PATH, NULL);
         if (fd < 0)
                 return log_error_errno(fd, "Failed to make directory '%s': %m", c->entry_dir);
@@ -940,7 +940,7 @@ static int context_remove_entry_dir(Context *c) {
                 return 0;
 
         log_debug("rm -rf %s", c->entry_dir);
-        fd = chase_and_openat(c->rfd, c->entry_dir, CHASE_AT_RESOLVE_IN_ROOT, O_CLOEXEC | O_DIRECTORY, &p);
+        fd = chase_and_openat(c->rfd, c->rfd, c->entry_dir, /* chase_flags= */ 0, O_CLOEXEC | O_DIRECTORY, &p);
         if (fd < 0) {
                 if (IN_SET(fd, -ENOTDIR, -ENOENT))
                         return 0;
@@ -1237,7 +1237,7 @@ static int verb_add_all(int argc, char *argv[], uintptr_t _data, void *userdata)
         if (r < 0)
                 return r;
 
-        fd = chase_and_openat(c.rfd, "/usr/lib/modules", CHASE_AT_RESOLVE_IN_ROOT, O_DIRECTORY|O_RDONLY|O_CLOEXEC, NULL);
+        fd = chase_and_openat(c.rfd, c.rfd, "/usr/lib/modules", /* chase_flags= */ 0, O_DIRECTORY|O_RDONLY|O_CLOEXEC, NULL);
         if (fd < 0)
                 return log_error_errno(fd, "Failed to open %s/usr/lib/modules/: %m", strempty(arg_root));
 
@@ -1467,7 +1467,7 @@ static int verb_list(int argc, char *argv[], uintptr_t _data, void *userdata) {
         if (r < 0)
                 return r;
 
-        fd = chase_and_openat(c.rfd, "/usr/lib/modules", CHASE_AT_RESOLVE_IN_ROOT, O_DIRECTORY|O_RDONLY|O_CLOEXEC, NULL);
+        fd = chase_and_openat(c.rfd, c.rfd, "/usr/lib/modules", /* chase_flags= */ 0, O_DIRECTORY|O_RDONLY|O_CLOEXEC, NULL);
         if (fd < 0)
                 return log_error_errno(fd, "Failed to open %s/usr/lib/modules/: %m", strempty(arg_root));
 
index f2a7209a257dc0b01189e0584680b631a9ea6df2..852b01ab1a3f5e4ed6b56e9aaa5131b7fe65960e 100644 (file)
@@ -147,7 +147,7 @@ int id128_get_machine_at(int rfd, sd_id128_t *ret) {
                 return sd_id128_get_machine(ret);
 
         _cleanup_close_ int fd =
-                chase_and_openat(rfd, "/etc/machine-id", CHASE_AT_RESOLVE_IN_ROOT|CHASE_MUST_BE_REGULAR, O_RDONLY|O_CLOEXEC|O_NOCTTY, /* ret_path= */ NULL);
+                chase_and_openat(rfd, rfd, "/etc/machine-id", CHASE_MUST_BE_REGULAR, O_RDONLY|O_CLOEXEC|O_NOCTTY, /* ret_path= */ NULL);
         if (fd < 0)
                 return fd;
 
index 42860db13479e5b14153ad23c52d4563154a4428..54a5203da2cc677c0d37a3c45383b69c7f83abbf 100644 (file)
@@ -242,7 +242,7 @@ static int verify_trusted_image_fd_by_path(int fd) {
                         if (!filename_is_valid(e))
                                 continue;
 
-                        r = chaseat(dir_fd, e, CHASE_SAFE|CHASE_TRIGGER_AUTOFS, NULL, &inode_fd);
+                        r = chaseat(XAT_FDROOT, dir_fd, e, CHASE_SAFE|CHASE_TRIGGER_AUTOFS, NULL, &inode_fd);
                         if (r < 0)
                                 return log_error_errno(r, "Couldn't verify that specified image '%s' is in search path '%s': %m", p, s);
 
index 5e60ad4694fda5b6eecddb1792712d7d2f583f5b..8b2be8cead63673b86aea77ce672e1f28b7f7855 100644 (file)
@@ -363,7 +363,7 @@ static int extract_now(
                 _cleanup_free_ char *relative = NULL, *resolved = NULL;
                 _cleanup_closedir_ DIR *d = NULL;
 
-                r = chase_and_opendirat(rfd, *i, CHASE_AT_RESOLVE_IN_ROOT, &relative, &d);
+                r = chase_and_opendirat(rfd, rfd, *i, /* chase_flags= */ 0, &relative, &d);
                 if (r < 0) {
                         log_debug_errno(r, "Failed to open unit path '%s', ignoring: %m", *i);
                         continue;
index c9e966ba04ea883bfea6a930dc02368e5f32b36e..ce7580749113f7d8cab88cbf5bd5f98769ed02fe 100644 (file)
@@ -32,7 +32,7 @@ static int entry_token_load_one(int rfd, const char *dir, BootEntryTokenType *ty
         if (!p)
                 return log_oom();
 
-        r = chase_and_fopenat_unlocked(rfd, p, CHASE_AT_RESOLVE_IN_ROOT, "re", NULL, &f);
+        r = chase_and_fopenat_unlocked(rfd, rfd, p, /* chase_flags= */ 0, "re", NULL, &f);
         if (r == -ENOENT)
                 return 0;
         if (r < 0)
index c3774a5235fad0dd26450d66ccb953baf22a5ea2..3338d75f660df13f967717b6094320d6a572e95a 100644 (file)
@@ -1126,7 +1126,7 @@ static int boot_entries_find_unified_addons(
         assert(ret_addons);
         assert(config);
 
-        r = chase_and_opendirat(d_fd, addon_dir, CHASE_AT_RESOLVE_IN_ROOT, &full, &d);
+        r = chase_and_opendirat(d_fd, d_fd, addon_dir, /* chase_flags= */ 0, &full, &d);
         if (r == -ENOENT)
                 return 0;
         if (r < 0)
index 2095d803f59cf99afb7656999411ed3f20faff16..bb3e28f6bbba46f98ed1a959354352d72d3a1513 100644 (file)
@@ -1014,7 +1014,7 @@ int btrfs_subvol_remove_at(int dir_fd, const char *path, BtrfsRemoveFlags flags)
 
         assert(path);
 
-        fd = chase_and_openat(dir_fd, path, CHASE_PARENT|CHASE_EXTRACT_FILENAME, O_CLOEXEC, &subvolume);
+        fd = chase_and_openat(XAT_FDROOT, dir_fd, path, CHASE_PARENT|CHASE_EXTRACT_FILENAME, O_CLOEXEC, &subvolume);
         if (fd < 0)
                 return fd;
 
@@ -1427,7 +1427,7 @@ int btrfs_subvol_snapshot_at_full(
         if (old_fd < 0)
                 return old_fd;
 
-        new_fd = chase_and_openat(dir_fdt, to, CHASE_PARENT|CHASE_EXTRACT_FILENAME, O_CLOEXEC, &subvolume);
+        new_fd = chase_and_openat(XAT_FDROOT, dir_fdt, to, CHASE_PARENT|CHASE_EXTRACT_FILENAME, O_CLOEXEC, &subvolume);
         if (new_fd < 0)
                 return new_fd;
 
index b448032939d5359e75b435b14c72759862d126d4..ba0bc4ad8c44237e8f0cfdbdaa6242f5e09803db 100644 (file)
@@ -509,7 +509,7 @@ static int config_parse_many_files(
         /* Pin and stat() all dropins */
         STRV_FOREACH(fn, files) {
                 _cleanup_fclose_ FILE *f = NULL;
-                r = chase_and_fopenat_unlocked(root_fd, *fn, CHASE_AT_RESOLVE_IN_ROOT|CHASE_MUST_BE_REGULAR, "re", /* ret_path= */ NULL, &f);
+                r = chase_and_fopenat_unlocked(root_fd, root_fd, *fn, CHASE_MUST_BE_REGULAR, "re", /* ret_path= */ NULL, &f);
                 if (r == -ENOENT)
                         continue;
                 if (r < 0)
@@ -544,7 +544,7 @@ static int config_parse_many_files(
         /* First process the first found main config file. */
         STRV_FOREACH(fn, conf_files) {
                 _cleanup_fclose_ FILE *f = NULL;
-                r = chase_and_fopenat_unlocked(root_fd, *fn, CHASE_AT_RESOLVE_IN_ROOT|CHASE_MUST_BE_REGULAR, "re", /* ret_path= */ NULL, &f);
+                r = chase_and_fopenat_unlocked(root_fd, root_fd, *fn, CHASE_MUST_BE_REGULAR, "re", /* ret_path= */ NULL, &f);
                 if (r == -ENOENT)
                         continue;
                 if (r < 0)
index 1595bb218404ac9b437951a9ea914d431a75488a..0410b523baafd678ff11935e9cae2f6559f42065 100644 (file)
@@ -891,7 +891,7 @@ int image_find(RuntimeScope scope,
                 _cleanup_closedir_ DIR *d = NULL;
                 _cleanup_free_ char *search_path = NULL;
 
-                r = chase_and_opendirat(rfd, *s, CHASE_AT_RESOLVE_IN_ROOT, &search_path, &d);
+                r = chase_and_opendirat(rfd, rfd, *s, /* chase_flags= */ 0, &search_path, &d);
                 if (r == -ENOENT)
                         continue;
                 if (r < 0)
@@ -907,7 +907,7 @@ int image_find(RuntimeScope scope,
                                 return -ENOMEM;
 
                         /* Follow symlinks only inside given root */
-                        r = chaseat(rfd, fname_path, CHASE_AT_RESOLVE_IN_ROOT, &chased_path, &fd);
+                        r = chaseat(rfd, rfd, fname_path, /* flags= */ 0, &chased_path, &fd);
                         if (r == -ENOENT)
                                 continue;
                         if (r < 0)
@@ -1097,7 +1097,7 @@ int image_discover(
                 _cleanup_closedir_ DIR *d = NULL;
                 _cleanup_free_ char *search_path = NULL;
 
-                r = chase_and_opendirat(rfd, *s, CHASE_AT_RESOLVE_IN_ROOT, &search_path, &d);
+                r = chase_and_opendirat(rfd, rfd, *s, /* chase_flags= */ 0, &search_path, &d);
                 if (r == -ENOENT)
                         continue;
                 if (r < 0)
@@ -1121,7 +1121,7 @@ int image_discover(
                                 return -ENOMEM;
 
                         /* Follow symlinks only inside given root */
-                        r = chaseat(rfd, fname_path, CHASE_AT_RESOLVE_IN_ROOT, &chased_path, &fd);
+                        r = chaseat(rfd, rfd, fname_path, /* flags= */ 0, &chased_path, &fd);
                         if (r == -ENOENT)
                                 continue;
                         if (r < 0)
index d29719785feddf8af800d95bba350bd6c935e6aa..3497ca6da3c6724914a2c01f6d09f454039bef04 100644 (file)
@@ -337,7 +337,7 @@ static int verify_esp(
 
         _cleanup_free_ char *p = NULL;
         _cleanup_close_ int fd = -EBADF;
-        r = chaseat(rfd, path, CHASE_AT_RESOLVE_IN_ROOT|CHASE_TRIGGER_AUTOFS, &p, &fd);
+        r = chaseat(rfd, rfd, path, CHASE_TRIGGER_AUTOFS, &p, &fd);
         if (r < 0)
                 return log_full_errno((searching && r == -ENOENT) ||
                                       (unprivileged_mode && ERRNO_IS_PRIVILEGE(r)) ? LOG_DEBUG : LOG_ERR,
@@ -441,7 +441,7 @@ int find_esp_and_warn_at_full(
                                                "$SYSTEMD_ESP_PATH does not refer to an absolute path, refusing to use it: \"%s\"",
                                                path);
 
-                r = chaseat(rfd, path, CHASE_AT_RESOLVE_IN_ROOT|CHASE_TRIGGER_AUTOFS, &p, &fd);
+                r = chaseat(rfd, rfd, path, CHASE_TRIGGER_AUTOFS, &p, &fd);
                 if (r < 0)
                         return log_error_errno(r, "Failed to resolve path \"%s\": %m", path);
 
@@ -735,7 +735,7 @@ static int verify_xbootldr(
 
         _cleanup_free_ char *p = NULL;
         _cleanup_close_ int fd = -EBADF;
-        r = chaseat(rfd, path, CHASE_AT_RESOLVE_IN_ROOT|CHASE_TRIGGER_AUTOFS, &p, &fd);
+        r = chaseat(rfd, rfd, path, CHASE_TRIGGER_AUTOFS, &p, &fd);
         if (r < 0)
                 return log_full_errno((searching && r == -ENOENT) ||
                                       (unprivileged_mode && ERRNO_IS_PRIVILEGE(r)) ? LOG_DEBUG : LOG_ERR,
@@ -809,7 +809,7 @@ int find_xbootldr_and_warn_at_full(
                                                "$SYSTEMD_XBOOTLDR_PATH does not refer to an absolute path, refusing to use it: \"%s\"",
                                                path);
 
-                r = chaseat(rfd, path, CHASE_AT_RESOLVE_IN_ROOT|CHASE_TRIGGER_AUTOFS, &p, &fd);
+                r = chaseat(rfd, rfd, path, CHASE_TRIGGER_AUTOFS, &p, &fd);
                 if (r < 0)
                         return log_error_errno(r, "Failed to resolve path \"%s\": %m", p);
 
index 32e95fd23096e6cb76684239a22d2178ccdc1947..cc3cb9a75f7cbae5a4eb0b20c7ff486fab5712f5 100644 (file)
@@ -1032,7 +1032,7 @@ int mstack_bind_mounts(
 
         if (mstack->usr_mount_fd >= 0) {
                 _cleanup_close_ int subdir_fd = -EBADF;
-                r = chaseat(root_fd, "usr", CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, /* ret_path= */ NULL, &subdir_fd);
+                r = chaseat(root_fd, root_fd, "usr", CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, /* ret_path= */ NULL, &subdir_fd);
                 if (r < 0)
                         return log_debug_errno(r, "Failed to open mount point inode '%s': %m", where);
 
@@ -1051,7 +1051,7 @@ int mstack_bind_mounts(
                 assert(m->mount_fd >= 0);
 
                 _cleanup_close_ int subdir_fd = -EBADF;
-                r = chaseat(root_fd, m->where, CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, /* ret_path= */ NULL, &subdir_fd);
+                r = chaseat(root_fd, root_fd, m->where, CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, /* ret_path= */ NULL, &subdir_fd);
                 if (r < 0)
                         return log_debug_errno(r, "Failed to open mount point inode '%s': %m", m->where);
 
index 3f7dc88a6a9af8dab9b7eb884ba900ecf9339733..dad6337b7588ccf51e67581a897f9682cc031372 100644 (file)
@@ -922,8 +922,8 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) {
                                                                 "Invalid hardlink path name '%s' in entry, refusing.", target);
 
                                         _cleanup_close_ int target_fd = -EBADF;
-                                        r = chaseat(tree_fd, target,
-                                                    CHASE_PROHIBIT_SYMLINKS|CHASE_AT_RESOLVE_IN_ROOT|CHASE_NOFOLLOW,
+                                        r = chaseat(tree_fd, tree_fd, target,
+                                                    CHASE_PROHIBIT_SYMLINKS|CHASE_NOFOLLOW,
                                                     /* ret_path= */ NULL, &target_fd);
                                         if (r < 0)
                                                 return log_error_errno(
@@ -953,8 +953,8 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) {
 
                                                 _cleanup_close_ int target_parent_fd = -EBADF;
                                                 _cleanup_free_ char *target_filename = NULL;
-                                                r = chaseat(tree_fd, target,
-                                                            CHASE_PROHIBIT_SYMLINKS|CHASE_AT_RESOLVE_IN_ROOT|CHASE_PARENT|CHASE_EXTRACT_FILENAME|CHASE_NOFOLLOW,
+                                                r = chaseat(tree_fd, tree_fd, target,
+                                                            CHASE_PROHIBIT_SYMLINKS|CHASE_PARENT|CHASE_EXTRACT_FILENAME|CHASE_NOFOLLOW,
                                                             &target_filename, &target_parent_fd);
                                                 if (r < 0)
                                                         return log_error_errno(r, "Failed to find inode '%s' which shall be hardlinked as '%s': %m",
index adf1b0f689b030cea4734b3c458f157e2f2fe0d4..9a2b7c0412825f9d324e0c6756ad5f0d01212cf9 100644 (file)
@@ -477,6 +477,18 @@ _noreturn_ void log_test_failed_internal(const char *file, int line, const char
         })
 #endif
 
+#ifdef __COVERITY__
+#  define ASSERT_PATH_EQ(expr1, expr2) __coverity_check__(path_equal((expr1), (expr2)))
+#else
+#  define ASSERT_PATH_EQ(expr1, expr2)                                                                          \
+        ({                                                                                                      \
+                const char *_expr1 = (expr1), *_expr2 = (expr2);                                                \
+                if (!path_equal(_expr1, _expr2))                                                                 \
+                        log_test_failed("Expected \"%s == %s\", got \"%s != %s\"",                              \
+                                        #expr1, #expr2, strnull(_expr1), strnull(_expr2));                      \
+        })
+#endif
+
 #ifdef __COVERITY__
 #  define ASSERT_NOT_STREQ(expr1, expr2) __coverity_check__(!streq_ptr((expr1), (expr2)))
 #else
index 38ceb225cd5ae37dd99feec87283e64a0ca209d5..b68991cda19b67d93884ec1f7002c68b66815637 100644 (file)
@@ -197,8 +197,9 @@ static int pin_choice(
 
         if (inode_fd < 0 || FLAGS_SET(flags, PICK_RESOLVE)) {
                 r = chaseat(toplevel_fd,
+                            toplevel_fd,
                             inode_path,
-                            CHASE_AT_RESOLVE_IN_ROOT,
+                            /* flags= */ 0,
                             FLAGS_SET(flags, PICK_RESOLVE) ? &resolved_path : NULL,
                             inode_fd < 0 ? &inode_fd : NULL);
                 if (r < 0)
@@ -327,7 +328,7 @@ static int make_choice(
         assert(ret);
 
         if (inode_fd < 0) {
-                r = chaseat(toplevel_fd, inode_path, CHASE_AT_RESOLVE_IN_ROOT, NULL, &inode_fd);
+                r = chaseat(toplevel_fd, toplevel_fd, inode_path, /* flags= */ 0, NULL, &inode_fd);
                 if (r < 0)
                         return r;
         }
@@ -344,7 +345,7 @@ static int make_choice(
                         return log_oom_debug();
 
                 _cleanup_close_ int object_fd = -EBADF;
-                r = chaseat(toplevel_fd, p, CHASE_AT_RESOLVE_IN_ROOT, &object_path, &object_fd);
+                r = chaseat(toplevel_fd, toplevel_fd, p, /* flags= */ 0, &object_path, &object_fd);
                 if (r == -ENOENT) {
                         *ret = PICK_RESULT_NULL;
                         return 0;
index 0ac35d1b56b8d78d62977e64db40df9fab532019..0475de3e6e9962b517247ce708524202db1cb326 100644 (file)
@@ -1056,7 +1056,7 @@ static int resolve_mutable_directory(
                 /* This also creates, e.g., /var/lib/extensions.mutable/usr if needed and all parent
                  * directories plus it also works when the last part is a symlink to the real /usr but we
                  * can't use chase_and_open here because it does not behave the same. */
-                r = chase(path, root, CHASE_AT_RESOLVE_IN_ROOT|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY|CHASE_PREFIX_ROOT, /* ret_path */ NULL, &path_fd);
+                r = chase(path, root, CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY|CHASE_PREFIX_ROOT, /* ret_path */ NULL, &path_fd);
                 if (r < 0)
                         return log_error_errno(r, "Failed to chase/create base directory '%s/%s': %m", strempty(root), skip_leading_slash(path));
 
index 721f56a250663087fb576cfade9525f7ab25dddf..ed07ac5cef68df2df7302956a0733bf0e1950546 100644 (file)
@@ -27,10 +27,10 @@ static void test_chase_extract_filename_one(const char *path, const char *root,
 
         log_debug("/* %s(path=%s, root=%s) */", __func__, path, strnull(root));
 
-        assert_se(chase(path, root, CHASE_EXTRACT_FILENAME, &ret1, NULL) > 0);
+        ASSERT_OK_POSITIVE(chase(path, root, CHASE_EXTRACT_FILENAME, &ret1, NULL));
         ASSERT_STREQ(ret1, expected);
 
-        assert_se(chase(path, root, 0, &ret2, NULL) > 0);
+        ASSERT_OK_POSITIVE(chase(path, root, 0, &ret2, NULL));
         ASSERT_OK(chase_extract_filename(ret2, root, &fname));
         ASSERT_STREQ(fname, expected);
 }
@@ -41,97 +41,84 @@ TEST(chase) {
         char *temp;
         const char *top, *p, *pslash, *q, *qslash;
         struct stat st;
-        int r;
 
         temp = strjoina(arg_test_dir ?: "/tmp", "/test-chase.XXXXXX");
-        assert_se(mkdtemp(temp));
+        ASSERT_NOT_NULL(mkdtemp(temp));
 
         top = strjoina(temp, "/top");
         ASSERT_OK(mkdir(top, 0700));
 
         p = strjoina(top, "/dot");
         if (symlink(".", p) < 0) {
-                assert_se(IN_SET(errno, EINVAL, ENOSYS, ENOTTY, EPERM));
+                ASSERT_TRUE(IN_SET(errno, EINVAL, ENOSYS, ENOTTY, EPERM));
                 log_tests_skipped_errno(errno, "symlink() not possible");
                 goto cleanup;
         };
 
         p = strjoina(top, "/dotdot");
-        ASSERT_OK(symlink("..", p));
+        ASSERT_OK_ERRNO(symlink("..", p));
 
         p = strjoina(top, "/dotdota");
-        ASSERT_OK(symlink("../a", p));
+        ASSERT_OK_ERRNO(symlink("../a", p));
 
         p = strjoina(temp, "/a");
-        ASSERT_OK(symlink("b", p));
+        ASSERT_OK_ERRNO(symlink("b", p));
 
         p = strjoina(temp, "/b");
-        ASSERT_OK(symlink("/usr", p));
+        ASSERT_OK_ERRNO(symlink("/usr", p));
 
         p = strjoina(temp, "/start");
-        ASSERT_OK(symlink("top/dot/dotdota", p));
+        ASSERT_OK_ERRNO(symlink("top/dot/dotdota", p));
 
         /* Paths that use symlinks underneath the "root" */
 
-        r = chase(p, NULL, 0, &result, NULL);
-        assert_se(r > 0);
-        assert_se(path_equal(result, "/usr"));
+        ASSERT_OK_POSITIVE(chase(p, NULL, 0, &result, NULL));
+        ASSERT_PATH_EQ(result, "/usr");
         result = mfree(result);
 
-        r = chase(p, "/.//../../../", 0, &result, NULL);
-        assert_se(r > 0);
-        assert_se(path_equal(result, "/usr"));
+        ASSERT_OK_POSITIVE(chase(p, "/.//../../../", 0, &result, NULL));
+        ASSERT_PATH_EQ(result, "/usr");
         result = mfree(result);
 
         pslash = strjoina(p, "/");
-        r = chase(pslash, NULL, 0, &result, NULL);
-        assert_se(r > 0);
-        assert_se(path_equal(result, "/usr/"));
+        ASSERT_OK_POSITIVE(chase(pslash, NULL, 0, &result, NULL));
+        ASSERT_PATH_EQ(result, "/usr/");
         result = mfree(result);
 
-        r = chase(p, temp, 0, &result, NULL);
-        assert_se(r == -ENOENT);
-
-        r = chase(pslash, temp, 0, &result, NULL);
-        assert_se(r == -ENOENT);
+        ASSERT_ERROR(chase(p, temp, 0, &result, NULL), ENOENT);
+        ASSERT_ERROR(chase(pslash, temp, 0, &result, NULL), ENOENT);
 
         q = strjoina(temp, "/usr");
 
-        r = chase(p, temp, CHASE_NONEXISTENT, &result, NULL);
-        assert_se(r == 0);
-        assert_se(path_equal(result, q));
+        ASSERT_OK_ZERO(chase(p, temp, CHASE_NONEXISTENT, &result, NULL));
+        ASSERT_PATH_EQ(result, q);
         result = mfree(result);
 
         qslash = strjoina(q, "/");
 
-        r = chase(pslash, temp, CHASE_NONEXISTENT, &result, NULL);
-        assert_se(r == 0);
-        assert_se(path_equal(result, qslash));
+        ASSERT_OK_ZERO(chase(pslash, temp, CHASE_NONEXISTENT, &result, NULL));
+        ASSERT_PATH_EQ(result, qslash);
         result = mfree(result);
 
-        ASSERT_OK(mkdir(q, 0700));
+        ASSERT_OK_ERRNO(mkdir(q, 0700));
 
-        r = chase(p, temp, 0, &result, NULL);
-        assert_se(r > 0);
-        assert_se(path_equal(result, q));
+        ASSERT_OK_POSITIVE(chase(p, temp, 0, &result, NULL));
+        ASSERT_PATH_EQ(result, q);
         result = mfree(result);
 
-        r = chase(pslash, temp, 0, &result, NULL);
-        assert_se(r > 0);
-        assert_se(path_equal(result, qslash));
+        ASSERT_OK_POSITIVE(chase(pslash, temp, 0, &result, NULL));
+        ASSERT_PATH_EQ(result, qslash);
         result = mfree(result);
 
         p = strjoina(temp, "/slash");
-        assert_se(symlink("/", p) >= 0);
+        ASSERT_OK_ERRNO(symlink("/", p));
 
-        r = chase(p, NULL, 0, &result, NULL);
-        assert_se(r > 0);
-        assert_se(path_equal(result, "/"));
+        ASSERT_OK_POSITIVE(chase(p, NULL, 0, &result, NULL));
+        ASSERT_PATH_EQ(result, "/");
         result = mfree(result);
 
-        r = chase(p, temp, 0, &result, NULL);
-        assert_se(r > 0);
-        assert_se(path_equal(result, temp));
+        ASSERT_OK_POSITIVE(chase(p, temp, 0, &result, NULL));
+        ASSERT_PATH_EQ(result, temp);
         result = mfree(result);
 
         /* Tests for CHASE_EXTRACT_FILENAME and chase_extract_filename() */
@@ -150,205 +137,182 @@ TEST(chase) {
         /* Paths that would "escape" outside of the "root" */
 
         p = strjoina(temp, "/6dots");
-        ASSERT_OK(symlink("../../..", p));
+        ASSERT_OK_ERRNO(symlink("../../..", p));
 
-        r = chase(p, temp, 0, &result, NULL);
-        assert_se(r > 0 && path_equal(result, temp));
+        ASSERT_OK_POSITIVE(chase(p, temp, 0, &result, NULL));
+        ASSERT_PATH_EQ(result, temp);
         result = mfree(result);
 
         p = strjoina(temp, "/6dotsusr");
-        ASSERT_OK(symlink("../../../usr", p));
+        ASSERT_OK_ERRNO(symlink("../../../usr", p));
 
-        r = chase(p, temp, 0, &result, NULL);
-        assert_se(r > 0 && path_equal(result, q));
+        ASSERT_OK_POSITIVE(chase(p, temp, 0, &result, NULL));
+        ASSERT_PATH_EQ(result, q);
         result = mfree(result);
 
         p = strjoina(temp, "/top/8dotsusr");
-        ASSERT_OK(symlink("../../../../usr", p));
+        ASSERT_OK_ERRNO(symlink("../../../../usr", p));
 
-        r = chase(p, temp, 0, &result, NULL);
-        assert_se(r > 0 && path_equal(result, q));
+        ASSERT_OK_POSITIVE(chase(p, temp, 0, &result, NULL));
+        ASSERT_PATH_EQ(result, q);
         result = mfree(result);
 
         /* Paths that contain repeated slashes */
 
         p = strjoina(temp, "/slashslash");
-        ASSERT_OK(symlink("///usr///", p));
+        ASSERT_OK_ERRNO(symlink("///usr///", p));
 
-        r = chase(p, NULL, 0, &result, NULL);
-        assert_se(r > 0);
-        assert_se(path_equal(result, "/usr"));
+        ASSERT_OK_POSITIVE(chase(p, NULL, 0, &result, NULL));
+        ASSERT_PATH_EQ(result, "/usr");
         ASSERT_STREQ(result, "/usr"); /* we guarantee that we drop redundant slashes */
         result = mfree(result);
 
-        r = chase(p, temp, 0, &result, NULL);
-        assert_se(r > 0);
-        assert_se(path_equal(result, q));
+        ASSERT_OK_POSITIVE(chase(p, temp, 0, &result, NULL));
+        ASSERT_PATH_EQ(result, q);
         result = mfree(result);
 
         /* Paths underneath the "root" with different UIDs while using CHASE_SAFE */
 
         if (geteuid() == 0 && !userns_has_single_user()) {
                 p = strjoina(temp, "/user");
-                ASSERT_OK(mkdir(p, 0755));
-                ASSERT_OK(chown(p, UID_NOBODY, GID_NOBODY));
+                ASSERT_OK_ERRNO(mkdir(p, 0755));
+                ASSERT_OK_ERRNO(chown(p, UID_NOBODY, GID_NOBODY));
 
                 q = strjoina(temp, "/user/root");
-                ASSERT_OK(mkdir(q, 0755));
+                ASSERT_OK_ERRNO(mkdir(q, 0755));
 
                 p = strjoina(q, "/link");
-                ASSERT_OK(symlink("/", p));
+                ASSERT_OK_ERRNO(symlink("/", p));
 
                 /* Fail when user-owned directories contain root-owned subdirectories. */
-                r = chase(p, temp, CHASE_SAFE, &result, NULL);
-                assert_se(r == -ENOLINK);
+                ASSERT_ERROR(chase(p, temp, CHASE_SAFE, &result, NULL), ENOLINK);
                 result = mfree(result);
 
                 /* Allow this when the user-owned directories are all in the "root". */
-                r = chase(p, q, CHASE_SAFE, &result, NULL);
-                assert_se(r > 0);
+                ASSERT_OK_POSITIVE(chase(p, q, CHASE_SAFE, &result, NULL));
                 result = mfree(result);
         }
 
         /* Paths using . */
 
-        r = chase("/etc/./.././", NULL, 0, &result, NULL);
-        assert_se(r > 0);
-        assert_se(path_equal(result, "/"));
+        ASSERT_OK_POSITIVE(chase("/etc/./.././", NULL, 0, &result, NULL));
+        ASSERT_PATH_EQ(result, "/");
         result = mfree(result);
 
-        r = chase("/etc/./.././", "/etc", 0, &result, NULL);
-        assert_se(r > 0 && path_equal(result, "/etc"));
+        ASSERT_OK_POSITIVE(chase("/etc/./.././", "/etc", 0, &result, NULL));
+        ASSERT_PATH_EQ(result, "/etc");
         result = mfree(result);
 
-        r = chase("/../.././//../../etc", NULL, 0, &result, NULL);
-        assert_se(r > 0);
+        ASSERT_OK_POSITIVE(chase("/../.././//../../etc", NULL, 0, &result, NULL));
         ASSERT_STREQ(result, "/etc");
         result = mfree(result);
 
-        r = chase("/../.././//../../test-chase.fsldajfl", NULL, CHASE_NONEXISTENT, &result, NULL);
-        assert_se(r == 0);
+        ASSERT_OK_ZERO(chase("/../.././//../../test-chase.fsldajfl", NULL, CHASE_NONEXISTENT, &result, NULL));
         ASSERT_STREQ(result, "/test-chase.fsldajfl");
         result = mfree(result);
 
-        r = chase("/../.././//../../etc", "/", CHASE_PREFIX_ROOT, &result, NULL);
-        assert_se(r > 0);
+        ASSERT_OK_POSITIVE(chase("/../.././//../../etc", "/", CHASE_PREFIX_ROOT, &result, NULL));
         ASSERT_STREQ(result, "/etc");
         result = mfree(result);
 
-        r = chase("/../.././//../../test-chase.fsldajfl", "/", CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &result, NULL);
-        assert_se(r == 0);
+        ASSERT_OK_ZERO(chase("/../.././//../../test-chase.fsldajfl", "/", CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &result, NULL));
         ASSERT_STREQ(result, "/test-chase.fsldajfl");
         result = mfree(result);
 
-        r = chase("/.path/with/dot", temp, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &result, NULL);
-        ASSERT_OK(r);
+        ASSERT_OK(chase("/.path/with/dot", temp, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &result, NULL));
         q = strjoina(temp, "/.path/with/dot");
         ASSERT_STREQ(result, q);
         result = mfree(result);
 
-        r = chase("/etc/machine-id/foo", NULL, 0, &result, NULL);
-        assert_se(IN_SET(r, -ENOTDIR, -ENOENT));
+        ASSERT_TRUE(IN_SET(chase("/etc/machine-id/foo", NULL, 0, &result, NULL), -ENOTDIR, -ENOENT));
         result = mfree(result);
 
         /* Path that loops back to self */
 
         p = strjoina(temp, "/recursive-symlink");
-        ASSERT_OK(symlink("recursive-symlink", p));
-        r = chase(p, NULL, 0, &result, NULL);
-        assert_se(r == -ELOOP);
+        ASSERT_OK_ERRNO(symlink("recursive-symlink", p));
+        ASSERT_ERROR(chase(p, NULL, 0, &result, NULL), ELOOP);
 
         /* Path which doesn't exist */
 
         p = strjoina(temp, "/idontexist");
-        r = chase(p, NULL, 0, &result, NULL);
-        assert_se(r == -ENOENT);
-
-        r = chase(p, NULL, CHASE_NONEXISTENT, &result, NULL);
-        assert_se(r == 0);
-        assert_se(path_equal(result, p));
+        ASSERT_ERROR(chase(p, NULL, 0, &result, NULL), ENOENT);
+        ASSERT_OK_ZERO(chase(p, NULL, CHASE_NONEXISTENT, &result, NULL));
+        ASSERT_PATH_EQ(result, p);
         result = mfree(result);
 
         p = strjoina(temp, "/idontexist/meneither");
-        r = chase(p, NULL, 0, &result, NULL);
-        assert_se(r == -ENOENT);
+        ASSERT_ERROR(chase(p, NULL, 0, &result, NULL), ENOENT);
 
-        r = chase(p, NULL, CHASE_NONEXISTENT, &result, NULL);
-        assert_se(r == 0);
-        assert_se(path_equal(result, p));
+        ASSERT_OK_ZERO(chase(p, NULL, CHASE_NONEXISTENT, &result, NULL));
+        ASSERT_PATH_EQ(result, p);
         result = mfree(result);
 
         /* Relative paths */
 
         ASSERT_OK(safe_getcwd(&pwd));
 
-        ASSERT_OK(chdir(temp));
+        ASSERT_OK_ERRNO(chdir(temp));
 
         p = "this/is/a/relative/path";
-        r = chase(p, NULL, CHASE_NONEXISTENT, &result, NULL);
-        assert_se(r == 0);
+        ASSERT_OK_ZERO(chase(p, NULL, CHASE_NONEXISTENT, &result, NULL));
 
         p = strjoina(temp, "/", p);
-        assert_se(path_equal(result, p));
+        ASSERT_PATH_EQ(result, p);
         result = mfree(result);
 
         p = "this/is/a/relative/path";
-        r = chase(p, temp, CHASE_NONEXISTENT, &result, NULL);
-        assert_se(r == 0);
+        ASSERT_OK_ZERO(chase(p, temp, CHASE_NONEXISTENT, &result, NULL));
 
         p = strjoina(temp, "/", p);
-        assert_se(path_equal(result, p));
+        ASSERT_PATH_EQ(result, p);
         result = mfree(result);
 
-        assert_se(chdir(pwd) >= 0);
+        ASSERT_OK_ERRNO(chdir(pwd));
 
         /* Path which doesn't exist, but contains weird stuff */
 
         p = strjoina(temp, "/idontexist/..");
-        r = chase(p, NULL, 0, &result, NULL);
-        assert_se(r == -ENOENT);
+        ASSERT_ERROR(chase(p, NULL, 0, &result, NULL), ENOENT);
 
-        r = chase(p, NULL, CHASE_NONEXISTENT, &result, NULL);
-        assert_se(r == -ENOENT);
+        ASSERT_ERROR(chase(p, NULL, CHASE_NONEXISTENT, &result, NULL), ENOENT);
 
         p = strjoina(temp, "/target");
         q = strjoina(temp, "/top");
-        assert_se(symlink(q, p) >= 0);
+        ASSERT_OK_ERRNO(symlink(q, p));
         p = strjoina(temp, "/target/idontexist");
-        r = chase(p, NULL, 0, &result, NULL);
-        assert_se(r == -ENOENT);
+        ASSERT_ERROR(chase(p, NULL, 0, &result, NULL), ENOENT);
 
         if (geteuid() == 0 && !userns_has_single_user()) {
                 p = strjoina(temp, "/priv1");
-                ASSERT_OK(mkdir(p, 0755));
+                ASSERT_OK_ERRNO(mkdir(p, 0755));
 
                 q = strjoina(p, "/priv2");
-                ASSERT_OK(mkdir(q, 0755));
+                ASSERT_OK_ERRNO(mkdir(q, 0755));
 
                 ASSERT_OK(chase(q, NULL, CHASE_SAFE, NULL, NULL));
 
-                ASSERT_OK(chown(q, UID_NOBODY, GID_NOBODY));
+                ASSERT_OK_ERRNO(chown(q, UID_NOBODY, GID_NOBODY));
                 ASSERT_OK(chase(q, NULL, CHASE_SAFE, NULL, NULL));
 
                 ASSERT_OK(chown(p, UID_NOBODY, GID_NOBODY));
                 ASSERT_OK(chase(q, NULL, CHASE_SAFE, NULL, NULL));
 
-                assert_se(chown(q, 0, 0) >= 0);
-                assert_se(chase(q, NULL, CHASE_SAFE, NULL, NULL) == -ENOLINK);
+                ASSERT_OK_ERRNO(chown(q, 0, 0));
+                ASSERT_ERROR(chase(q, NULL, CHASE_SAFE, NULL, NULL), ENOLINK);
 
-                ASSERT_OK(rmdir(q));
-                ASSERT_OK(symlink("/etc/passwd", q));
-                assert_se(chase(q, NULL, CHASE_SAFE, NULL, NULL) == -ENOLINK);
+                ASSERT_OK_ERRNO(rmdir(q));
+                ASSERT_OK_ERRNO(symlink("/etc/passwd", q));
+                ASSERT_ERROR(chase(q, NULL, CHASE_SAFE, NULL, NULL), ENOLINK);
 
-                assert_se(chown(p, 0, 0) >= 0);
+                ASSERT_OK_ERRNO(chown(p, 0, 0));
                 ASSERT_OK(chase(q, NULL, CHASE_SAFE, NULL, NULL));
         }
 
         p = strjoina(temp, "/machine-id-test");
-        ASSERT_OK(symlink("/usr/../etc/./machine-id", p));
+        ASSERT_OK_ERRNO(symlink("/usr/../etc/./machine-id", p));
 
-        r = chase(p, NULL, 0, NULL, &pfd);
-        if (r != -ENOENT && sd_id128_get_machine(NULL) >= 0) {
+        if (chase(p, NULL, 0, NULL, &pfd) != -ENOENT && sd_id128_get_machine(NULL) >= 0) {
                 _cleanup_close_ int fd = -EBADF;
                 sd_id128_t a, b;
 
@@ -360,112 +324,187 @@ TEST(chase) {
 
                 ASSERT_OK(id128_read_fd(fd, ID128_FORMAT_PLAIN, &a));
                 ASSERT_OK(sd_id128_get_machine(&b));
-                assert_se(sd_id128_equal(a, b));
+                ASSERT_TRUE(sd_id128_equal(a, b));
         }
 
-        assert_se(lstat(p, &st) >= 0);
-        r = chase_and_unlink(p, NULL, 0, 0, &result);
-        assert_se(r == 0);
-        assert_se(path_equal(result, p));
+        ASSERT_OK_ERRNO(lstat(p, &st));
+        ASSERT_OK_ZERO(chase_and_unlink(p, NULL, 0, 0, &result));
+        ASSERT_PATH_EQ(result, p);
         result = mfree(result);
-        assert_se(lstat(p, &st) == -1 && errno == ENOENT);
+        ASSERT_ERROR_ERRNO(lstat(p, &st), ENOENT);
 
         /* Test CHASE_NOFOLLOW */
 
         p = strjoina(temp, "/target");
         q = strjoina(temp, "/symlink");
-        assert_se(symlink(p, q) >= 0);
-        r = chase(q, NULL, CHASE_NOFOLLOW, &result, &pfd);
-        ASSERT_OK(r);
-        ASSERT_OK(pfd);
-        assert_se(path_equal(result, q));
-        ASSERT_OK(fstat(pfd, &st));
-        assert_se(S_ISLNK(st.st_mode));
+        ASSERT_OK_ERRNO(symlink(p, q));
+        ASSERT_OK(chase(q, NULL, CHASE_NOFOLLOW, &result, &pfd));
+        ASSERT_PATH_EQ(result, q);
+        ASSERT_OK_ERRNO(fstat(pfd, &st));
+        ASSERT_TRUE(S_ISLNK(st.st_mode));
         result = mfree(result);
         pfd = safe_close(pfd);
 
         /* s1 -> s2 -> nonexistent */
         q = strjoina(temp, "/s1");
-        ASSERT_OK(symlink("s2", q));
+        ASSERT_OK_ERRNO(symlink("s2", q));
         p = strjoina(temp, "/s2");
-        ASSERT_OK(symlink("nonexistent", p));
-        r = chase(q, NULL, CHASE_NOFOLLOW, &result, &pfd);
-        ASSERT_OK(r);
-        ASSERT_OK(pfd);
-        assert_se(path_equal(result, q));
-        ASSERT_OK(fstat(pfd, &st));
-        assert_se(S_ISLNK(st.st_mode));
+        ASSERT_OK_ERRNO(symlink("nonexistent", p));
+        ASSERT_OK(chase(q, NULL, CHASE_NOFOLLOW, &result, &pfd));
+        ASSERT_PATH_EQ(result, q);
+        ASSERT_OK_ERRNO(fstat(pfd, &st));
+        ASSERT_TRUE(S_ISLNK(st.st_mode));
         result = mfree(result);
         pfd = safe_close(pfd);
 
         /* Test CHASE_STEP */
 
         p = strjoina(temp, "/start");
-        r = chase(p, NULL, CHASE_STEP, &result, NULL);
-        assert_se(r == 0);
+        ASSERT_OK_ZERO(chase(p, NULL, CHASE_STEP, &result, NULL));
         p = strjoina(temp, "/top/dot/dotdota");
         ASSERT_STREQ(p, result);
         result = mfree(result);
 
-        r = chase(p, NULL, CHASE_STEP, &result, NULL);
-        assert_se(r == 0);
+        ASSERT_OK_ZERO(chase(p, NULL, CHASE_STEP, &result, NULL));
         p = strjoina(temp, "/top/dotdota");
         ASSERT_STREQ(p, result);
         result = mfree(result);
 
-        r = chase(p, NULL, CHASE_STEP, &result, NULL);
-        assert_se(r == 0);
+        ASSERT_OK_ZERO(chase(p, NULL, CHASE_STEP, &result, NULL));
         p = strjoina(temp, "/top/../a");
         ASSERT_STREQ(p, result);
         result = mfree(result);
 
-        r = chase(p, NULL, CHASE_STEP, &result, NULL);
-        assert_se(r == 0);
+        ASSERT_OK_ZERO(chase(p, NULL, CHASE_STEP, &result, NULL));
         p = strjoina(temp, "/a");
         ASSERT_STREQ(p, result);
         result = mfree(result);
 
-        r = chase(p, NULL, CHASE_STEP, &result, NULL);
-        assert_se(r == 0);
+        ASSERT_OK_ZERO(chase(p, NULL, CHASE_STEP, &result, NULL));
         p = strjoina(temp, "/b");
         ASSERT_STREQ(p, result);
         result = mfree(result);
 
-        r = chase(p, NULL, CHASE_STEP, &result, NULL);
-        assert_se(r == 0);
+        ASSERT_OK_ZERO(chase(p, NULL, CHASE_STEP, &result, NULL));
         ASSERT_STREQ(result, "/usr");
         result = mfree(result);
 
-        r = chase("/usr", NULL, CHASE_STEP, &result, NULL);
-        assert_se(r > 0);
+        ASSERT_OK_POSITIVE(chase("/usr", NULL, CHASE_STEP, &result, NULL));
         ASSERT_STREQ(result, "/usr");
         result = mfree(result);
 
         /* Make sure that symlinks in the "root" path are not resolved, but those below are */
         p = strjoina("/etc/..", temp, "/self");
-        assert_se(symlink(".", p) >= 0);
+        ASSERT_OK_ERRNO(symlink(".", p));
         q = strjoina(p, "/top/dot/dotdota");
-        r = chase(q, p, 0, &result, NULL);
-        assert_se(r > 0);
-        assert_se(path_equal(path_startswith(result, p), "usr"));
+        ASSERT_OK_POSITIVE(chase(q, p, 0, &result, NULL));
+        ASSERT_PATH_EQ(path_startswith(result, p), "usr");
         result = mfree(result);
 
         /* Test CHASE_PROHIBIT_SYMLINKS */
 
-        assert_se(chase("top/dot", temp, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, NULL, NULL) == -EREMCHG);
-        assert_se(chase("top/dot", temp, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_WARN, NULL, NULL) == -EREMCHG);
-        assert_se(chase("top/dotdot", temp, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, NULL, NULL) == -EREMCHG);
-        assert_se(chase("top/dotdot", temp, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_WARN, NULL, NULL) == -EREMCHG);
-        assert_se(chase("top/dot/dot", temp, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, NULL, NULL) == -EREMCHG);
-        assert_se(chase("top/dot/dot", temp, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_WARN, NULL, NULL) == -EREMCHG);
+        ASSERT_ERROR(chase("top/dot", temp, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, NULL, NULL), EREMCHG);
+        ASSERT_ERROR(chase("top/dot", temp, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_WARN, NULL, NULL), EREMCHG);
+        ASSERT_ERROR(chase("top/dotdot", temp, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, NULL, NULL), EREMCHG);
+        ASSERT_ERROR(chase("top/dotdot", temp, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_WARN, NULL, NULL), EREMCHG);
+        ASSERT_ERROR(chase("top/dot/dot", temp, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, NULL, NULL), EREMCHG);
+        ASSERT_ERROR(chase("top/dot/dot", temp, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_WARN, NULL, NULL), EREMCHG);
 
  cleanup:
         ASSERT_OK(rm_rf(temp, REMOVE_ROOT|REMOVE_PHYSICAL));
 }
 
+TEST(chase_and_open) {
+        _cleanup_free_ char *result = NULL;
+        _cleanup_close_ int fd = -EBADF;
+
+        /* Test chase_and_open() with various CHASE_PARENT / CHASE_EXTRACT_FILENAME combinations. */
+
+        /* No CHASE_PARENT, no CHASE_EXTRACT_FILENAME, with ret_path — opens the target, returns full path. */
+        fd = ASSERT_OK(chase_and_open("/usr/lib", NULL, 0, O_PATH|O_DIRECTORY|O_CLOEXEC, &result));
+        ASSERT_OK(fd_verify_directory(fd));
+        ASSERT_STREQ(result, "/usr/lib");
+        fd = safe_close(fd);
+        result = mfree(result);
+
+        /* CHASE_PARENT with ret_path — opens parent dir, returns full path including final component. */
+        fd = ASSERT_OK(chase_and_open("/usr/lib", NULL, CHASE_PARENT, O_PATH|O_DIRECTORY|O_CLOEXEC, &result));
+        ASSERT_OK(fd_verify_directory(fd));
+        ASSERT_OK_ERRNO(faccessat(fd, "lib", F_OK, 0));
+        ASSERT_STREQ(result, "/usr/lib");
+        fd = safe_close(fd);
+        result = mfree(result);
+
+        /* CHASE_PARENT|CHASE_EXTRACT_FILENAME — opens parent dir, returns just the filename. */
+        fd = ASSERT_OK(chase_and_open("/usr/lib", NULL, CHASE_PARENT|CHASE_EXTRACT_FILENAME, O_PATH|O_DIRECTORY|O_CLOEXEC, &result));
+        ASSERT_OK(fd_verify_directory(fd));
+        ASSERT_OK_ERRNO(faccessat(fd, "lib", F_OK, 0));
+        ASSERT_STREQ(result, "lib");
+        fd = safe_close(fd);
+        result = mfree(result);
+
+        /* CHASE_EXTRACT_FILENAME only — opens the target itself, returns just the filename. */
+        fd = ASSERT_OK(chase_and_open("/usr/lib", NULL, CHASE_EXTRACT_FILENAME, O_PATH|O_DIRECTORY|O_CLOEXEC, &result));
+        ASSERT_OK(fd_verify_directory(fd));
+        ASSERT_STREQ(result, "lib");
+        fd = safe_close(fd);
+        result = mfree(result);
+
+        /* CHASE_EXTRACT_FILENAME on a regular file (regression test for a bug where chase_and_open()
+         * reopened the parent directory instead of the target file). */
+        fd = ASSERT_OK(chase_and_open("/etc/os-release", NULL, CHASE_EXTRACT_FILENAME, O_PATH|O_CLOEXEC, &result));
+        ASSERT_STREQ(result, "os-release");
+        ASSERT_OK(fd_verify_regular(fd));
+        fd = safe_close(fd);
+        result = mfree(result);
+
+        /* CHASE_PARENT through a symlink — symlink is followed, parent of the target is opened. */
+        fd = ASSERT_OK(chase_and_open("/etc/os-release", NULL, CHASE_PARENT, O_PATH|O_DIRECTORY|O_CLOEXEC, &result));
+        ASSERT_OK(fd_verify_directory(fd));
+        ASSERT_NOT_NULL(result);
+        fd = safe_close(fd);
+        result = mfree(result);
+
+        /* CHASE_PARENT|CHASE_NOFOLLOW through a symlink — symlink is NOT followed, parent of the
+         * symlink is opened. */
+        fd = ASSERT_OK(chase_and_open("/etc/os-release", NULL, CHASE_PARENT|CHASE_NOFOLLOW, O_PATH|O_DIRECTORY|O_CLOEXEC, &result));
+        ASSERT_OK(fd_verify_directory(fd));
+        ASSERT_OK_ERRNO(faccessat(fd, "os-release", F_OK, AT_SYMLINK_NOFOLLOW));
+        ASSERT_STREQ(result, "/etc/os-release");
+        fd = safe_close(fd);
+        result = mfree(result);
+
+        /* CHASE_PARENT|CHASE_NOFOLLOW|CHASE_EXTRACT_FILENAME through a symlink — parent of the symlink
+         * is opened, returns just the symlink name. */
+        fd = ASSERT_OK(chase_and_open("/etc/os-release", NULL, CHASE_PARENT|CHASE_NOFOLLOW|CHASE_EXTRACT_FILENAME, O_PATH|O_DIRECTORY|O_CLOEXEC, &result));
+        ASSERT_OK(fd_verify_directory(fd));
+        ASSERT_OK_ERRNO(faccessat(fd, "os-release", F_OK, AT_SYMLINK_NOFOLLOW));
+        ASSERT_STREQ(result, "os-release");
+        fd = safe_close(fd);
+        result = mfree(result);
+
+        /* When the resolved path equals the root directory itself, the filename should be "." — not
+         * the basename of the root directory. This is the edge case that chase_extract_filename()
+         * handles by stripping the root prefix before extracting, which plain path_extract_filename()
+         * would get wrong. */
+        _cleanup_(rm_rf_physical_and_freep) char *tmpdir = NULL;
+        _cleanup_close_ int tfd = -EBADF;
+
+        tfd = ASSERT_OK(mkdtemp_open(NULL, 0, &tmpdir));
+        /* Create a symlink to "/" — when chased under tmpdir as root, it resolves to tmpdir itself. */
+        ASSERT_OK_ERRNO(symlinkat("/", tfd, "to_root"));
+
+        _cleanup_free_ char *link_path = ASSERT_NOT_NULL(path_join(tmpdir, "to_root"));
+        fd = ASSERT_OK(chase_and_open(link_path, tmpdir, 0, O_PATH|O_DIRECTORY|O_CLOEXEC, &result));
+        ASSERT_OK(fd_verify_directory(fd));
+        ASSERT_PATH_EQ(result, tmpdir);
+        fd = safe_close(fd);
+        result = mfree(result);
+}
+
 TEST(chaseat) {
         _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
-        _cleanup_close_ int tfd = -EBADF, fd = -EBADF;
+        _cleanup_close_ int tfd = -EBADF, fd = -EBADF, fd2 = -EBADF;
         _cleanup_free_ char *result = NULL;
         _cleanup_closedir_ DIR *dir = NULL;
         _cleanup_fclose_ FILE *f = NULL;
@@ -474,13 +513,13 @@ TEST(chaseat) {
 
         ASSERT_OK(tfd = mkdtemp_open(NULL, 0, &t));
 
-        /* Test that AT_FDCWD with CHASE_AT_RESOLVE_IN_ROOT resolves against / and not the current working
+        /* Test that AT_FDCWD resolves against / and not the current working
          * directory. */
 
-        ASSERT_OK(symlinkat("/usr", tfd, "abc"));
+        ASSERT_OK_ERRNO(symlinkat("/usr", tfd, "abc"));
 
         p = strjoina(t, "/abc");
-        ASSERT_OK(chaseat(AT_FDCWD, p, CHASE_AT_RESOLVE_IN_ROOT, &result, NULL));
+        ASSERT_OK(chaseat(XAT_FDROOT, AT_FDCWD, p, 0, &result, NULL));
         ASSERT_STREQ(result, "/usr");
         result = mfree(result);
 
@@ -489,11 +528,24 @@ TEST(chaseat) {
         fd = open("/", O_CLOEXEC | O_DIRECTORY | O_PATH);
         ASSERT_OK(fd);
 
-        ASSERT_OK(chaseat(fd, p, 0, &result, NULL));
+        ASSERT_OK(chaseat(XAT_FDROOT, fd, p, 0, &result, NULL));
         ASSERT_STREQ(result, "/usr");
         result = mfree(result);
 
-        ASSERT_OK(chaseat(fd, p, CHASE_AT_RESOLVE_IN_ROOT, &result, NULL));
+        /* (XAT_FDROOT, fd-of-/, relative): fd points to "/" which is the host root, so root_fd is
+         * normalized to XAT_FDROOT internally. A relative path resolves from "/". Result is absolute. */
+        ASSERT_OK(chaseat(XAT_FDROOT, fd, "usr", 0, &result, &fd2));
+        ASSERT_STREQ(result, "/usr");
+        ASSERT_TRUE(inode_same_at(fd2, NULL, AT_FDCWD, "/usr", AT_EMPTY_PATH));
+        result = mfree(result);
+        fd2 = safe_close(fd2);
+
+        /* Same without ret_path to exercise the shortcut. */
+        ASSERT_OK(chaseat(XAT_FDROOT, fd, "usr", 0, NULL, &fd2));
+        ASSERT_TRUE(inode_same_at(fd2, NULL, AT_FDCWD, "/usr", AT_EMPTY_PATH));
+        fd2 = safe_close(fd2);
+
+        ASSERT_OK(chaseat(fd, fd, p, 0, &result, NULL));
         ASSERT_STREQ(result, "/usr");
         result = mfree(result);
 
@@ -501,12 +553,12 @@ TEST(chaseat) {
 
         /* Same but with XAT_FDROOT */
         _cleanup_close_ int found_fd1 = -EBADF;
-        ASSERT_OK(chaseat(XAT_FDROOT, p, 0, &result, &found_fd1));
+        ASSERT_OK(chaseat(XAT_FDROOT, XAT_FDROOT, p, 0, &result, &found_fd1));
         ASSERT_STREQ(result, "/usr");
         result = mfree(result);
 
         _cleanup_close_ int found_fd2 = -EBADF;
-        ASSERT_OK(chaseat(XAT_FDROOT, p, CHASE_AT_RESOLVE_IN_ROOT, &result, &found_fd2));
+        ASSERT_OK(chaseat(XAT_FDROOT, XAT_FDROOT, p, 0, &result, &found_fd2));
         ASSERT_STREQ(result, "/usr");
         result = mfree(result);
         assert(fd_inode_same(found_fd1, found_fd2) > 0);
@@ -514,12 +566,12 @@ TEST(chaseat) {
         /* Do the same XAT_FDROOT tests again, this time without querying the path, so that the open_tree()
          * shortcut can work */
         _cleanup_close_ int found_fd3 = -EBADF;
-        ASSERT_OK(chaseat(XAT_FDROOT, p, 0, NULL, &found_fd3));
+        ASSERT_OK(chaseat(XAT_FDROOT, XAT_FDROOT, p, 0, NULL, &found_fd3));
         assert(fd_inode_same(found_fd1, found_fd3) > 0);
         assert(fd_inode_same(found_fd2, found_fd3) > 0);
 
         _cleanup_close_ int found_fd4 = -EBADF;
-        ASSERT_OK(chaseat(XAT_FDROOT, p, CHASE_AT_RESOLVE_IN_ROOT, NULL, &found_fd4));
+        ASSERT_OK(chaseat(XAT_FDROOT, XAT_FDROOT, p, 0, NULL, &found_fd4));
         assert(fd_inode_same(found_fd1, found_fd4) > 0);
         assert(fd_inode_same(found_fd2, found_fd4) > 0);
         assert(fd_inode_same(found_fd3, found_fd4) > 0);
@@ -529,102 +581,138 @@ TEST(chaseat) {
         found_fd3 = safe_close(found_fd3);
         found_fd4 = safe_close(found_fd4);
 
-        /* If the file descriptor does not point to the root directory, the result will be relative
-         * unless the result is outside of the specified file descriptor. */
+        /* (XAT_FDROOT, XAT_FDROOT, relative): relative path from host root. XAT_FDROOT as dir_fd
+         * redirects to root_fd which is also XAT_FDROOT (/), so "usr" resolves to /usr. Result is
+         * absolute because root_fd == XAT_FDROOT. */
+        ASSERT_OK(chaseat(XAT_FDROOT, XAT_FDROOT, "usr", 0, &result, NULL));
+        ASSERT_STREQ(result, "/usr");
+        result = mfree(result);
+
+        /* Same without ret_path so the shortcut can fire. */
+        ASSERT_OK(chaseat(XAT_FDROOT, XAT_FDROOT, "usr", 0, NULL, &fd));
+        ASSERT_TRUE(inode_same_at(fd, NULL, AT_FDCWD, "/usr", AT_EMPTY_PATH));
+        fd = safe_close(fd);
+
+        /* (XAT_FDROOT, AT_FDCWD, relative): relative path from current working directory. */
+        _cleanup_free_ char *cwd_saved = NULL;
+        ASSERT_OK(safe_getcwd(&cwd_saved));
+
+        ASSERT_OK_ERRNO(chdir(t));
 
-        ASSERT_OK(chaseat(tfd, "abc", 0, &result, NULL));
+        ASSERT_OK(chaseat(XAT_FDROOT, AT_FDCWD, "abc", 0, &result, &fd));
+        ASSERT_TRUE(inode_same_at(fd, NULL, AT_FDCWD, "/usr", AT_EMPTY_PATH));
         ASSERT_STREQ(result, "/usr");
+        fd = safe_close(fd);
+        result = mfree(result);
+
+        /* Same without ret_path to exercise the shortcut. */
+        ASSERT_OK(chaseat(XAT_FDROOT, AT_FDCWD, "abc", 0, NULL, &fd));
+        ASSERT_TRUE(inode_same_at(fd, NULL, AT_FDCWD, "/usr", AT_EMPTY_PATH));
+        fd = safe_close(fd);
+
+        /* A plain file (no symlink indirection) should also work. */
+        fd = ASSERT_OK_ERRNO(openat(tfd, "cwd_test", O_CREAT|O_CLOEXEC, 0600));
+        fd = safe_close(fd);
+
+        ASSERT_OK(chaseat(XAT_FDROOT, AT_FDCWD, "cwd_test", 0, &result, &fd));
+        ASSERT_TRUE(inode_same_at(fd, NULL, tfd, "cwd_test", AT_EMPTY_PATH));
+        fd = safe_close(fd);
         result = mfree(result);
 
-        ASSERT_OK(chaseat(tfd, "/abc", 0, &result, NULL));
+        ASSERT_OK_ERRNO(chdir(cwd_saved));
+
+        /* If the file descriptor does not point to the root directory, the result will be relative
+         * unless the result is outside of the specified file descriptor. */
+
+        ASSERT_OK(chaseat(XAT_FDROOT, tfd, "abc", 0, &result, NULL));
         ASSERT_STREQ(result, "/usr");
         result = mfree(result);
 
-        assert_se(chaseat(tfd, "abc", CHASE_AT_RESOLVE_IN_ROOT, NULL, NULL) == -ENOENT);
-        assert_se(chaseat(tfd, "/abc", CHASE_AT_RESOLVE_IN_ROOT, NULL, NULL) == -ENOENT);
+        ASSERT_ERROR(chaseat(tfd, tfd, "abc", 0, NULL, NULL), ENOENT);
+        ASSERT_ERROR(chaseat(tfd, tfd, "/abc", 0, NULL, NULL), ENOENT);
 
-        ASSERT_OK(chaseat(tfd, "abc", CHASE_AT_RESOLVE_IN_ROOT | CHASE_NONEXISTENT, &result, NULL));
+        ASSERT_OK(chaseat(tfd, tfd, "abc", CHASE_NONEXISTENT, &result, NULL));
         ASSERT_STREQ(result, "usr");
         result = mfree(result);
 
-        ASSERT_OK(chaseat(tfd, "/abc", CHASE_AT_RESOLVE_IN_ROOT | CHASE_NONEXISTENT, &result, NULL));
+        ASSERT_OK(chaseat(tfd, tfd, "/abc", CHASE_NONEXISTENT, &result, NULL));
         ASSERT_STREQ(result, "usr");
         result = mfree(result);
 
         /* Test that absolute path or not are the same when resolving relative to a directory file
          * descriptor and that we always get a relative path back. */
 
-        ASSERT_OK(fd = openat(tfd, "def", O_CREAT|O_CLOEXEC, 0700));
+        fd = ASSERT_OK_ERRNO(openat(tfd, "def", O_CREAT|O_CLOEXEC, 0700));
         fd = safe_close(fd);
-        ASSERT_OK(symlinkat("/def", tfd, "qed"));
-        ASSERT_OK(chaseat(tfd, "qed", CHASE_AT_RESOLVE_IN_ROOT, &result, NULL));
+        ASSERT_OK_ERRNO(symlinkat("/def", tfd, "qed"));
+        ASSERT_OK(chaseat(tfd, tfd, "qed", 0, &result, NULL));
         ASSERT_STREQ(result, "def");
         result = mfree(result);
-        ASSERT_OK(chaseat(tfd, "/qed", CHASE_AT_RESOLVE_IN_ROOT, &result, NULL));
+        ASSERT_OK(chaseat(tfd, tfd, "/qed", 0, &result, NULL));
         ASSERT_STREQ(result, "def");
         result = mfree(result);
 
-        /* Valid directory file descriptor without CHASE_AT_RESOLVE_IN_ROOT should resolve symlinks against
+        /* Valid directory file descriptor should resolve symlinks against
          * host's root. */
-        assert_se(chaseat(tfd, "/qed", 0, NULL, NULL) == -ENOENT);
+        ASSERT_ERROR(chaseat(XAT_FDROOT, tfd, "/qed", 0, NULL, NULL), ENOENT);
 
         /* Test CHASE_PARENT */
 
-        ASSERT_OK(fd = open_mkdir_at(tfd, "chase", O_CLOEXEC, 0755));
-        ASSERT_OK(symlinkat("/def", fd, "parent"));
+        fd = ASSERT_OK(open_mkdir_at(tfd, "chase", O_CLOEXEC, 0755));
+        ASSERT_OK_ERRNO(symlinkat("/def", fd, "parent"));
         fd = safe_close(fd);
 
         /* Make sure that when we chase a symlink parent directory, that we chase the parent directory of the
          * symlink target and not the symlink itself. But if we add CHASE_NOFOLLOW, we get the parent
          * directory of the symlink itself. */
 
-        ASSERT_OK(chaseat(tfd, "chase/parent", CHASE_PARENT|CHASE_AT_RESOLVE_IN_ROOT, &result, &fd));
-        ASSERT_OK(faccessat(fd, "def", F_OK, 0));
+        ASSERT_OK(chaseat(tfd, tfd, "chase/parent", CHASE_PARENT, &result, &fd));
+        ASSERT_OK_ERRNO(faccessat(fd, "def", F_OK, 0));
         ASSERT_STREQ(result, "def");
         fd = safe_close(fd);
         result = mfree(result);
 
-        ASSERT_OK(chaseat(tfd, "chase/parent", CHASE_AT_RESOLVE_IN_ROOT|CHASE_PARENT|CHASE_NOFOLLOW, &result, &fd));
-        ASSERT_OK(faccessat(fd, "parent", F_OK, AT_SYMLINK_NOFOLLOW));
+        ASSERT_OK(chaseat(tfd, tfd, "chase/parent", CHASE_PARENT|CHASE_NOFOLLOW, &result, &fd));
+        ASSERT_OK_ERRNO(faccessat(fd, "parent", F_OK, AT_SYMLINK_NOFOLLOW));
         ASSERT_STREQ(result, "chase/parent");
         fd = safe_close(fd);
         result = mfree(result);
 
-        ASSERT_OK(chaseat(tfd, "chase", CHASE_PARENT|CHASE_AT_RESOLVE_IN_ROOT, &result, &fd));
-        ASSERT_OK(faccessat(fd, "chase", F_OK, 0));
+        ASSERT_OK(chaseat(tfd, tfd, "chase", CHASE_PARENT, &result, &fd));
+        ASSERT_OK_ERRNO(faccessat(fd, "chase", F_OK, 0));
         ASSERT_STREQ(result, "chase");
         fd = safe_close(fd);
         result = mfree(result);
 
-        ASSERT_OK(chaseat(tfd, "/", CHASE_PARENT|CHASE_AT_RESOLVE_IN_ROOT, &result, NULL));
+        ASSERT_OK(chaseat(tfd, tfd, "/", CHASE_PARENT, &result, NULL));
         ASSERT_STREQ(result, ".");
         result = mfree(result);
 
-        assert_se(chaseat(tfd, ".", CHASE_PARENT|CHASE_AT_RESOLVE_IN_ROOT, &result, NULL));
+        ASSERT_OK(chaseat(tfd, tfd, ".", CHASE_PARENT, &result, NULL));
         ASSERT_STREQ(result, ".");
         result = mfree(result);
 
         /* Test CHASE_MKDIR_0755 */
 
-        ASSERT_OK(chaseat(tfd, "m/k/d/i/r", CHASE_MKDIR_0755|CHASE_NONEXISTENT, &result, NULL));
-        ASSERT_OK(faccessat(tfd, "m/k/d/i", F_OK, 0));
-        assert_se(RET_NERRNO(faccessat(tfd, "m/k/d/i/r", F_OK, 0)) == -ENOENT);
+        ASSERT_OK(chaseat(XAT_FDROOT, tfd, "m/k/d/i/r", CHASE_MKDIR_0755|CHASE_NONEXISTENT, &result, NULL));
+        ASSERT_OK_ERRNO(faccessat(tfd, "m/k/d/i", F_OK, 0));
+        ASSERT_ERROR(RET_NERRNO(faccessat(tfd, "m/k/d/i/r", F_OK, 0)), ENOENT);
         ASSERT_STREQ(result, "m/k/d/i/r");
         result = mfree(result);
 
-        ASSERT_OK(chaseat(tfd, "m/../q", CHASE_MKDIR_0755|CHASE_NONEXISTENT, &result, NULL));
-        ASSERT_OK(faccessat(tfd, "m", F_OK, 0));
-        assert_se(RET_NERRNO(faccessat(tfd, "q", F_OK, 0)) == -ENOENT);
+        ASSERT_OK(chaseat(XAT_FDROOT, tfd, "m/../q", CHASE_MKDIR_0755|CHASE_NONEXISTENT, &result, NULL));
+        ASSERT_OK_ERRNO(faccessat(tfd, "m", F_OK, 0));
+        ASSERT_ERROR(RET_NERRNO(faccessat(tfd, "q", F_OK, 0)), ENOENT);
         ASSERT_STREQ(result, "q");
         result = mfree(result);
 
-        assert_se(chaseat(tfd, "i/../p", CHASE_MKDIR_0755|CHASE_NONEXISTENT, NULL, NULL) == -ENOENT);
+        ASSERT_ERROR(chaseat(XAT_FDROOT, tfd, "i/../p", CHASE_MKDIR_0755|CHASE_NONEXISTENT, NULL, NULL), ENOENT);
 
         /* Test CHASE_MKDIR_0755|CHASE_PARENT — creates intermediate dirs but not the final component */
 
-        ASSERT_OK(chaseat(tfd, "mkp/a/r/e/n/t/file", CHASE_MKDIR_0755|CHASE_PARENT, &result, &fd));
-        ASSERT_OK(faccessat(tfd, "mkp/a/r/e/n/t", F_OK, 0));
-        assert_se(RET_NERRNO(faccessat(tfd, "mkp/a/r/e/n/t/file", F_OK, 0)) == -ENOENT);
+        ASSERT_OK(chaseat(XAT_FDROOT, tfd, "mkp/a/r/e/n/t/file", CHASE_MKDIR_0755|CHASE_PARENT, &result, &fd));
+        ASSERT_OK_ERRNO(faccessat(tfd, "mkp/a/r/e/n/t", F_OK, 0));
+        ASSERT_ERROR(RET_NERRNO(faccessat(tfd, "mkp/a/r/e/n/t/file", F_OK, 0)), ENOENT);
         ASSERT_OK(fd_verify_directory(fd));
         fd = safe_close(fd);
         ASSERT_STREQ(result, "mkp/a/r/e/n/t/file");
@@ -632,68 +720,133 @@ TEST(chaseat) {
 
         /* Test CHASE_EXTRACT_FILENAME */
 
-        ASSERT_OK(chaseat(tfd, "chase/parent", CHASE_AT_RESOLVE_IN_ROOT|CHASE_PARENT|CHASE_NOFOLLOW|CHASE_EXTRACT_FILENAME, &result, &fd));
-        ASSERT_OK(faccessat(fd, result, F_OK, AT_SYMLINK_NOFOLLOW));
+        ASSERT_OK(chaseat(tfd, tfd, "chase/parent", CHASE_PARENT|CHASE_NOFOLLOW|CHASE_EXTRACT_FILENAME, &result, &fd));
+        ASSERT_OK_ERRNO(faccessat(fd, result, F_OK, AT_SYMLINK_NOFOLLOW));
         ASSERT_STREQ(result, "parent");
         fd = safe_close(fd);
         result = mfree(result);
 
-        ASSERT_OK(chaseat(tfd, "chase", CHASE_PARENT|CHASE_AT_RESOLVE_IN_ROOT|CHASE_EXTRACT_FILENAME, &result, &fd));
-        ASSERT_OK(faccessat(fd, result, F_OK, 0));
+        ASSERT_OK(chaseat(tfd, tfd, "chase", CHASE_PARENT|CHASE_EXTRACT_FILENAME, &result, &fd));
+        ASSERT_OK_ERRNO(faccessat(fd, result, F_OK, 0));
         ASSERT_STREQ(result, "chase");
         fd = safe_close(fd);
         result = mfree(result);
 
-        ASSERT_OK(chaseat(tfd, "/", CHASE_PARENT|CHASE_AT_RESOLVE_IN_ROOT|CHASE_EXTRACT_FILENAME, &result, NULL));
+        ASSERT_OK(chaseat(tfd, tfd, "/", CHASE_PARENT|CHASE_EXTRACT_FILENAME, &result, NULL));
         ASSERT_STREQ(result, ".");
         result = mfree(result);
 
-        ASSERT_OK(chaseat(tfd, ".", CHASE_PARENT|CHASE_AT_RESOLVE_IN_ROOT|CHASE_EXTRACT_FILENAME, &result, NULL));
+        ASSERT_OK(chaseat(tfd, tfd, ".", CHASE_PARENT|CHASE_EXTRACT_FILENAME, &result, NULL));
         ASSERT_STREQ(result, ".");
         result = mfree(result);
 
-        ASSERT_OK(chaseat(tfd, NULL, CHASE_PARENT|CHASE_AT_RESOLVE_IN_ROOT|CHASE_EXTRACT_FILENAME, &result, NULL));
+        ASSERT_OK(chaseat(tfd, tfd, NULL, CHASE_PARENT|CHASE_EXTRACT_FILENAME, &result, NULL));
         ASSERT_STREQ(result, ".");
         result = mfree(result);
 
         /* Test chase_and_openat() */
 
-        fd = chase_and_openat(tfd, "o/p/e/n/f/i/l/e", CHASE_MKDIR_0755, O_CREAT|O_EXCL|O_CLOEXEC, NULL);
-        ASSERT_OK(fd);
+        fd = ASSERT_OK(chase_and_openat(XAT_FDROOT, tfd, "o/p/e/n/f/i/l/e", CHASE_MKDIR_0755, O_CREAT|O_EXCL|O_CLOEXEC, NULL));
         ASSERT_OK(fd_verify_regular(fd));
         fd = safe_close(fd);
 
-        fd = chase_and_openat(tfd, "o/p/e/n/d/i/r", CHASE_MKDIR_0755, O_DIRECTORY|O_CREAT|O_EXCL|O_CLOEXEC, NULL);
-        ASSERT_OK(fd);
+        fd = ASSERT_OK(chase_and_openat(XAT_FDROOT, tfd, "o/p/e/n/d/i/r", CHASE_MKDIR_0755, O_DIRECTORY|O_CREAT|O_EXCL|O_CLOEXEC, NULL));
         ASSERT_OK(fd_verify_directory(fd));
         fd = safe_close(fd);
 
-        fd = chase_and_openat(tfd, NULL, CHASE_PARENT|CHASE_EXTRACT_FILENAME, O_PATH|O_DIRECTORY|O_CLOEXEC, &result);
-        ASSERT_OK(fd);
+        fd = ASSERT_OK(chase_and_openat(XAT_FDROOT, tfd, NULL, CHASE_PARENT|CHASE_EXTRACT_FILENAME, O_PATH|O_DIRECTORY|O_CLOEXEC, &result));
         ASSERT_STREQ(result, ".");
         fd = safe_close(fd);
         result = mfree(result);
 
         /* Test chase_and_openat() with CHASE_MKDIR_0755|CHASE_PARENT — opens parent dir */
 
-        fd = chase_and_openat(tfd, "mkopen/p/a/r/file.txt", CHASE_MKDIR_0755|CHASE_PARENT, O_RDONLY|O_CLOEXEC, NULL);
-        ASSERT_OK(fd);
+        fd = ASSERT_OK(chase_and_openat(XAT_FDROOT, tfd, "mkopen/p/a/r/file.txt", CHASE_MKDIR_0755|CHASE_PARENT, O_RDONLY|O_CLOEXEC, NULL));
         ASSERT_OK(fd_verify_directory(fd));
         ASSERT_OK(faccessat(tfd, "mkopen/p/a/r", F_OK, 0));
-        assert_se(RET_NERRNO(faccessat(tfd, "mkopen/p/a/r/file.txt", F_OK, 0)) == -ENOENT);
+        ASSERT_ERROR(RET_NERRNO(faccessat(tfd, "mkopen/p/a/r/file.txt", F_OK, 0)), ENOENT);
         fd = safe_close(fd);
 
         /* Test chase_and_openat() with CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY + O_CREAT — creates and opens target dir */
 
-        fd = chase_and_openat(tfd, "mkopen/d/i/r/target", CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, O_CREAT|O_RDONLY|O_CLOEXEC, NULL);
-        ASSERT_OK(fd);
+        fd = ASSERT_OK(chase_and_openat(XAT_FDROOT, tfd, "mkopen/d/i/r/target", CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, O_CREAT|O_RDONLY|O_CLOEXEC, NULL));
         ASSERT_OK(fd_verify_directory(fd));
         ASSERT_OK(faccessat(tfd, "mkopen/d/i/r/target", F_OK, 0));
         fd = safe_close(fd);
 
+        /* Test chase_and_openat() with various CHASE_PARENT / CHASE_EXTRACT_FILENAME combinations */
+
+        /* No CHASE_PARENT, no CHASE_EXTRACT_FILENAME, with ret_path — opens the target, returns full path. */
+        fd = ASSERT_OK(chase_and_openat(XAT_FDROOT, tfd, "o/p/e/n/d/i/r", 0, O_PATH|O_DIRECTORY|O_CLOEXEC, &result));
+        ASSERT_OK(fd_verify_directory(fd));
+        ASSERT_STREQ(result, "o/p/e/n/d/i/r");
+        fd = safe_close(fd);
+        result = mfree(result);
+
+        /* CHASE_PARENT with ret_path — opens parent dir, returns full path including final component. */
+        fd = ASSERT_OK(chase_and_openat(XAT_FDROOT, tfd, "o/p/e/n/d/i/r", CHASE_PARENT, O_PATH|O_DIRECTORY|O_CLOEXEC, &result));
+        ASSERT_OK(fd_verify_directory(fd));
+        ASSERT_OK_ERRNO(faccessat(fd, "r", F_OK, 0));
+        ASSERT_STREQ(result, "o/p/e/n/d/i/r");
+        fd = safe_close(fd);
+        result = mfree(result);
+
+        /* CHASE_PARENT|CHASE_EXTRACT_FILENAME with a real multi-component path — opens parent dir,
+         * returns just the filename. */
+        fd = ASSERT_OK(chase_and_openat(XAT_FDROOT, tfd, "o/p/e/n/d/i/r", CHASE_PARENT|CHASE_EXTRACT_FILENAME, O_PATH|O_DIRECTORY|O_CLOEXEC, &result));
+        ASSERT_OK(fd_verify_directory(fd));
+        ASSERT_OK_ERRNO(faccessat(fd, "r", F_OK, 0));
+        ASSERT_STREQ(result, "r");
+        fd = safe_close(fd);
+        result = mfree(result);
+
+        /* CHASE_EXTRACT_FILENAME only (without CHASE_PARENT) — opens the target itself, returns just
+         * the filename. */
+        fd = ASSERT_OK(chase_and_openat(XAT_FDROOT, tfd, "o/p/e/n/d/i/r", CHASE_EXTRACT_FILENAME, O_PATH|O_DIRECTORY|O_CLOEXEC, &result));
+        ASSERT_OK(fd_verify_directory(fd));
+        ASSERT_STREQ(result, "r");
+        fd = safe_close(fd);
+        result = mfree(result);
+
+        /* CHASE_PARENT through a symlink — the symlink is followed, parent of the target is opened.
+         * "chase/parent" where parent→/def: resolves to /def, parent is the root dir. */
+        fd = ASSERT_OK(chase_and_openat(tfd, tfd, "chase/parent", CHASE_PARENT, O_PATH|O_DIRECTORY|O_CLOEXEC, &result));
+        ASSERT_OK(fd_verify_directory(fd));
+        ASSERT_OK_ERRNO(faccessat(fd, "def", F_OK, 0));
+        ASSERT_STREQ(result, "def");
+        fd = safe_close(fd);
+        result = mfree(result);
+
+        /* CHASE_PARENT|CHASE_EXTRACT_FILENAME through a symlink — parent of the target is opened,
+         * returns just the target filename. */
+        fd = ASSERT_OK(chase_and_openat(tfd, tfd, "chase/parent", CHASE_PARENT|CHASE_EXTRACT_FILENAME, O_PATH|O_DIRECTORY|O_CLOEXEC, &result));
+        ASSERT_OK(fd_verify_directory(fd));
+        ASSERT_OK_ERRNO(faccessat(fd, "def", F_OK, 0));
+        ASSERT_STREQ(result, "def");
+        fd = safe_close(fd);
+        result = mfree(result);
+
+        /* CHASE_PARENT|CHASE_NOFOLLOW through a symlink — the symlink is NOT followed, parent of the
+         * symlink is opened. */
+        fd = ASSERT_OK(chase_and_openat(tfd, tfd, "chase/parent", CHASE_PARENT|CHASE_NOFOLLOW, O_PATH|O_DIRECTORY|O_CLOEXEC, &result));
+        ASSERT_OK(fd_verify_directory(fd));
+        ASSERT_OK_ERRNO(faccessat(fd, "parent", F_OK, AT_SYMLINK_NOFOLLOW));
+        ASSERT_STREQ(result, "chase/parent");
+        fd = safe_close(fd);
+        result = mfree(result);
+
+        /* CHASE_PARENT|CHASE_NOFOLLOW|CHASE_EXTRACT_FILENAME through a symlink — parent of the symlink
+         * is opened, returns just the symlink name. */
+        fd = ASSERT_OK(chase_and_openat(tfd, tfd, "chase/parent", CHASE_PARENT|CHASE_NOFOLLOW|CHASE_EXTRACT_FILENAME, O_PATH|O_DIRECTORY|O_CLOEXEC, &result));
+        ASSERT_OK(fd_verify_directory(fd));
+        ASSERT_OK_ERRNO(faccessat(fd, "parent", F_OK, AT_SYMLINK_NOFOLLOW));
+        ASSERT_STREQ(result, "parent");
+        fd = safe_close(fd);
+        result = mfree(result);
+
         /* Test chase_and_openatdir() */
 
-        ASSERT_OK(chase_and_opendirat(tfd, "o/p/e/n/d/i", 0, &result, &dir));
+        ASSERT_OK(chase_and_opendirat(XAT_FDROOT, tfd, "o/p/e/n/d/i", 0, &result, &dir));
         FOREACH_DIRENT(de, dir, assert_not_reached())
                 ASSERT_STREQ(de->d_name, "r");
         ASSERT_STREQ(result, "o/p/e/n/d/i");
@@ -701,57 +854,165 @@ TEST(chaseat) {
 
         /* Test chase_and_statat() */
 
-        ASSERT_OK(chase_and_statat(tfd, "o/p", 0, &result, &st));
+        ASSERT_OK(chase_and_statat(XAT_FDROOT, tfd, "o/p", 0, &result, &st));
         ASSERT_OK(stat_verify_directory(&st));
         ASSERT_STREQ(result, "o/p");
         result = mfree(result);
 
         /* Test chase_and_accessat() */
 
-        ASSERT_OK(chase_and_accessat(tfd, "o/p/e", 0, F_OK, &result));
+        ASSERT_OK(chase_and_accessat(XAT_FDROOT, tfd, "o/p/e", 0, F_OK, &result));
         ASSERT_STREQ(result, "o/p/e");
         result = mfree(result);
 
         /* Test chase_and_fopenat_unlocked() */
 
-        ASSERT_OK(chase_and_fopenat_unlocked(tfd, "o/p/e/n/f/i/l/e", 0, "re", &result, &f));
-        assert_se(fread(&(char[1]) {}, 1, 1, f) == 0);
-        assert_se(feof(f));
+        ASSERT_OK(chase_and_fopenat_unlocked(XAT_FDROOT, tfd, "o/p/e/n/f/i/l/e", 0, "re", &result, &f));
+        ASSERT_EQ(fread(&(char[1]) {}, 1, 1, f), 0u);
+        ASSERT_TRUE(feof(f));
         f = safe_fclose(f);
         ASSERT_STREQ(result, "o/p/e/n/f/i/l/e");
         result = mfree(result);
 
         /* Test chase_and_unlinkat() */
 
-        ASSERT_OK(chase_and_unlinkat(tfd, "o/p/e/n/f/i/l/e", 0, 0, &result));
+        ASSERT_OK(chase_and_unlinkat(XAT_FDROOT, tfd, "o/p/e/n/f/i/l/e", 0, 0, &result));
         ASSERT_STREQ(result, "o/p/e/n/f/i/l/e");
         result = mfree(result);
 
         /* Test chase_and_open_parent_at() */
 
-        ASSERT_OK(fd = chase_and_open_parent_at(tfd, "chase/parent", CHASE_AT_RESOLVE_IN_ROOT|CHASE_NOFOLLOW, &result));
-        ASSERT_OK(faccessat(fd, result, F_OK, AT_SYMLINK_NOFOLLOW));
+        fd = ASSERT_OK(chase_and_open_parent_at(XAT_FDROOT, tfd, "chase/parent", CHASE_NOFOLLOW, &result));
+        ASSERT_OK_ERRNO(faccessat(fd, result, F_OK, AT_SYMLINK_NOFOLLOW));
         ASSERT_STREQ(result, "parent");
         fd = safe_close(fd);
         result = mfree(result);
 
-        ASSERT_OK(fd = chase_and_open_parent_at(tfd, "chase", CHASE_AT_RESOLVE_IN_ROOT, &result));
-        ASSERT_OK(faccessat(fd, result, F_OK, 0));
+        fd = ASSERT_OK(chase_and_open_parent_at(XAT_FDROOT, tfd, "chase", 0, &result));
+        ASSERT_OK_ERRNO(faccessat(fd, result, F_OK, 0));
         ASSERT_STREQ(result, "chase");
         fd = safe_close(fd);
         result = mfree(result);
 
-        ASSERT_OK(fd = chase_and_open_parent_at(tfd, "/", CHASE_AT_RESOLVE_IN_ROOT, &result));
+        fd = ASSERT_OK(chase_and_open_parent_at(XAT_FDROOT, tfd, "/", 0, &result));
         ASSERT_STREQ(result, ".");
         fd = safe_close(fd);
         result = mfree(result);
 
-        ASSERT_OK(fd = chase_and_open_parent_at(tfd, ".", CHASE_AT_RESOLVE_IN_ROOT, &result));
+        fd = ASSERT_OK(chase_and_open_parent_at(XAT_FDROOT, tfd, ".", 0, &result));
         ASSERT_STREQ(result, ".");
         fd = safe_close(fd);
         result = mfree(result);
 }
 
+TEST(chaseat_separate_root_and_dir) {
+        _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+        _cleanup_close_ int root_fd = -EBADF, sub_fd = -EBADF, fd = -EBADF;
+        _cleanup_free_ char *result = NULL;
+
+        /* Exercise chaseat() with root_fd != dir_fd. The root marks the chroot boundary (symlinks may
+         * not escape it, absolute symlinks resolve to it), while dir_fd is the starting directory for
+         * relative paths. */
+
+        root_fd = ASSERT_OK(mkdtemp_open(NULL, 0, &t));
+
+        /* Create a file at the root and a subdirectory containing another file. */
+        ASSERT_OK_ERRNO(mkdirat(root_fd, "sub", 0755));
+        sub_fd = ASSERT_OK_ERRNO(openat(root_fd, "sub", O_CLOEXEC|O_DIRECTORY|O_PATH));
+
+        fd = ASSERT_OK_ERRNO(openat(root_fd, "outside", O_CREAT|O_CLOEXEC, 0600));
+        fd = safe_close(fd);
+
+        fd = ASSERT_OK_ERRNO(openat(sub_fd, "inside", O_CREAT|O_CLOEXEC, 0600));
+        fd = safe_close(fd);
+
+        /* Relative lookup from sub_fd under root_fd finds sub's own files. */
+        ASSERT_OK(chaseat(root_fd, sub_fd, "inside", 0, &result, NULL));
+        ASSERT_STREQ(result, "inside");
+        result = mfree(result);
+
+        /* Absolute path with dir_fd=sub_fd and root_fd set: path is relative to root_fd so "/inside" finds
+         * nothing. */
+        ASSERT_ERROR(chaseat(root_fd, sub_fd, "/inside", 0, &result, NULL), ENOENT);
+        ASSERT_OK_ZERO(chaseat(root_fd, sub_fd, "/inside", CHASE_NONEXISTENT, &result, NULL));
+        result = mfree(result);
+
+        /* "../outside" from sub_fd goes up one level (within root), finds root's file. */
+        ASSERT_OK(chaseat(root_fd, sub_fd, "../outside", 0, &result, NULL));
+        ASSERT_STREQ(result, "../outside");
+        result = mfree(result);
+
+        /* "../../../outside" cannot escape above root_fd — clamped. Still resolves to root's file. */
+        ASSERT_OK(chaseat(root_fd, sub_fd, "../../../outside", 0, &result, NULL));
+        ASSERT_STREQ(result, "../outside");
+        result = mfree(result);
+
+        /* Absolute symlink inside sub pointing at "/outside" — with root_fd set, /outside resolves to
+         * root_fd/outside, not the host's /outside. */
+        ASSERT_OK_ERRNO(symlinkat("/outside", sub_fd, "escape_abs"));
+        ASSERT_OK(chaseat(root_fd, sub_fd, "escape_abs", 0, &result, &fd));
+        ASSERT_TRUE(inode_same_at(fd, NULL, root_fd, "outside", AT_EMPTY_PATH));
+        ASSERT_STREQ(result, "/outside");
+        result = mfree(result);
+        fd = safe_close(fd);
+
+        /* Relative symlink trying to escape via many ".." — also clamped to root. */
+        ASSERT_OK_ERRNO(symlinkat("../../../../../outside", sub_fd, "escape_rel"));
+        ASSERT_OK(chaseat(root_fd, sub_fd, "escape_rel", 0, &result, NULL));
+        ASSERT_STREQ(result, "../outside");
+        result = mfree(result);
+
+        /* Symlink pointing to an absolute host path that does NOT exist under our root must fail, not
+         * leak to the host. /etc almost always exists on the host; under our tmp root it doesn't. */
+        ASSERT_OK_ERRNO(symlinkat("/etc", sub_fd, "escape_host"));
+        ASSERT_ERROR(chaseat(root_fd, sub_fd, "escape_host/hosts", 0, NULL, NULL), ENOENT);
+
+        /* Chasing just ".." from root_fd itself stays at root. */
+        ASSERT_OK(chaseat(root_fd, root_fd, "..", 0, &result, NULL));
+        ASSERT_STREQ(result, ".");
+        result = mfree(result);
+
+        /* (real-fd, XAT_FDROOT, relative): XAT_FDROOT as dir_fd redirects to root_fd, so relative
+         * paths start at root_fd. Result is relative because root_fd is a non-host-root fd and
+         * dir_fd (after redirection) equals root_fd. */
+        ASSERT_OK(chaseat(root_fd, XAT_FDROOT, "sub/inside", 0, &result, &fd));
+        ASSERT_TRUE(inode_same_at(fd, NULL, sub_fd, "inside", AT_EMPTY_PATH));
+        ASSERT_STREQ(result, "sub/inside");
+        fd = safe_close(fd);
+        result = mfree(result);
+
+        /* (real-fd, XAT_FDROOT, absolute): same as relative — absolute paths also resolve from
+         * root_fd. Leading slash is stripped. */
+        ASSERT_OK(chaseat(root_fd, XAT_FDROOT, "/sub/inside", 0, &result, &fd));
+        ASSERT_TRUE(inode_same_at(fd, NULL, sub_fd, "inside", AT_EMPTY_PATH));
+        ASSERT_STREQ(result, "sub/inside");
+        fd = safe_close(fd);
+        result = mfree(result);
+
+        /* (real-fd, XAT_FDROOT, absolute) resolving to root: "/outside" lives directly under
+         * root_fd. */
+        ASSERT_OK(chaseat(root_fd, XAT_FDROOT, "/outside", 0, &result, &fd));
+        ASSERT_TRUE(inode_same_at(fd, NULL, root_fd, "outside", AT_EMPTY_PATH));
+        ASSERT_STREQ(result, "outside");
+        fd = safe_close(fd);
+        result = mfree(result);
+
+        /* (real-fd, XAT_FDROOT) with an absolute symlink: the symlink target "/outside" resolves
+         * relative to root_fd, not the host root. Since dir_fd == root_fd (XAT_FDROOT was redirected),
+         * the result stays relative. */
+        ASSERT_OK(chaseat(root_fd, XAT_FDROOT, "sub/escape_abs", 0, &result, &fd));
+        ASSERT_TRUE(inode_same_at(fd, NULL, root_fd, "outside", AT_EMPTY_PATH));
+        ASSERT_STREQ(result, "outside");
+        fd = safe_close(fd);
+        result = mfree(result);
+
+        /* (real-fd, XAT_FDROOT) with non-existent path. */
+        ASSERT_OK_ZERO(chaseat(root_fd, XAT_FDROOT, "/nonexistent", CHASE_NONEXISTENT, &result, NULL));
+        ASSERT_STREQ(result, "nonexistent");
+        result = mfree(result);
+        ASSERT_ERROR(chaseat(root_fd, XAT_FDROOT, "/nonexistent", 0, NULL, NULL), ENOENT);
+}
+
 TEST(chaseat_prefix_root) {
         _cleanup_free_ char *cwd = NULL, *ret = NULL, *expected = NULL;
 
@@ -773,7 +1034,7 @@ TEST(chaseat_prefix_root) {
         ret = mfree(ret);
 
         ASSERT_OK(chaseat_prefix_root("hoge", "a/b//./c///", &ret));
-        assert_se(expected = path_join(cwd, "a/b/c/hoge"));
+        expected = ASSERT_NOT_NULL(path_join(cwd, "a/b/c/hoge"));
         ASSERT_STREQ(ret, expected);
 
         ret = mfree(ret);
@@ -784,8 +1045,8 @@ TEST(chaseat_prefix_root) {
 
         ret = mfree(ret);
 
-        assert_se(chaseat_prefix_root("./hoge/aaa/../././b", "a/b//./c///", &ret) >= 0);
-        assert_se(expected = path_join(cwd, "a/b/c/hoge/aaa/../././b"));
+        ASSERT_OK(chaseat_prefix_root("./hoge/aaa/../././b", "a/b//./c///", &ret));
+        expected = ASSERT_NOT_NULL(path_join(cwd, "a/b/c/hoge/aaa/../././b"));
         ASSERT_STREQ(ret, expected);
 }
 
@@ -794,9 +1055,9 @@ TEST(trailing_dot_dot) {
         _cleanup_close_ int fd = -EBADF;
 
         ASSERT_OK(chase("/usr/..", NULL, CHASE_PARENT, &path, &fd));
-        assert_se(path_equal(path, "/"));
+        ASSERT_PATH_EQ(path, "/");
         ASSERT_OK(fd_get_path(fd, &fdpath));
-        assert_se(path_equal(fdpath, "/"));
+        ASSERT_PATH_EQ(fdpath, "/");
 
         path = mfree(path);
         fdpath = mfree(fdpath);
@@ -811,9 +1072,9 @@ TEST(trailing_dot_dot) {
         _cleanup_free_ char *expected1 = ASSERT_PTR(path_join(t, "a/b/c"));
         _cleanup_free_ char *expected2 = ASSERT_PTR(path_join(t, "a/b"));
 
-        assert_se(path_equal(path, expected1));
+        ASSERT_PATH_EQ(path, expected1);
         ASSERT_OK(fd_get_path(fd, &fdpath));
-        assert_se(path_equal(fdpath, expected2));
+        ASSERT_PATH_EQ(fdpath, expected2);
 }
 
 TEST(use_chase_as_mkdir_p) {
index 71ad6b5e9d1c7f05b102a91e7688cbc61f7aff59..db5ec09312b5e3dbe88f9381a5eb539fa96ddc8e 100644 (file)
@@ -174,6 +174,7 @@ static int run(int argc, char *argv[]) {
 
                 _cleanup_free_ char *unprefixed_state_dir = NULL;
                 r = chaseat(esp_fd,
+                            esp_fd,
                             "/loader/swtpm",
                             CHASE_TRIGGER_AUTOFS|CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY,
                             &unprefixed_state_dir,