diff --git a/src/basic/fd-util.c b/src/basic/fd-util.c index a6118b4247..7ddd083cc2 100644 --- a/src/basic/fd-util.c +++ b/src/basic/fd-util.c @@ -1035,10 +1035,9 @@ int fd_get_diskseq(int fd, uint64_t *ret) { } int path_is_root_at(int dir_fd, const char *path) { - _cleanup_close_ int fd = -EBADF, pfd = -EBADF; - assert(dir_fd >= 0 || dir_fd == AT_FDCWD); + _cleanup_close_ int fd = -EBADF; if (!isempty(path)) { fd = openat(dir_fd, path, O_PATH|O_DIRECTORY|O_CLOEXEC); if (fd < 0) @@ -1047,19 +1046,19 @@ int path_is_root_at(int dir_fd, const char *path) { dir_fd = fd; } - pfd = openat(dir_fd, "..", O_PATH|O_DIRECTORY|O_CLOEXEC); - if (pfd < 0) - return errno == ENOTDIR ? false : -errno; + _cleanup_close_ int root_fd = openat(AT_FDCWD, "/", O_PATH|O_DIRECTORY|O_CLOEXEC); + if (root_fd < 0) + return -errno; - /* 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: + /* Even if the root directory has the same inode as our fd, the fd may not point to the root + * directory "/", and we also need to check that the mount ids are the same. Otherwise, a construct + * like the following could be used to trick us: * - * $ mkdir /tmp/x /tmp/x/y - * $ mount --bind /tmp/x /tmp/x/y + * $ mkdir /tmp/x + * $ mount --bind / /tmp/x */ - return fds_are_same_mount(dir_fd, pfd); + return fds_are_same_mount(dir_fd, root_fd); } int fds_are_same_mount(int fd1, int fd2) { diff --git a/src/test/test-fd-util.c b/src/test/test-fd-util.c index 621e611de3..c4e1515d53 100644 --- a/src/test/test-fd-util.c +++ b/src/test/test-fd-util.c @@ -680,6 +680,134 @@ TEST(dir_fd_is_root) { assert_se(dir_fd_is_root_or_cwd(fd) == 0); } +static void test_path_is_root_at_one(bool expected) { + ASSERT_OK_POSITIVE(path_is_root("/")); + ASSERT_OK_POSITIVE(path_is_root("/.")); + ASSERT_OK_EQ(path_is_root("/./.."), expected); + ASSERT_OK_EQ(path_is_root("/.."), expected); + ASSERT_OK_EQ(path_is_root("/../"), expected); + ASSERT_OK_EQ(path_is_root("/../."), expected); + ASSERT_OK_EQ(path_is_root("/../.."), expected); + + ASSERT_OK_ZERO(path_is_root("/usr")); + ASSERT_OK_ZERO(path_is_root("/./usr")); + ASSERT_OK_ZERO(path_is_root("/../usr")); + ASSERT_OK_ZERO(path_is_root("/.././usr")); + ASSERT_OK_ZERO(path_is_root("/../../usr")); + + _cleanup_close_ int fd = -EBADF; + ASSERT_OK_ERRNO(fd = open("/", O_CLOEXEC|O_PATH|O_DIRECTORY|O_NOFOLLOW)); + + ASSERT_OK_POSITIVE(path_is_root_at(fd, NULL)); + ASSERT_OK_POSITIVE(path_is_root_at(fd, "")); + ASSERT_OK_POSITIVE(path_is_root_at(fd, ".")); + ASSERT_OK_EQ(path_is_root_at(fd, "./../"), expected); + ASSERT_OK_EQ(path_is_root_at(fd, "../"), expected); + ASSERT_OK_POSITIVE(path_is_root_at(fd, "/")); + ASSERT_OK_POSITIVE(path_is_root_at(fd, "/.")); + ASSERT_OK_EQ(path_is_root_at(fd, "/./.."), expected); + ASSERT_OK_EQ(path_is_root_at(fd, "/.."), expected); + ASSERT_OK_EQ(path_is_root_at(fd, "/../"), expected); + ASSERT_OK_EQ(path_is_root_at(fd, "/../."), expected); + ASSERT_OK_EQ(path_is_root_at(fd, "/../.."), expected); + + ASSERT_OK_ZERO(path_is_root_at(fd, "usr")); + ASSERT_OK_ZERO(path_is_root_at(fd, "./usr")); + ASSERT_OK_ZERO(path_is_root_at(fd, "../usr")); + ASSERT_OK_ZERO(path_is_root_at(fd, "/usr")); + ASSERT_OK_ZERO(path_is_root_at(fd, "/./usr")); + ASSERT_OK_ZERO(path_is_root_at(fd, "/../usr")); + ASSERT_OK_ZERO(path_is_root_at(fd, "/.././usr")); + ASSERT_OK_ZERO(path_is_root_at(fd, "/../../usr")); + + safe_close(fd); + ASSERT_OK_ERRNO(fd = open("/../", O_CLOEXEC|O_PATH|O_DIRECTORY|O_NOFOLLOW)); + + ASSERT_OK_EQ(path_is_root_at(fd, NULL), expected); + ASSERT_OK_EQ(path_is_root_at(fd, ""), expected); + ASSERT_OK_EQ(path_is_root_at(fd, "."), expected); + ASSERT_OK_EQ(path_is_root_at(fd, "./.."), expected); + ASSERT_OK_EQ(path_is_root_at(fd, "../"), expected); + ASSERT_OK_POSITIVE(path_is_root_at(fd, "/")); + ASSERT_OK_POSITIVE(path_is_root_at(fd, "/.")); + ASSERT_OK_EQ(path_is_root_at(fd, "/./.."), expected); + ASSERT_OK_EQ(path_is_root_at(fd, "/.."), expected); + ASSERT_OK_EQ(path_is_root_at(fd, "/../"), expected); + ASSERT_OK_EQ(path_is_root_at(fd, "/../."), expected); + ASSERT_OK_EQ(path_is_root_at(fd, "/../.."), expected); + + ASSERT_OK_ZERO(path_is_root_at(fd, "usr")); + ASSERT_OK_ZERO(path_is_root_at(fd, "./usr")); + ASSERT_OK_ZERO(path_is_root_at(fd, "../usr")); + ASSERT_OK_ZERO(path_is_root_at(fd, "/usr")); + ASSERT_OK_ZERO(path_is_root_at(fd, "/./usr")); + ASSERT_OK_ZERO(path_is_root_at(fd, "/../usr")); + ASSERT_OK_ZERO(path_is_root_at(fd, "/.././usr")); + ASSERT_OK_ZERO(path_is_root_at(fd, "/../../usr")); +} + +TEST(path_is_root_at) { + int r; + + test_path_is_root_at_one(true); + + r = detach_mount_namespace(); + if (r < 0) + return (void) log_tests_skipped_errno(r, "Failed to detach mount namespace"); + + /* Interestingly, even after bind mount a path on "/", still "/" points to the previous root + * directory, but "/../" points to the new root directory. Hence, path_is_root("/") is true but + * path_is_root("/../") is false. Such spurious situation is resolved after chroot()ing to the new + * root directory. */ + ASSERT_OK(mount_nofollow_verbose(LOG_DEBUG, "/", "/", NULL, MS_BIND|MS_REC, NULL)); + log_debug("/* %s: bind mount(\"/\", \"/\") */", __func__); + test_path_is_root_at_one(false); + + /* chroot("/") does not change anything. */ + ASSERT_OK_ERRNO(chroot("/")); + log_debug("/* %s: chroot(\"/\") */", __func__); + test_path_is_root_at_one(false); + + /* chdir("/") neither change anything. */ + ASSERT_OK_ERRNO(chdir("/")); + log_debug("/* %s: chdir(\"/\") */", __func__); + test_path_is_root_at_one(false); + + /* chdir("/../") neither change anything. */ + ASSERT_OK_ERRNO(chdir("/../")); + log_debug("/* %s: chdir(\"/../\") */", __func__); + test_path_is_root_at_one(false); + + /* After chroot("/../"), both "/" and "/../" point to the new root directory. */ + ASSERT_OK_ERRNO(chroot("/../")); + log_debug("/* %s: chroot(\"/../\") */", __func__); + test_path_is_root_at_one(true); + + /* chdir("/../") does not change anything. */ + ASSERT_OK_ERRNO(chdir("/../")); + log_debug("/* %s: chdir(\"/../\") again */", __func__); + test_path_is_root_at_one(true); + + /* bind mounting to non-root directory has no problem, of course. */ + _cleanup_(rm_rf_physical_and_freep) char *tmp = NULL; + ASSERT_OK(mkdtemp_malloc("/tmp/test-path_is_root-XXXXXX", &tmp)); + ASSERT_OK(mount_nofollow_verbose(LOG_DEBUG, "/", tmp, NULL, MS_BIND|MS_REC, NULL)); + log_debug("/* %s: bind mount(\"/\", \"%s\") */", __func__, tmp); + test_path_is_root_at_one(true); + + ASSERT_OK_ERRNO(chdir(tmp)); + log_debug("/* %s: chdir(\"%s\") */", __func__, tmp); + test_path_is_root_at_one(true); + + ASSERT_OK_ERRNO(chroot(tmp)); + log_debug("/* %s: chroot(\"%s\") */", __func__, tmp); + test_path_is_root_at_one(true); + + ASSERT_OK_ERRNO(chdir(tmp)); + log_debug("/* %s: chdir(\"%s\") again */", __func__, tmp); + test_path_is_root_at_one(true); +} + TEST(fds_are_same_mount) { _cleanup_close_ int fd1 = -EBADF, fd2 = -EBADF, fd3 = -EBADF, fd4 = -EBADF;