From b7ae93f093927e1ba717e783a57687c8e068dc20 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Sun, 4 Oct 2026 13:10:00 +0200 Subject: [PATCH] stat-util: fix null_or_empty_path_with_root() below a root null_or_empty_path_with_root() passes a path that already includes the root to chase_and_stat() with CHASE_PREFIX_ROOT, so the root is added twice. It also only recognizes /dev/null when the path itself is /dev/null. Below a root, a symlink to an empty file therefore fails with ENOENT. A symlink to /dev/null fails the same way when the image has no /dev/null. Resolve the symlink inside the root without requiring the target to exist, and stat the target through the O_PATH fd that chase() returns. chase() accepts ret_fd together with CHASE_NONEXISTENT and sets the fd to -EBADF when the target is missing. Its comment says that ret_fd cannot be combined with CHASE_NONEXISTENT. Fix the comment. (cherry picked from commit f2b0c29597466bdcc800dcfa8da4c8192fb9e824) --- src/basic/chase.c | 4 ++-- src/basic/stat-util.c | 26 ++++++++++++++++++++++---- src/test/test-stat-util.c | 24 ++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/basic/chase.c b/src/basic/chase.c index 23a523798e..c07e8f6d92 100644 --- a/src/basic/chase.c +++ b/src/basic/chase.c @@ -286,8 +286,8 @@ int chaseat(int root_fd, int dir_fd, const char *path, ChaseFlags flags, char ** * 2. With ret_fd: in this case the destination is opened after chasing it as O_PATH and this file * descriptor is returned as return value. This is useful to open files relative to some root * directory. Note that the returned O_PATH file descriptors must be converted into a regular one - * (using fd_reopen() or such) before it can be used for reading/writing. ret_fd may not be - * combined with CHASE_NONEXISTENT. + * (using fd_reopen() or such) before it can be used for reading/writing. If CHASE_NONEXISTENT + * is also set and the destination doesn't exist, 0 is returned and ret_fd is set to -EBADF. * * 3. With CHASE_STEP: in this case only a single step of the normalization is executed, i.e. only * the first symlink or ".." component of the path is resolved, and the resulting path is diff --git a/src/basic/stat-util.c b/src/basic/stat-util.c index 72307fa721..e455f41a8c 100644 --- a/src/basic/stat-util.c +++ b/src/basic/stat-util.c @@ -342,22 +342,40 @@ bool stat_is_empty(struct stat *st) { } int null_or_empty_path_with_root(const char *fn, const char *root) { + _cleanup_free_ char *resolved = NULL; + _cleanup_close_ int fd = -EBADF; struct stat st; int r; assert(fn); - /* A symlink to /dev/null or an empty file? - * When looking under root_dir, we can't expect /dev/ to be mounted, - * so let's see if the path is a (possibly dangling) symlink to /dev/null. */ + /* fn includes the root prefix. */ if (path_equal(path_startswith(fn, root ?: "/"), "dev/null")) return true; - r = chase_and_stat(fn, root, CHASE_PREFIX_ROOT, NULL, &st); + if (empty_or_root(root)) { + if (stat(fn, &st) < 0) + return -errno; + + return null_or_empty(&st); + } + + /* An image below a root directory usually has no /dev/null, so a symlink to /dev/null dangles there. + * CHASE_NONEXISTENT resolves such a symlink without requiring the target to exist. */ + r = chase(fn, root, CHASE_NONEXISTENT, &resolved, &fd); if (r < 0) return r; + if (path_equal(path_startswith(resolved, root), "dev/null")) + return true; + + if (r == 0) + return -ENOENT; + + if (fstat(fd, &st) < 0) + return -errno; + return null_or_empty(&st); } diff --git a/src/test/test-stat-util.c b/src/test/test-stat-util.c index 86a93c487f..2bb1b46550 100644 --- a/src/test/test-stat-util.c +++ b/src/test/test-stat-util.c @@ -10,6 +10,7 @@ #include "errno-list.h" #include "errno-util.h" #include "fd-util.h" +#include "fileio.h" #include "fs-util.h" #include "mount-util.h" #include "mountpoint-util.h" @@ -78,6 +79,29 @@ TEST(null_or_empty_path_with_root) { assert_se(null_or_empty_path_with_root("/foobar/barbar/dev/null", "/foobar/barbar/") == 1); } +TEST(null_or_empty_path_with_root_symlinks) { + _cleanup_(rm_rf_physical_and_freep) char *root = NULL; + + ASSERT_OK(mkdtemp_malloc("/tmp/test-stat-util-XXXXXX", &root)); + ASSERT_OK_ERRNO(mkdir(strjoina(root, "/etc"), 0755)); + + /* The root has no /dev/null, so the symlink to it dangles. */ + ASSERT_OK_ERRNO(symlink("/dev/null", strjoina(root, "/etc/null"))); + ASSERT_OK_ERRNO(symlink("../../../../../../dev/null", strjoina(root, "/etc/null-relative"))); + ASSERT_OK(touch(strjoina(root, "/etc/empty"))); + ASSERT_OK_ERRNO(symlink("/etc/empty", strjoina(root, "/etc/to-empty"))); + ASSERT_OK(write_string_file(strjoina(root, "/etc/full"), "full", WRITE_STRING_FILE_CREATE)); + ASSERT_OK_ERRNO(symlink("full", strjoina(root, "/etc/to-full"))); + + ASSERT_OK_POSITIVE(null_or_empty_path_with_root(strjoina(root, "/etc/null"), root)); + ASSERT_OK_POSITIVE(null_or_empty_path_with_root(strjoina(root, "/etc/null-relative"), root)); + ASSERT_OK_POSITIVE(null_or_empty_path_with_root(strjoina(root, "/etc/empty"), root)); + ASSERT_OK_POSITIVE(null_or_empty_path_with_root(strjoina(root, "/etc/to-empty"), root)); + ASSERT_OK_ZERO(null_or_empty_path_with_root(strjoina(root, "/etc/full"), root)); + ASSERT_OK_ZERO(null_or_empty_path_with_root(strjoina(root, "/etc/to-full"), root)); + ASSERT_ERROR(null_or_empty_path_with_root(strjoina(root, "/etc/missing"), root), ENOENT); +} + TEST(inode_same) { _cleanup_close_ int fd = -EBADF; _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-files_same.XXXXXX";