chase-symlinks: Rework open() functions and some chase flags

Currently, when CHASE_PARENT is specified, we chase the parent directory
of the symlink itself. Let's change this and chase the parent directory
of the symlink target so that trying to open the actual file later with
O_NOFOLLOW doesn't fail with ELOOP.

To get the current behavior, callers can add CHASE_NOFOLLOW to chase
the parent directory of the symlink itself.

Currently, when CHASE_MKDIR_0755 is specified, we create all components
of the path as directories. Instead, let's change the flag to only create
parent directories and leave the final component of the PATH untouched.
Also, allow CHASE_NONEXISTENT with CHASE_MKDIR_0755 now that it doesn't
create all components anymore.

Finally, rework chase_symlinks_and_open() and chase_symlinkat_at_and_open()
to always chase the parent directory and use xopenat() to open the final
component of the path. This allows us to pass O_CREAT to create the file or
directory (O_DIRECTORY) if it is missing. If CHASE_PARENT is configured, we
just reopen the parent directory that we chased.
This commit is contained in:
Daan De Meyer
2023-03-13 16:17:21 +01:00
parent 06344330a1
commit 47f0e1b5e0
3 changed files with 106 additions and 71 deletions

View File

@@ -98,20 +98,9 @@ int chase_symlinks_at(
if ((flags & CHASE_STEP) && ret_fd)
return -EINVAL;
if (FLAGS_SET(flags, CHASE_MKDIR_0755|CHASE_NONEXISTENT))
return -EINVAL;
if (isempty(path))
path = ".";
if (flags & CHASE_PARENT) {
r = path_extract_directory(path, &buffer);
if (r == -EDESTADDRREQ)
path = "."; /* If we don't have a parent directory, fall back to the dir_fd directory. */
else if (r < 0)
return r;
}
/* This function resolves symlinks of the path relative to the given directory file descriptor. If
* CHASE_SYMLINKS_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
@@ -292,6 +281,9 @@ int chase_symlinks_at(
previous_stat = st;
}
if (FLAGS_SET(flags, CHASE_PARENT) && isempty(todo))
break;
close_and_replace(fd, fd_parent);
continue;
@@ -306,20 +298,23 @@ int chase_symlinks_at(
if (!isempty(todo) && !path_is_safe(todo))
return r;
if (flags & CHASE_NONEXISTENT) {
if (FLAGS_SET(flags, CHASE_MKDIR_0755) && !isempty(todo)) {
child = open_mkdir_at(fd, first, O_CLOEXEC|O_PATH|O_EXCL, 0755);
if (child < 0)
return child;
} else if (FLAGS_SET(flags, CHASE_PARENT) && isempty(todo)) {
if (!path_extend(&done, first))
return -ENOMEM;
break;
} else if (flags & CHASE_NONEXISTENT) {
if (!path_extend(&done, first, todo))
return -ENOMEM;
exists = false;
break;
}
if (!(flags & CHASE_MKDIR_0755))
} else
return r;
child = open_mkdir_at(fd, first, O_CLOEXEC|O_PATH|O_EXCL, 0755);
if (child < 0)
return child;
}
if (fstat(child, &st) < 0)
@@ -394,11 +389,14 @@ int chase_symlinks_at(
if (!path_extend(&done, first))
return -ENOMEM;
if (FLAGS_SET(flags, CHASE_PARENT) && isempty(todo))
break;
/* And iterate again, but go one directory further down. */
close_and_replace(fd, child);
}
if (flags & (CHASE_PARENT|CHASE_MKDIR_0755)) {
if (flags & CHASE_PARENT) {
r = fd_verify_directory(fd);
if (r < 0)
return r;
@@ -551,28 +549,38 @@ int chase_symlinks_and_open(
char **ret_path) {
_cleanup_close_ int path_fd = -EBADF;
_cleanup_free_ char *p = NULL;
_cleanup_free_ char *p = NULL, *fname = NULL;
mode_t mode = open_flags & O_DIRECTORY ? 0755 : 0644;
const char *q;
int r;
if (chase_flags & (CHASE_NONEXISTENT|CHASE_STEP))
return -EINVAL;
if (empty_or_root(root) && !ret_path &&
(chase_flags & (CHASE_NO_AUTOFS|CHASE_SAFE|CHASE_PROHIBIT_SYMLINKS|CHASE_PARENT|CHASE_MKDIR_0755)) == 0) {
(chase_flags & (CHASE_NO_AUTOFS|CHASE_SAFE|CHASE_PROHIBIT_SYMLINKS|CHASE_PARENT|CHASE_MKDIR_0755)) == 0)
/* Shortcut this call if none of the special features of this call are requested */
r = open(path, open_flags | (FLAGS_SET(chase_flags, CHASE_NOFOLLOW) ? O_NOFOLLOW : 0));
if (r < 0)
return -errno;
return RET_NERRNO(xopenat(AT_FDCWD, path,
open_flags | (FLAGS_SET(chase_flags, CHASE_NOFOLLOW) ? O_NOFOLLOW : 0),
mode));
return r;
}
r = chase_symlinks(path, root, chase_flags, ret_path ? &p : NULL, &path_fd);
r = chase_symlinks(path, root, CHASE_PARENT|chase_flags, &p, &path_fd);
if (r < 0)
return r;
assert(path_fd >= 0);
r = fd_reopen(path_fd, open_flags);
assert_se(q = path_startswith(p, empty_to_root(root)));
if (isempty(q))
q = ".";
r = path_extract_filename(q, &fname);
if (r < 0 && r != -EADDRNOTAVAIL)
return r;
if (FLAGS_SET(chase_flags, CHASE_PARENT) || r == -EADDRNOTAVAIL)
r = fd_reopen(path_fd, open_flags);
else
r = xopenat(path_fd, fname, open_flags|O_NOFOLLOW, mode);
if (r < 0)
return r;
@@ -748,31 +756,25 @@ int chase_symlinks_and_unlink(
int unlink_flags,
char **ret_path) {
_cleanup_free_ char *p = NULL, *rp = NULL, *fname = NULL;
_cleanup_free_ char *p = NULL, *fname = NULL;
_cleanup_close_ int fd = -EBADF;
int r;
assert(path);
r = path_extract_filename(path, &fname);
if (r < 0)
return r;
fd = chase_symlinks_and_open(path, root, chase_flags|CHASE_PARENT, O_PATH|O_DIRECTORY|O_CLOEXEC, ret_path ? &p : NULL);
fd = chase_symlinks_and_open(path, root, chase_flags|CHASE_PARENT|CHASE_NOFOLLOW, O_PATH|O_DIRECTORY|O_CLOEXEC, &p);
if (fd < 0)
return fd;
if (p) {
rp = path_join(p, fname);
if (!rp)
return -ENOMEM;
}
r = path_extract_filename(p, &fname);
if (r < 0)
return r;
if (unlinkat(fd, fname, unlink_flags) < 0)
return -errno;
if (ret_path)
*ret_path = TAKE_PTR(rp);
*ret_path = TAKE_PTR(p);
return 0;
}
@@ -785,7 +787,8 @@ int chase_symlinks_at_and_open(
char **ret_path) {
_cleanup_close_ int path_fd = -EBADF;
_cleanup_free_ char *p = NULL;
_cleanup_free_ char *p = NULL, *fname = NULL;
mode_t mode = open_flags & O_DIRECTORY ? 0755 : 0644;
int r;
if (chase_flags & (CHASE_NONEXISTENT|CHASE_STEP))
@@ -794,14 +797,22 @@ int chase_symlinks_at_and_open(
if (dir_fd == AT_FDCWD && !ret_path &&
(chase_flags & (CHASE_NO_AUTOFS|CHASE_SAFE|CHASE_PROHIBIT_SYMLINKS|CHASE_PARENT|CHASE_MKDIR_0755)) == 0)
/* Shortcut this call if none of the special features of this call are requested */
return RET_NERRNO(openat(dir_fd, path, open_flags | (FLAGS_SET(chase_flags, CHASE_NOFOLLOW) ? O_NOFOLLOW : 0)));
return RET_NERRNO(xopenat(dir_fd, path,
open_flags | (FLAGS_SET(chase_flags, CHASE_NOFOLLOW) ? O_NOFOLLOW : 0),
mode));
r = chase_symlinks_at(dir_fd, path, chase_flags, ret_path ? &p : NULL, &path_fd);
r = chase_symlinks_at(dir_fd, path, chase_flags|CHASE_PARENT, &p, &path_fd);
if (r < 0)
return r;
assert(path_fd >= 0);
r = fd_reopen(path_fd, open_flags);
r = path_extract_filename(p, &fname);
if (r < 0 && r != -EDESTADDRREQ)
return r;
if (FLAGS_SET(chase_flags, CHASE_PARENT) || r == -EDESTADDRREQ)
r = fd_reopen(path_fd, open_flags);
else
r = xopenat(path_fd, fname, open_flags|O_NOFOLLOW, mode);
if (r < 0)
return r;

View File

@@ -20,8 +20,10 @@ typedef enum ChaseSymlinksFlags {
CHASE_AT_RESOLVE_IN_ROOT = 1 << 8, /* Same as openat2()'s RESOLVE_IN_ROOT flag, symlinks are resolved
* relative to the given directory fd instead of root. */
CHASE_PROHIBIT_SYMLINKS = 1 << 9, /* Refuse all symlinks */
CHASE_PARENT = 1 << 10, /* Chase the parent directory of the given path. */
CHASE_MKDIR_0755 = 1 << 11, /* Create any missing directories in the given path. */
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. */
CHASE_MKDIR_0755 = 1 << 11, /* Create any missing parent directories in the given path. */
} ChaseSymlinksFlags;
bool unsafe_transition(const struct stat *a, const struct stat *b);

View File

@@ -331,10 +331,10 @@ TEST(chase_symlinks) {
}
assert_se(lstat(p, &st) >= 0);
r = chase_symlinks_and_unlink(p, NULL, 0, 0, &result);
r = chase_symlinks_and_unlink(p, NULL, 0, 0, &result);
assert_se(r == 0);
assert_se(path_equal(result, p));
result = mfree(result);
assert_se(r == 0);
assert_se(lstat(p, &st) == -1 && errno == ENOENT);
/* Test CHASE_NOFOLLOW */
@@ -426,18 +426,6 @@ TEST(chase_symlinks) {
assert_se(chase_symlinks("top/dot/dot", temp, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, NULL, NULL) == -EREMCHG);
assert_se(chase_symlinks("top/dot/dot", temp, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_WARN, NULL, NULL) == -EREMCHG);
/* Test CHASE_PARENT */
assert_se(chase_symlinks("/chase/parent", temp, CHASE_PREFIX_ROOT|CHASE_PARENT|CHASE_NONEXISTENT, &result, NULL) >= 0);
p = strjoina(temp, "/chase");
assert_se(streq(p, result));
result = mfree(result);
assert_se(chase_symlinks("/chase", temp, CHASE_PREFIX_ROOT|CHASE_PARENT|CHASE_NONEXISTENT, &result, NULL) >= 0);
assert_se(streq(temp, result));
result = mfree(result);
assert_se(chase_symlinks("/", temp, CHASE_PREFIX_ROOT|CHASE_PARENT|CHASE_NONEXISTENT, NULL, NULL) == -EADDRNOTAVAIL);
assert_se(chase_symlinks(".", temp, CHASE_PREFIX_ROOT|CHASE_PARENT|CHASE_NONEXISTENT, NULL, NULL) == -EADDRNOTAVAIL);
cleanup:
assert_se(rm_rf(temp, REMOVE_ROOT|REMOVE_PHYSICAL) >= 0);
}
@@ -479,23 +467,51 @@ TEST(chase_symlinks_at) {
/* Test CHASE_PARENT */
assert_se(chase_symlinks_at(tfd, "chase/parent", CHASE_NONEXISTENT|CHASE_PARENT, &result, NULL) >= 0);
assert_se(streq(result, "chase"));
assert_se((fd = open_mkdir_at(tfd, "chase", O_CLOEXEC, 0755)) >= 0);
assert_se(symlinkat("/def", fd, "parent") >= 0);
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_se(chase_symlinks_at(tfd, "chase/parent", CHASE_PARENT|CHASE_AT_RESOLVE_IN_ROOT, &result, &fd) >= 0);
assert_se(faccessat(fd, "def", F_OK, 0) >= 0);
assert_se(streq(result, "def"));
fd = safe_close(fd);
result = mfree(result);
assert_se(chase_symlinks_at(tfd, "chase", CHASE_NONEXISTENT|CHASE_PARENT, &result, NULL) >= 0);
assert_se(chase_symlinks_at(tfd, "chase/parent", CHASE_AT_RESOLVE_IN_ROOT|CHASE_PARENT|CHASE_NOFOLLOW, &result, &fd) >= 0);
assert_se(faccessat(fd, "parent", F_OK, AT_SYMLINK_NOFOLLOW) >= 0);
assert_se(streq(result, "chase/parent"));
fd = safe_close(fd);
result = mfree(result);
assert_se(chase_symlinks_at(tfd, "chase", CHASE_PARENT|CHASE_AT_RESOLVE_IN_ROOT, &result, &fd) >= 0);
assert_se(faccessat(fd, "chase", F_OK, 0) >= 0);
assert_se(streq(result, "chase"));
fd = safe_close(fd);
result = mfree(result);
assert_se(chase_symlinks_at(tfd, "/", CHASE_PARENT|CHASE_AT_RESOLVE_IN_ROOT, &result, NULL) >= 0);
assert_se(streq(result, "."));
result = mfree(result);
assert_se(chase_symlinks_at(tfd, ".", CHASE_PARENT|CHASE_AT_RESOLVE_IN_ROOT, &result, NULL) >= 0);
assert_se(streq(result, "."));
result = mfree(result);
/* Test CHASE_MKDIR_0755 */
assert_se(chase_symlinks_at(tfd, "m/k/d/i/r", CHASE_MKDIR_0755, &result, NULL) >= 0);
assert_se(faccessat(tfd, "m/k/d/i/r", F_OK, 0) >= 0);
assert_se(chase_symlinks_at(tfd, "m/k/d/i/r", CHASE_MKDIR_0755|CHASE_NONEXISTENT, &result, NULL) >= 0);
assert_se(faccessat(tfd, "m/k/d/i", F_OK, 0) >= 0);
assert_se(RET_NERRNO(faccessat(tfd, "m/k/d/i/r", F_OK, 0)) == -ENOENT);
assert_se(streq(result, "m/k/d/i/r"));
result = mfree(result);
assert_se(chase_symlinks_at(tfd, "m/../q", CHASE_MKDIR_0755, &result, NULL) >= 0);
assert_se(faccessat(tfd, "q", F_OK, 0) >= 0);
assert_se(chase_symlinks_at(tfd, "m/../q", CHASE_MKDIR_0755|CHASE_NONEXISTENT, &result, NULL) >= 0);
assert_se(faccessat(tfd, "m", F_OK, 0) >= 0);
assert_se(RET_NERRNO(faccessat(tfd, "q", F_OK, 0)) == -ENOENT);
assert_se(streq(result, "q"));
result = mfree(result);
@@ -503,8 +519,14 @@ TEST(chase_symlinks_at) {
/* Test chase_symlinks_at_and_open() */
fd = chase_symlinks_at_and_open(tfd, "o/p/e/n", CHASE_MKDIR_0755, O_CLOEXEC, NULL);
fd = chase_symlinks_at_and_open(tfd, "o/p/e/n/f/i/l/e", CHASE_MKDIR_0755, O_CREAT|O_EXCL|O_CLOEXEC, NULL);
assert_se(fd >= 0);
assert_se(fd_verify_regular(fd) >= 0);
fd = safe_close(fd);
fd = chase_symlinks_at_and_open(tfd, "o/p/e/n/d/i/r", CHASE_MKDIR_0755, O_DIRECTORY|O_CREAT|O_EXCL|O_CLOEXEC, NULL);
assert_se(fd >= 0);
assert_se(fd_verify_directory(fd) >= 0);
fd = safe_close(fd);
}