Merge pull request #27299 from yuwata/chase-absolute

chase: return absolute path when dir_fd points to the root directory
This commit is contained in:
Daan De Meyer
2023-04-20 09:19:22 +02:00
committed by GitHub
11 changed files with 194 additions and 109 deletions

View File

@@ -111,12 +111,26 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
* 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.
*
* If "dir_fd" is a valid directory fd, "path" is an absolute path and "ret_path" is not NULL, this
* functions returns a relative path in "ret_path" because openat() like functions generally ignore
* the directory fd if they are provided with an absolute path. On the other hand, if "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 "/".
* 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 "/".
*
* 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
*
* 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
@@ -190,8 +204,9 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
return -ENOMEM;
/* 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. */
bool need_absolute = dir_fd == AT_FDCWD && path_is_absolute(path);
* 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. */
bool need_absolute = (dir_fd == AT_FDCWD && path_is_absolute(path)) || dir_fd_is_root(dir_fd) > 0;
if (need_absolute) {
done = strdup("/");
if (!done)
@@ -373,6 +388,11 @@ int chaseat(int dir_fd, const char *path, ChaseFlags flags, char **ret_path, int
unsafe_transition(&st_child, &st))
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))
need_absolute = true;
r = free_and_strdup(&done, need_absolute ? "/" : NULL);
if (r < 0)
return r;
@@ -474,8 +494,8 @@ chased_one:
return 0;
}
int chase(const char *path, const char *original_root, ChaseFlags flags, char **ret_path, int *ret_fd) {
_cleanup_free_ char *root = NULL, *absolute = NULL, *p = NULL;
int chase(const char *path, const char *root, ChaseFlags flags, char **ret_path, int *ret_fd) {
_cleanup_free_ char *root_abs = NULL, *absolute = NULL, *p = NULL;
_cleanup_close_ int fd = -EBADF, pfd = -EBADF;
int r;
@@ -484,18 +504,17 @@ int chase(const char *path, const char *original_root, ChaseFlags flags, char **
if (isempty(path))
return -EINVAL;
/* A root directory of "/" or "" is identical to none */
if (empty_or_root(original_root))
original_root = NULL;
if (original_root) {
r = path_make_absolute_cwd(original_root, &root);
/* A root directory of "/" or "" is identical to "/". */
if (empty_or_root(root))
root = "/";
else {
r = path_make_absolute_cwd(root, &root_abs);
if (r < 0)
return r;
/* Simplify the root directory, so that it has no duplicate slashes and nothing at the
* end. While we won't resolve the root path we still simplify it. */
path_simplify(root);
root = path_simplify(root_abs);
assert(path_is_absolute(root));
assert(!empty_or_root(root));
@@ -515,14 +534,14 @@ int chase(const char *path, const char *original_root, ChaseFlags flags, char **
return r;
}
path = path_startswith(absolute, empty_to_root(root));
path = path_startswith(absolute, root);
if (!path)
return log_full_errno(FLAGS_SET(flags, CHASE_WARN) ? LOG_WARNING : LOG_DEBUG,
SYNTHETIC_ERRNO(ECHRNG),
"Specified path '%s' is outside of specified root directory '%s', refusing to resolve.",
absolute, empty_to_root(root));
absolute, root);
fd = open(empty_to_root(root), O_CLOEXEC|O_DIRECTORY|O_PATH);
fd = open(root, O_CLOEXEC|O_DIRECTORY|O_PATH);
if (fd < 0)
return -errno;
@@ -532,19 +551,27 @@ int chase(const char *path, const char *original_root, ChaseFlags flags, char **
if (ret_path) {
if (!FLAGS_SET(flags, CHASE_EXTRACT_FILENAME)) {
_cleanup_free_ char *q = NULL;
q = path_join(empty_to_root(root), p);
if (!q)
return -ENOMEM;
/* When "root" points to the root directory, the result of chaseat() is always
* absolute, hence it is not necessary to prefix with the root. When "root" points to
* a non-root directory, the result path is always normalized and relative, hence
* we can simply call path_join() and not necessary to call path_simplify().
* Note that the result of chaseat() may start with "." (more specifically, it may be
* "." or "./"), and we need to drop "." in that case. */
path_simplify(q);
if (empty_or_root(root))
assert(path_is_absolute(p));
else {
char *q;
if (FLAGS_SET(flags, CHASE_TRAIL_SLASH) && ENDSWITH_SET(path, "/", "/."))
if (!strextend(&q, "/"))
assert(!path_is_absolute(p));
q = path_join(root, p + (*p == '.'));
if (!q)
return -ENOMEM;
free_and_replace(p, q);
free_and_replace(p, q);
}
}
*ret_path = TAKE_PTR(p);
@@ -556,6 +583,37 @@ int chase(const char *path, const char *original_root, ChaseFlags flags, char **
return r;
}
int chaseat_prefix_root(const char *path, const char *root, char **ret) {
char *q;
int r;
assert(path);
assert(ret);
/* This is mostly for prefixing the result of chaseat(). */
if (!path_is_absolute(path)) {
_cleanup_free_ char *root_abs = NULL;
/* If the dir_fd points to the root directory, chaseat() always returns an absolute path. */
assert(!empty_or_root(root));
r = path_make_absolute_cwd(root, &root_abs);
if (r < 0)
return r;
root = path_simplify(root_abs);
q = path_join(root, path + (path[0] == '.' && IN_SET(path[1], '/', '\0')));
} else
q = strdup(path);
if (!q)
return -ENOMEM;
*ret = q;
return 0;
}
int chase_and_open(const char *path, const char *root, ChaseFlags chase_flags, int open_flags, char **ret_path) {
_cleanup_close_ int path_fd = -EBADF;
_cleanup_free_ char *p = NULL, *fname = NULL;

View File

@@ -42,6 +42,8 @@ bool unsafe_transition(const struct stat *a, const struct stat *b);
int chase(const char *path_with_prefix, const char *root, ChaseFlags chase_flags, char **ret_path, int *ret_fd);
int chaseat_prefix_root(const char *path, const char *root, char **ret);
int chase_and_open(const char *path, const char *root, ChaseFlags chase_flags, int open_flags, char **ret_path);
int chase_and_opendir(const char *path, const char *root, ChaseFlags chase_flags, char **ret_path, DIR **ret_dir);
int chase_and_stat(const char *path, const char *root, ChaseFlags chase_flags, char **ret_path, struct stat *ret_stat);

View File

@@ -911,10 +911,23 @@ int dir_fd_is_root(int dir_fd) {
if (!statx_inode_same(&st.sx, &pst.sx))
return false;
/* Even if the parent directory has the same inode, the fd may not point to the root directory "/",
* and we also need to check that the mount ids are the same. Otherwise, a construct like the
* following could be used to trick us:
*
* $ mkdir /tmp/x /tmp/x/y
* $ mount --bind /tmp/x /tmp/x/y
*
* Note, statx() does not provide the mount ID and path_get_mnt_id_at() does not work when an old
* kernel is used without /proc mounted. In that case, let's assume that we do not have such spurious
* mount points in an early boot stage, and silently skip the following check. */
if (!FLAGS_SET(st.nsx.stx_mask, STATX_MNT_ID)) {
int mntid;
r = path_get_mnt_id_at(dir_fd, "", &mntid);
if (r == -ENOSYS)
return true; /* skip the mount ID check */
if (r < 0)
return r;
assert(mntid >= 0);
@@ -927,6 +940,8 @@ int dir_fd_is_root(int dir_fd) {
int mntid;
r = path_get_mnt_id_at(dir_fd, "..", &mntid);
if (r == -ENOSYS)
return true; /* skip the mount ID check */
if (r < 0)
return r;
assert(mntid >= 0);
@@ -935,13 +950,6 @@ int dir_fd_is_root(int dir_fd) {
pst.nsx.stx_mask |= STATX_MNT_ID;
}
/* Even if the parent directory has the same inode, the fd may not point to the root directory "/",
* and we also need to check that the mount ids are the same. Otherwise, a construct like the
* following could be used to trick us:
*
* $ mkdir /tmp/x /tmp/x/y
* $ mount --bind /tmp/x /tmp/x/y
*/
return statx_mount_same(&st.nsx, &pst.nsx);
}

View File

@@ -123,7 +123,7 @@ static int fd_fdinfo_mnt_id(int fd, const char *filename, int flags, int *ret_mn
r = read_full_virtual_file(path, &fdinfo, NULL);
if (r == -ENOENT) /* The fdinfo directory is a relatively new addition */
return -EOPNOTSUPP;
return proc_mounted() > 0 ? -EOPNOTSUPP : -ENOSYS;
if (r < 0)
return r;
@@ -280,7 +280,7 @@ int fd_is_mount_point(int fd, const char *filename, int flags) {
fallback_fdinfo:
r = fd_fdinfo_mnt_id(fd, filename, flags, &mount_id);
if (IN_SET(r, -EOPNOTSUPP, -EACCES, -EPERM))
if (IN_SET(r, -EOPNOTSUPP, -EACCES, -EPERM, -ENOSYS))
goto fallback_fstat;
if (r < 0)
return r;
@@ -549,6 +549,8 @@ int dev_is_devtmpfs(void) {
return r;
r = fopen_unlocked("/proc/self/mountinfo", "re", &proc_self_mountinfo);
if (r == -ENOENT)
return proc_mounted() > 0 ? -ENOENT : -ENOSYS;
if (r < 0)
return r;

View File

@@ -155,7 +155,7 @@ int open_os_release(const char *root, char **ret_path, int *ret_fd) {
return r;
if (ret_path) {
r = path_prefix_root_cwd(p, root, ret_path);
r = chaseat_prefix_root(p, root, ret_path);
if (r < 0)
return r;
}
@@ -292,7 +292,7 @@ int open_extension_release(
return r;
if (ret_path) {
r = path_prefix_root_cwd(p, root, ret_path);
r = chaseat_prefix_root(p, root, ret_path);
if (r < 0)
return r;
}

View File

@@ -100,34 +100,6 @@ int path_make_absolute_cwd(const char *p, char **ret) {
return 0;
}
int path_prefix_root_cwd(const char *p, const char *root, char **ret) {
_cleanup_free_ char *root_abs = NULL;
char *c;
int r;
assert(p);
assert(ret);
/* Unlike path_make_absolute(), this always prefixes root path if specified.
* The root path is always simplified, but the provided path will not.
* This is useful for prefixing the result of chaseat(). */
if (empty_or_root(root))
return path_make_absolute_cwd(p, ret);
r = path_make_absolute_cwd(root, &root_abs);
if (r < 0)
return r;
path_simplify(root_abs);
c = path_join(root_abs, p);
if (!c)
return -ENOMEM;
*ret = c;
return 0;
}
int path_make_relative(const char *from, const char *to, char **ret) {
_cleanup_free_ char *result = NULL;
unsigned n_parents;

View File

@@ -60,7 +60,6 @@ int path_split_and_make_absolute(const char *p, char ***ret);
char* path_make_absolute(const char *p, const char *prefix);
int safe_getcwd(char **ret);
int path_make_absolute_cwd(const char *p, char **ret);
int path_prefix_root_cwd(const char *p, const char *root, char **ret);
int path_make_relative(const char *from, const char *to, char **ret);
int path_make_relative_parent(const char *from_child, const char *to, char **ret);
char *path_startswith_full(const char *path, const char *prefix, bool accept_dot_dot) _pure_;

View File

@@ -540,7 +540,7 @@ int find_esp_and_warn(
return r;
if (ret_path) {
r = path_prefix_root_cwd(p, root, ret_path);
r = chaseat_prefix_root(p, root, ret_path);
if (r < 0)
return r;
}
@@ -859,7 +859,7 @@ int find_xbootldr_and_warn(
return r;
if (ret_path) {
r = path_prefix_root_cwd(p, root, ret_path);
r = chaseat_prefix_root(p, root, ret_path);
if (r < 0)
return r;
}

View File

@@ -442,6 +442,43 @@ TEST(chaseat) {
assert_se(streq(result, "/usr"));
result = mfree(result);
/* If the file descriptor points to the root directory, the result will be absolute. */
fd = open("/", O_CLOEXEC | O_DIRECTORY | O_PATH);
assert_se(fd >= 0);
assert_se(chaseat(fd, p, 0, &result, NULL) >= 0);
assert_se(streq(result, "/usr"));
result = mfree(result);
assert_se(chaseat(fd, p, CHASE_AT_RESOLVE_IN_ROOT, &result, NULL) >= 0);
assert_se(streq(result, "/usr"));
result = mfree(result);
fd = safe_close(fd);
/* 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_se(chaseat(tfd, "abc", 0, &result, NULL) >= 0);
assert_se(streq(result, "/usr"));
result = mfree(result);
assert_se(chaseat(tfd, "/abc", 0, &result, NULL) >= 0);
assert_se(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_se(chaseat(tfd, "abc", CHASE_AT_RESOLVE_IN_ROOT | CHASE_NONEXISTENT, &result, NULL) >= 0);
assert_se(streq(result, "usr"));
result = mfree(result);
assert_se(chaseat(tfd, "/abc", CHASE_AT_RESOLVE_IN_ROOT | CHASE_NONEXISTENT, &result, NULL) >= 0);
assert_se(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. */
@@ -611,4 +648,41 @@ static int intro(void) {
return EXIT_SUCCESS;
}
TEST(chaseat_prefix_root) {
_cleanup_free_ char *cwd = NULL, *ret = NULL, *expected = NULL;
assert_se(safe_getcwd(&cwd) >= 0);
assert_se(chaseat_prefix_root("/hoge", NULL, &ret) >= 0);
assert_se(streq(ret, "/hoge"));
ret = mfree(ret);
assert_se(chaseat_prefix_root("/hoge", "a/b/c", &ret) >= 0);
assert_se(streq(ret, "/hoge"));
ret = mfree(ret);
assert_se(chaseat_prefix_root("hoge", "/a/b//./c///", &ret) >= 0);
assert_se(streq(ret, "/a/b/c/hoge"));
ret = mfree(ret);
assert_se(chaseat_prefix_root("hoge", "a/b//./c///", &ret) >= 0);
assert_se(expected = path_join(cwd, "a/b/c/hoge"));
assert_se(streq(ret, expected));
ret = mfree(ret);
expected = mfree(expected);
assert_se(chaseat_prefix_root("./hoge/aaa/../././b", "/a/b//./c///", &ret) >= 0);
assert_se(streq(ret, "/a/b/c/hoge/aaa/../././b"));
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_se(streq(ret, expected));
}
DEFINE_TEST_MAIN_WITH_INTRO(LOG_INFO, intro);

View File

@@ -494,45 +494,6 @@ TEST(fsck_exists) {
assert_se(fsck_exists_for_fstype("/../bin/") == 0);
}
TEST(path_prefix_root_cwd) {
_cleanup_free_ char *cwd = NULL, *ret = NULL, *expected = NULL;
assert_se(safe_getcwd(&cwd) >= 0);
assert_se(path_prefix_root_cwd("hoge", NULL, &ret) >= 0);
assert_se(expected = path_join(cwd, "hoge"));
assert_se(streq(ret, expected));
ret = mfree(ret);
expected = mfree(expected);
assert_se(path_prefix_root_cwd("/hoge", NULL, &ret) >= 0);
assert_se(streq(ret, "/hoge"));
ret = mfree(ret);
assert_se(path_prefix_root_cwd("hoge", "/a/b//./c///", &ret) >= 0);
assert_se(streq(ret, "/a/b/c/hoge"));
ret = mfree(ret);
assert_se(path_prefix_root_cwd("hoge", "a/b//./c///", &ret) >= 0);
assert_se(expected = path_join(cwd, "a/b/c/hoge"));
assert_se(streq(ret, expected));
ret = mfree(ret);
expected = mfree(expected);
assert_se(path_prefix_root_cwd("/../hoge/aaa/../././b", "/a/b//./c///", &ret) >= 0);
assert_se(streq(ret, "/a/b/c/../hoge/aaa/../././b"));
ret = mfree(ret);
assert_se(path_prefix_root_cwd("/../hoge/aaa/../././b", "a/b//./c///", &ret) >= 0);
assert_se(expected = path_join(cwd, "a/b/c/../hoge/aaa/../././b"));
assert_se(streq(ret, expected));
}
static void test_path_make_relative_one(const char *from, const char *to, const char *expected) {
_cleanup_free_ char *z = NULL;
int r;

View File

@@ -28,6 +28,15 @@ command -v jq >/dev/null || {
"$bootctl" -R || test "$?" -eq 80
"$bootctl" -RR || test "$?" -eq 80
# regression tests for
# https://github.com/systemd/systemd/pull/27199#issuecomment-1511387731
if ret=$("$bootctl" --print-esp-path); then
test "$ret" = "/efi" -o "$ret" = "/boot" -o "$ret" = "/boot/efi"
fi
if ret=$("bootctl" --print-boot-path); then
test "$ret" = "/efi" -o "$ret" = "/boot" -o "$ret" = "/boot/efi"
fi
if "$bootctl" -R > /dev/null ; then
P=$("$bootctl" -R)
PP=$("$bootctl" -RR)