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)
This commit is contained in:
Daan De Meyer
2026-10-16 11:36:33 +01:00
committed by Luca Boccassi
parent 5f73eab263
commit b7ae93f093
3 changed files with 48 additions and 6 deletions
+2 -2
View File
@@ -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
+22 -4
View File
@@ -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);
}
+24
View File
@@ -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";