]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
conf-files: chase symlink files in conf directories
authorYu Watanabe <watanabe.yu+github@gmail.com>
Thu, 26 Jun 2025 16:18:35 +0000 (01:18 +0900)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Fri, 27 Jun 2025 19:13:38 +0000 (04:13 +0900)
Previously, symlinks in paths to conf directories are chased, but
symlink files in conf directories were not.

This also makes symlink files in conf directories chased. And, any
unresolvable symlinks are dropped, even if no verification is requested.

src/basic/conf-files.c
src/test/test-conf-files.c

index 2d551425819451aad50a56b52ba3116d8c0b36be..85b9ea8e5f3b670029310a451cad9439730e388f 100644 (file)
@@ -23,6 +23,8 @@
 static int files_add(
                 DIR *dir,
                 const char *dirpath,
+                int rfd,
+                const char *root, /* for logging, can be NULL */
                 Hashmap **files,
                 Set **masked,
                 const char *suffix,
@@ -32,12 +34,14 @@ static int files_add(
 
         assert(dir);
         assert(dirpath);
+        assert(rfd >= 0 || rfd == AT_FDCWD);
         assert(files);
         assert(masked);
 
-        FOREACH_DIRENT(de, dir, return -errno) {
-                _cleanup_free_ char *n = NULL, *p = NULL;
-                struct stat st;
+        root = strempty(root);
+
+        FOREACH_DIRENT(de, dir, return log_debug_errno(errno, "Failed to read directory '%s/%s': %m",
+                                                       root, skip_leading_slash(dirpath))) {
 
                 /* Does this match the suffix? */
                 if (suffix && !endswith(de->d_name, suffix))
@@ -45,31 +49,83 @@ static int files_add(
 
                 /* Has this file already been found in an earlier directory? */
                 if (hashmap_contains(*files, de->d_name)) {
-                        log_debug("Skipping overridden file '%s/%s'.", dirpath, de->d_name);
+                        log_debug("Skipping overridden file '%s/%s/%s'.",
+                                  root, skip_leading_slash(dirpath), de->d_name);
                         continue;
                 }
 
                 /* Has this been masked in an earlier directory? */
-                if ((flags & CONF_FILES_FILTER_MASKED) && set_contains(*masked, de->d_name)) {
-                        log_debug("File '%s/%s' is masked by previous entry.", dirpath, de->d_name);
+                if ((flags & CONF_FILES_FILTER_MASKED) != 0 && set_contains(*masked, de->d_name)) {
+                        log_debug("File '%s/%s/%s' is masked by previous entry.",
+                                  root, skip_leading_slash(dirpath), de->d_name);
                         continue;
                 }
 
-                /* Read file metadata if we shall validate the check for file masks, for node types or whether the node is marked executable. */
-                if (flags & (CONF_FILES_FILTER_MASKED|CONF_FILES_REGULAR|CONF_FILES_DIRECTORY|CONF_FILES_EXECUTABLE))
-                        if (fstatat(dirfd(dir), de->d_name, &st, 0) < 0) {
-                                log_debug_errno(errno, "Failed to stat '%s/%s', ignoring: %m", dirpath, de->d_name);
+                _cleanup_free_ char *p = path_join(dirpath, de->d_name);
+                if (!p)
+                        return log_oom_debug();
+
+                _cleanup_free_ char *resolved_path = NULL;
+                bool need_stat = (flags & (CONF_FILES_FILTER_MASKED | CONF_FILES_REGULAR | CONF_FILES_DIRECTORY | CONF_FILES_EXECUTABLE)) != 0;
+                struct stat st;
+
+                if (!need_stat || FLAGS_SET(flags, CONF_FILES_FILTER_MASKED_BY_SYMLINK)) {
+
+                        /* Even if no verification is requested, let's unconditionally call chaseat(),
+                         * to drop unsafe symlinks. */
+
+                        r = chaseat(rfd, p, CHASE_AT_RESOLVE_IN_ROOT | CHASE_NONEXISTENT, &resolved_path, /* ret_fd = */ NULL);
+                        if (r < 0) {
+                                log_debug_errno(r, "Failed to chase '%s/%s', ignoring: %m",
+                                                root, skip_leading_slash(p));
                                 continue;
                         }
+                        if (r == 0 && FLAGS_SET(flags, CONF_FILES_FILTER_MASKED_BY_SYMLINK)) {
+
+                                /* If the path points to /dev/null in a image or so, then the device node may not exist. */
+                                if (path_equal(skip_leading_slash(resolved_path), "dev/null")) {
+                                        /* Mark this one as masked */
+                                        r = set_put_strdup(masked, de->d_name);
+                                        if (r < 0)
+                                                return log_oom_debug();
+
+                                        log_debug("File '%s/%s' is a mask (symlink to /dev/null).",
+                                                  root, skip_leading_slash(p));
+                                        continue;
+                                }
+
+                                /* If the flag is set, we need to have stat, hence, skip the entry. */
+                                log_debug_errno(SYNTHETIC_ERRNO(ENOENT), "Failed to chase '%s/%s', ignoring: %m",
+                                                root, skip_leading_slash(p));
+                                continue;
+                        }
+
+                        if (need_stat) {
+                                r = fstatat(rfd, resolved_path, &st, AT_SYMLINK_NOFOLLOW);
+                                if (r < 0) {
+                                        log_debug_errno(r, "Failed to stat '%s/%s', ignoring: %m",
+                                                        root, skip_leading_slash(p));
+                                        continue;
+                                }
+                        }
+
+                } else {
+                        r = chase_and_statat(rfd, p, CHASE_AT_RESOLVE_IN_ROOT, &resolved_path, &st);
+                        if (r < 0) {
+                                log_debug_errno(r, "Failed to chase and stat '%s/%s', ignoring: %m",
+                                                root, skip_leading_slash(p));
+                                continue;
+                        }
+                }
 
                 /* Is this a masking entry? */
                 if (FLAGS_SET(flags, CONF_FILES_FILTER_MASKED_BY_SYMLINK) && stat_may_be_dev_null(&st)) {
                         /* Mark this one as masked */
                         r = set_put_strdup(masked, de->d_name);
                         if (r < 0)
-                                return r;
+                                return log_oom_debug();
 
-                        log_debug("File '%s/%s' is a mask (symlink to /dev/null).", dirpath, de->d_name);
+                        log_debug("File '%s/%s' is a mask (symlink to /dev/null).", root, skip_leading_slash(p));
                         continue;
                 }
 
@@ -77,46 +133,42 @@ static int files_add(
                         /* Mark this one as masked */
                         r = set_put_strdup(masked, de->d_name);
                         if (r < 0)
-                                return r;
+                                return log_oom_debug();
 
-                        log_debug("File '%s/%s' is a mask (an empty file).", dirpath, de->d_name);
+                        log_debug("File '%s/%s' is a mask (an empty file).", root, skip_leading_slash(p));
                         continue;
                 }
 
-                /* Does this node have the right type? */
-                if (flags & (CONF_FILES_REGULAR|CONF_FILES_DIRECTORY))
-                        if (!((flags & CONF_FILES_DIRECTORY) && S_ISDIR(st.st_mode)) &&
-                            !((flags & CONF_FILES_REGULAR) && S_ISREG(st.st_mode))) {
-                                log_debug("Ignoring '%s/%s', as it does not have the right type.", dirpath, de->d_name);
-                                continue;
-                        }
+                /* Is this node a regular file? */
+                if (FLAGS_SET(flags, CONF_FILES_REGULAR) && !S_ISREG(st.st_mode)) {
+                        log_debug("Ignoring '%s/%s', as it is not a regular file.", root, skip_leading_slash(p));
+                        continue;
+                }
 
-                /* Does this node have the executable bit set? */
-                if (flags & CONF_FILES_EXECUTABLE)
-                        /* As requested: check if the file is marked executable. Note that we don't check access(X_OK)
-                         * here, as we care about whether the file is marked executable at all, and not whether it is
-                         * executable for us, because if so, such errors are stuff we should log about. */
+                /* Is this node a directory? */
+                if (FLAGS_SET(flags, CONF_FILES_DIRECTORY) && !S_ISDIR(st.st_mode)) {
+                        log_debug("Ignoring '%s/%s', as it is not a directory.", root, skip_leading_slash(p));
+                        continue;
+                }
 
-                        if ((st.st_mode & 0111) == 0) { /* not executable */
-                                log_debug("Ignoring '%s/%s', as it is not marked executable.", dirpath, de->d_name);
-                                continue;
-                        }
+                /* Does this node have the executable bit set?
+                 * As requested: check if the file is marked executable. Note that we don't check access(X_OK)
+                 * here, as we care about whether the file is marked executable at all, and not whether it is
+                 * executable for us, because if so, such errors are stuff we should log about. */
+                if (FLAGS_SET(flags, CONF_FILES_EXECUTABLE) && (st.st_mode & 0111) == 0) {
+                        log_debug("Ignoring '%s/%s', as it is not marked executable.", root, skip_leading_slash(p));
+                        continue;
+                }
 
-                n = strdup(de->d_name);
+                _cleanup_free_ char *n = strdup(de->d_name);
                 if (!n)
-                        return -ENOMEM;
+                        return log_oom_debug();
 
-                if ((flags & CONF_FILES_BASENAME))
-                        r = hashmap_ensure_put(files, &string_hash_ops_free, n, n);
-                else {
-                        p = path_join(dirpath, de->d_name);
-                        if (!p)
-                                return -ENOMEM;
-
-                        r = hashmap_ensure_put(files, &string_hash_ops_free_free, n, p);
+                r = hashmap_ensure_put(files, &string_hash_ops_free_free, n, p);
+                if (r < 0) {
+                        assert(r == -ENOMEM);
+                        return log_oom_debug();
                 }
-                if (r < 0)
-                        return r;
                 assert(r > 0);
 
                 TAKE_PTR(n);
@@ -126,58 +178,100 @@ static int files_add(
         return 0;
 }
 
-static int copy_and_sort_files_from_hashmap(Hashmap *fh, char ***ret) {
+static int copy_and_sort_files_from_hashmap(Hashmap *fh, const char *root, ConfFilesFlags flags, char ***ret) {
         _cleanup_free_ char **sv = NULL;
-        char **files;
+        _cleanup_strv_free_ char **files = NULL;
+        size_t n = 0;
         int r;
 
         assert(ret);
 
         r = hashmap_dump_sorted(fh, (void***) &sv, /* ret_n = */ NULL);
         if (r < 0)
-                return r;
+                return log_oom_debug();
 
         /* The entries in the array given by hashmap_dump_sorted() are still owned by the hashmap. */
-        files = strv_copy(sv);
-        if (!files)
-                return -ENOMEM;
+        STRV_FOREACH(s, sv) {
+                _cleanup_free_ char *p = NULL;
 
-        *ret = files;
+                if (FLAGS_SET(flags, CONF_FILES_BASENAME)) {
+                        r = path_extract_filename(*s, &p);
+                        if (r < 0)
+                                return log_debug_errno(r, "Failed to extract filename from '%s': %m", *s);
+                } else if (root) {
+                        p = path_join(root, skip_leading_slash(*s));
+                        if (!p)
+                                return log_oom_debug();
+                }
+
+                if (p)
+                        r = strv_consume_with_size(&files, &n, TAKE_PTR(p));
+                else
+                        r = strv_extend_with_size(&files, &n, *s);
+                if (r < 0)
+                        return log_oom_debug();
+        }
+
+        *ret = TAKE_PTR(files);
         return 0;
 }
 
-int conf_files_list_strv(
-                char ***ret,
+static int conf_files_list_impl(
                 const char *suffix,
-                const char *root,
+                int rfd,
+                const char *root, /* for logging, can be NULL */
                 ConfFilesFlags flags,
-                const char * const *dirs) {
+                const char * const *dirs,
+                Hashmap **ret) {
 
         _cleanup_hashmap_free_ Hashmap *fh = NULL;
         _cleanup_set_free_ Set *masked = NULL;
         int r;
 
+        assert(rfd >= 0 || rfd == AT_FDCWD);
         assert(ret);
 
         STRV_FOREACH(p, dirs) {
                 _cleanup_closedir_ DIR *dir = NULL;
                 _cleanup_free_ char *path = NULL;
 
-                r = chase_and_opendir(*p, root, CHASE_PREFIX_ROOT, &path, &dir);
+                r = chase_and_opendirat(rfd, *p, CHASE_AT_RESOLVE_IN_ROOT, &path, &dir);
                 if (r < 0) {
                         if (r != -ENOENT)
-                                log_debug_errno(r, "Failed to chase and open directory '%s', ignoring: %m", *p);
+                                log_debug_errno(r, "Failed to chase and open directory '%s%s', ignoring: %m", strempty(root), *p);
                         continue;
                 }
 
-                r = files_add(dir, path, &fh, &masked, suffix, flags);
+                r = files_add(dir, path, rfd, root, &fh, &masked, suffix, flags);
                 if (r == -ENOMEM)
                         return r;
-                if (r < 0)
-                        log_debug_errno(r, "Failed to search for files in '%s', ignoring: %m", path);
         }
 
-        return copy_and_sort_files_from_hashmap(fh, ret);
+        *ret = TAKE_PTR(fh);
+        return 0;
+}
+
+int conf_files_list_strv(
+                char ***ret,
+                const char *suffix,
+                const char *root,
+                ConfFilesFlags flags,
+                const char * const *dirs) {
+
+        _cleanup_hashmap_free_ Hashmap *fh = NULL;
+        int r;
+
+        assert(ret);
+
+        _cleanup_close_ int rfd = open(empty_to_root(root), O_CLOEXEC|O_DIRECTORY|O_PATH);
+        if (rfd < 0)
+                return log_debug_errno(errno, "Failed to open '%s': %m", root);
+
+        r = conf_files_list_impl(suffix, rfd, root, flags, dirs, &fh);
+        if (r < 0)
+                return r;
+
+        return copy_and_sort_files_from_hashmap(fh, empty_to_root(root), flags, ret);
 }
 
 int conf_files_list_strv_at(
@@ -188,31 +282,20 @@ int conf_files_list_strv_at(
                 const char * const *dirs) {
 
         _cleanup_hashmap_free_ Hashmap *fh = NULL;
-        _cleanup_set_free_ Set *masked = NULL;
+        _cleanup_free_ char *root = NULL;
         int r;
 
         assert(rfd >= 0 || rfd == AT_FDCWD);
         assert(ret);
 
-        STRV_FOREACH(p, dirs) {
-                _cleanup_closedir_ DIR *dir = NULL;
-                _cleanup_free_ char *path = NULL;
-
-                r = chase_and_opendirat(rfd, *p, CHASE_AT_RESOLVE_IN_ROOT, &path, &dir);
-                if (r < 0) {
-                        if (r != -ENOENT)
-                                log_debug_errno(r, "Failed to chase and open directory '%s', ignoring: %m", *p);
-                        continue;
-                }
+        if (rfd >= 0 && DEBUG_LOGGING)
+                (void) fd_get_path(rfd, &root); /* for logging */
 
-                r = files_add(dir, path, &fh, &masked, suffix, flags);
-                if (r == -ENOMEM)
-                        return r;
-                if (r < 0)
-                        log_debug_errno(r, "Failed to search for files in '%s', ignoring: %m", path);
-        }
+        r = conf_files_list_impl(suffix, rfd, root, flags, dirs, &fh);
+        if (r < 0)
+                return r;
 
-        return copy_and_sort_files_from_hashmap(fh, ret);
+        return copy_and_sort_files_from_hashmap(fh, /* root = */ NULL, flags, ret);
 }
 
 int conf_files_insert(char ***strv, const char *root, char **dirs, const char *path) {
index cd3b2c2d1c6e860428f67fdc4f38fbbf6541cd62..3901831e8374cc2afd20657be60be791fcf60110 100644 (file)
@@ -9,6 +9,7 @@
 #include "conf-files.h"
 #include "fd-util.h"
 #include "fileio.h"
+#include "fs-util.h"
 #include "path-util.h"
 #include "rm-rf.h"
 #include "string-util.h"
 #include "tmpfile-util.h"
 
 TEST(conf_files_list) {
-        _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
-        _cleanup_close_ int tfd = -EBADF;
+        _cleanup_(rm_rf_physical_and_freep) char *t = NULL, *t2 = NULL;
+        _cleanup_close_ int tfd = -EBADF, tfd2 = -EBADF;
         _cleanup_strv_free_ char **result = NULL;
-        const char *search1, *search2, *search1_a, *search1_b, *search1_c, *search2_aa;
+        const char *search1, *search2, *search3, *search1_a, *search1_b, *search1_c, *search2_aa, *search2_mm;
 
-        tfd = mkdtemp_open("/tmp/test-conf-files-XXXXXX", O_PATH, &t);
-        assert(tfd >= 0);
+        ASSERT_OK(tfd = mkdtemp_open("/tmp/test-conf-files-XXXXXX", O_PATH, &t));
+        ASSERT_OK(tfd2 = mkdtemp_open("/tmp/test-conf-files-XXXXXX", O_PATH, &t2));
 
-        assert_se(mkdirat(tfd, "dir1", 0755) >= 0);
-        assert_se(mkdirat(tfd, "dir2", 0755) >= 0);
+        ASSERT_OK_ERRNO(mkdirat(tfd, "dir1", 0755));
+        ASSERT_OK_ERRNO(mkdirat(tfd, "dir2", 0755));
+        ASSERT_OK_ERRNO(mkdirat(tfd, "dir3", 0755));
 
         search1 = strjoina(t, "/dir1/");
         search2 = strjoina(t, "/dir2/");
+        search3 = strjoina(t, "/dir3/");
 
         FOREACH_STRING(p, "a.conf", "b.conf", "c.foo") {
                 _cleanup_free_ char *path = NULL;
@@ -38,19 +41,45 @@ TEST(conf_files_list) {
                 assert_se(write_string_file(path, "foobar", WRITE_STRING_FILE_CREATE) >= 0);
         }
 
-        assert_se(symlinkat("/dev/null", tfd, "dir1/m.conf") >= 0);
+        ASSERT_OK_ERRNO(symlinkat("/dev/null", tfd, "dir1/m.conf"));
+        ASSERT_OK_ERRNO(symlinkat("../dev/null", tfd, "dir1/mm.conf"));
 
-        FOREACH_STRING(p, "a.conf", "aa.conf", "m.conf") {
+        FOREACH_STRING(p, "a.conf", "aa.conf", "m.conf", "mm.conf") {
                 _cleanup_free_ char *path = NULL;
 
                 assert_se(path = path_join(search2, p));
                 assert_se(write_string_file(path, "hogehoge", WRITE_STRING_FILE_CREATE) >= 0);
         }
 
+        ASSERT_OK(touch(strjoina(t2, "/absolute-empty.real")));
+        ASSERT_OK(symlinkat_atomic_full(strjoina(t2, "/absolute-empty.real"), AT_FDCWD, strjoina(search3, "absolute-empty.conf"), /* flags = */ 0));
+
+        ASSERT_OK(write_string_file_at(tfd2, "absolute-non-empty.real", "absolute-non-empty", WRITE_STRING_FILE_CREATE));
+        ASSERT_OK(symlinkat_atomic_full(strjoina(t2, "/absolute-non-empty.real"), AT_FDCWD, strjoina(search3, "absolute-non-empty.conf"), /* flags = */ 0));
+
+        ASSERT_OK(touch(strjoina(t2, "/relative-empty.real")));
+        ASSERT_OK(symlinkat_atomic_full(strjoina(t2, "/relative-empty.real"), AT_FDCWD, strjoina(search3, "relative-empty.conf"), SYMLINK_MAKE_RELATIVE));
+
+        ASSERT_OK(write_string_file_at(tfd2, "relative-non-empty.real", "relative-non-empty", WRITE_STRING_FILE_CREATE));
+        ASSERT_OK(symlinkat_atomic_full(strjoina(t2, "/relative-non-empty.real"), AT_FDCWD, strjoina(search3, "relative-non-empty.conf"), SYMLINK_MAKE_RELATIVE));
+
+        ASSERT_OK(touch(strjoina(t, "/absolute-empty-for-root.real")));
+        ASSERT_OK(symlinkat_atomic_full("/absolute-empty-for-root.real", AT_FDCWD, strjoina(search3, "absolute-empty-for-root.conf"), /* flags = */ 0));
+
+        ASSERT_OK(write_string_file_at(tfd, "absolute-non-empty-for-root.real", "absolute-non-empty", WRITE_STRING_FILE_CREATE));
+        ASSERT_OK(symlinkat_atomic_full("/absolute-non-empty-for-root.real", AT_FDCWD, strjoina(search3, "absolute-non-empty-for-root.conf"), /* flags = */ 0));
+
+        ASSERT_OK(touch(strjoina(t, "/relative-empty-for-root.real")));
+        ASSERT_OK(symlinkat_atomic_full("../../../../relative-empty-for-root.real", AT_FDCWD, strjoina(search3, "relative-empty-for-root.conf"), /* flags = */ 0));
+
+        ASSERT_OK(write_string_file_at(tfd, "relative-non-empty-for-root.real", "relative-non-empty", WRITE_STRING_FILE_CREATE));
+        ASSERT_OK(symlinkat_atomic_full("../../../../relative-non-empty-for-root.real", AT_FDCWD, strjoina(search3, "relative-non-empty-for-root.conf"), /* flags = */ 0));
+
         search1_a = strjoina(search1, "a.conf");
         search1_b = strjoina(search1, "b.conf");
         search1_c = strjoina(search1, "c.foo");
         search2_aa = strjoina(search2, "aa.conf");
+        search2_mm = strjoina(search2, "mm.conf");
 
         /* search dir1 without suffix */
         assert_se(conf_files_list(&result, NULL, NULL, CONF_FILES_FILTER_MASKED, search1) >= 0);
@@ -105,7 +134,7 @@ TEST(conf_files_list) {
         /* search two dirs */
         assert_se(conf_files_list_strv(&result, ".conf", NULL, CONF_FILES_FILTER_MASKED, STRV_MAKE_CONST(search1, search2)) >= 0);
         strv_print(result);
-        assert_se(strv_equal(result, STRV_MAKE(search1_a, search2_aa, search1_b)));
+        assert_se(strv_equal(result, STRV_MAKE(search1_a, search2_aa, search1_b, search2_mm)));
 
         result = strv_free(result);
 
@@ -117,7 +146,7 @@ TEST(conf_files_list) {
 
         assert_se(conf_files_list_strv_at(&result, ".conf", AT_FDCWD, CONF_FILES_FILTER_MASKED, STRV_MAKE_CONST(search1, search2)) >= 0);
         strv_print(result);
-        assert_se(strv_equal(result, STRV_MAKE(search1_a, search2_aa, search1_b)));
+        assert_se(strv_equal(result, STRV_MAKE(search1_a, search2_aa, search1_b, search2_mm)));
 
         result = strv_free(result);
 
@@ -127,10 +156,123 @@ TEST(conf_files_list) {
 
         result = strv_free(result);
 
+        /* search dir3 */
+        ASSERT_OK(conf_files_list(&result, /* suffix = */ NULL, /* root = */ NULL, CONF_FILES_FILTER_MASKED, search3));
+        strv_print(result);
+        ASSERT_TRUE(strv_equal(result, STRV_MAKE(strjoina(search3, "absolute-non-empty.conf"),
+                                                 strjoina(search3, "relative-non-empty.conf"))));
+        result = strv_free(result);
+
+        ASSERT_OK(conf_files_list(&result, /* suffix = */ NULL, /* root = */ NULL, CONF_FILES_FILTER_MASKED_BY_EMPTY, search3));
+        strv_print(result);
+        ASSERT_TRUE(strv_equal(result, STRV_MAKE(strjoina(search3, "absolute-non-empty.conf"),
+                                                 strjoina(search3, "relative-non-empty.conf"))));
+        result = strv_free(result);
+
+        ASSERT_OK(conf_files_list(&result, /* suffix = */ NULL, /* root = */ NULL, CONF_FILES_FILTER_MASKED_BY_SYMLINK, search3));
+        strv_print(result);
+        ASSERT_TRUE(strv_equal(result, STRV_MAKE(strjoina(search3, "absolute-empty.conf"),
+                                                 strjoina(search3, "absolute-non-empty.conf"),
+                                                 strjoina(search3, "relative-empty.conf"),
+                                                 strjoina(search3, "relative-non-empty.conf"))));
+        result = strv_free(result);
+
+        ASSERT_OK(conf_files_list(&result, /* suffix = */ NULL, /* root = */ NULL, CONF_FILES_REGULAR, search3));
+        strv_print(result);
+        ASSERT_TRUE(strv_equal(result, STRV_MAKE(strjoina(search3, "absolute-empty.conf"),
+                                                 strjoina(search3, "absolute-non-empty.conf"),
+                                                 strjoina(search3, "relative-empty.conf"),
+                                                 strjoina(search3, "relative-non-empty.conf"))));
+        result = strv_free(result);
+
+        ASSERT_OK(conf_files_list(&result, /* suffix = */ NULL, t, CONF_FILES_FILTER_MASKED, "/dir3/"));
+        strv_print(result);
+        ASSERT_TRUE(strv_equal(result, STRV_MAKE(strjoina(search3, "absolute-non-empty-for-root.conf"),
+                                                 strjoina(search3, "relative-non-empty-for-root.conf"))));
+        result = strv_free(result);
+
+        ASSERT_OK(conf_files_list(&result, /* suffix = */ NULL, t, CONF_FILES_FILTER_MASKED_BY_EMPTY, "/dir3/"));
+        strv_print(result);
+        ASSERT_TRUE(strv_equal(result, STRV_MAKE(strjoina(search3, "absolute-non-empty-for-root.conf"),
+                                                 strjoina(search3, "relative-non-empty-for-root.conf"))));
+        result = strv_free(result);
+
+        ASSERT_OK(conf_files_list(&result, /* suffix = */ NULL, t, CONF_FILES_FILTER_MASKED_BY_SYMLINK, "/dir3/"));
+        strv_print(result);
+        ASSERT_TRUE(strv_equal(result, STRV_MAKE(strjoina(search3, "absolute-empty-for-root.conf"),
+                                                 strjoina(search3, "absolute-non-empty-for-root.conf"),
+                                                 strjoina(search3, "relative-empty-for-root.conf"),
+                                                 strjoina(search3, "relative-non-empty-for-root.conf"))));
+        result = strv_free(result);
+
+        ASSERT_OK(conf_files_list(&result, /* suffix = */ NULL, t, CONF_FILES_REGULAR, "/dir3/"));
+        strv_print(result);
+        ASSERT_TRUE(strv_equal(result, STRV_MAKE(strjoina(search3, "absolute-empty-for-root.conf"),
+                                                 strjoina(search3, "absolute-non-empty-for-root.conf"),
+                                                 strjoina(search3, "relative-empty-for-root.conf"),
+                                                 strjoina(search3, "relative-non-empty-for-root.conf"))));
+        result = strv_free(result);
+
+        ASSERT_OK(conf_files_list_at(&result, /* suffix = */ NULL, AT_FDCWD, CONF_FILES_FILTER_MASKED, search3));
+        strv_print(result);
+        ASSERT_TRUE(strv_equal(result, STRV_MAKE(strjoina(search3, "absolute-non-empty.conf"),
+                                                 strjoina(search3, "relative-non-empty.conf"))));
+        result = strv_free(result);
+
+        ASSERT_OK(conf_files_list_at(&result, /* suffix = */ NULL, AT_FDCWD, CONF_FILES_FILTER_MASKED_BY_EMPTY, search3));
+        strv_print(result);
+        ASSERT_TRUE(strv_equal(result, STRV_MAKE(strjoina(search3, "absolute-non-empty.conf"),
+                                                 strjoina(search3, "relative-non-empty.conf"))));
+        result = strv_free(result);
+
+        ASSERT_OK(conf_files_list_at(&result, /* suffix = */ NULL, AT_FDCWD, CONF_FILES_FILTER_MASKED_BY_SYMLINK, search3));
+        strv_print(result);
+        ASSERT_TRUE(strv_equal(result, STRV_MAKE(strjoina(search3, "absolute-empty.conf"),
+                                                 strjoina(search3, "absolute-non-empty.conf"),
+                                                 strjoina(search3, "relative-empty.conf"),
+                                                 strjoina(search3, "relative-non-empty.conf"))));
+        result = strv_free(result);
+
+        ASSERT_OK(conf_files_list_at(&result, /* suffix = */ NULL, AT_FDCWD, CONF_FILES_REGULAR, search3));
+        strv_print(result);
+        ASSERT_TRUE(strv_equal(result, STRV_MAKE(strjoina(search3, "absolute-empty.conf"),
+                                                 strjoina(search3, "absolute-non-empty.conf"),
+                                                 strjoina(search3, "relative-empty.conf"),
+                                                 strjoina(search3, "relative-non-empty.conf"))));
+        result = strv_free(result);
+
+        ASSERT_OK(conf_files_list_at(&result, /* suffix = */ NULL, tfd, CONF_FILES_FILTER_MASKED, "/dir3/"));
+        strv_print(result);
+        ASSERT_TRUE(strv_equal(result, STRV_MAKE("dir3/absolute-non-empty-for-root.conf",
+                                                 "dir3/relative-non-empty-for-root.conf")));
+        result = strv_free(result);
+
+        ASSERT_OK(conf_files_list_at(&result, /* suffix = */ NULL, tfd, CONF_FILES_FILTER_MASKED_BY_EMPTY, "/dir3/"));
+        strv_print(result);
+        ASSERT_TRUE(strv_equal(result, STRV_MAKE("dir3/absolute-non-empty-for-root.conf",
+                                                 "dir3/relative-non-empty-for-root.conf")));
+        result = strv_free(result);
+
+        ASSERT_OK(conf_files_list_at(&result, /* suffix = */ NULL, tfd, CONF_FILES_FILTER_MASKED_BY_SYMLINK, "/dir3/"));
+        strv_print(result);
+        ASSERT_TRUE(strv_equal(result, STRV_MAKE("dir3/absolute-empty-for-root.conf",
+                                                 "dir3/absolute-non-empty-for-root.conf",
+                                                 "dir3/relative-empty-for-root.conf",
+                                                 "dir3/relative-non-empty-for-root.conf")));
+        result = strv_free(result);
+
+        ASSERT_OK(conf_files_list_at(&result, /* suffix = */ NULL, tfd, CONF_FILES_REGULAR, "/dir3/"));
+        strv_print(result);
+        ASSERT_TRUE(strv_equal(result, STRV_MAKE("dir3/absolute-empty-for-root.conf",
+                                                 "dir3/absolute-non-empty-for-root.conf",
+                                                 "dir3/relative-empty-for-root.conf",
+                                                 "dir3/relative-non-empty-for-root.conf")));
+        result = strv_free(result);
+
         /* filename only */
         assert_se(conf_files_list_strv(&result, ".conf", NULL, CONF_FILES_FILTER_MASKED | CONF_FILES_BASENAME, STRV_MAKE_CONST(search1, search2)) >= 0);
         strv_print(result);
-        assert_se(strv_equal(result, STRV_MAKE("a.conf", "aa.conf", "b.conf")));
+        assert_se(strv_equal(result, STRV_MAKE("a.conf", "aa.conf", "b.conf", "mm.conf")));
 
         result = strv_free(result);
 
@@ -142,7 +284,7 @@ TEST(conf_files_list) {
 
         assert_se(conf_files_list_strv_at(&result, ".conf", AT_FDCWD, CONF_FILES_FILTER_MASKED | CONF_FILES_BASENAME, STRV_MAKE_CONST(search1, search2)) >= 0);
         strv_print(result);
-        assert_se(strv_equal(result, STRV_MAKE("a.conf", "aa.conf", "b.conf")));
+        assert_se(strv_equal(result, STRV_MAKE("a.conf", "aa.conf", "b.conf", "mm.conf")));
 
         result = strv_free(result);