]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
chase-symlinks: Rework open() functions and some chase flags
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Mon, 13 Mar 2023 15:17:21 +0000 (16:17 +0100)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Wed, 15 Mar 2023 11:38:11 +0000 (12:38 +0100)
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.

src/basic/chase-symlinks.c
src/basic/chase-symlinks.h
src/test/test-fs-util.c

index 1b434950e62f204626ce55d67a1bb7d4d5aca3d0..b3bd635e8615fd6f7fd12b6fd50eee6c7649b868 100644 (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 r;
-        }
+                return RET_NERRNO(xopenat(AT_FDCWD, path,
+                                          open_flags | (FLAGS_SET(chase_flags, CHASE_NOFOLLOW) ? O_NOFOLLOW : 0),
+                                          mode));
 
-        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;
 
index 26e8cfceca77d7c86c73a9f1ec6efe3af56aecce..448471fa6de9a3c9ee559b7893c79d360b76a805 100644 (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);
index 065ad3c6e7f09b1631b4174c63976a90b40a95bc..a18dc00fc5d72b9a7936dd5161ac12f83b4799dd 100644 (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((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/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", CHASE_NONEXISTENT|CHASE_PARENT, &result, NULL) >= 0);
+        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);
 }