]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
recurse-dir: optionally, only enumerate dentries of a specific type
authorLennart Poettering <lennart@amutable.com>
Thu, 18 Jun 2026 09:02:57 +0000 (11:02 +0200)
committerLennart Poettering <lennart@amutable.com>
Mon, 22 Jun 2026 12:40:16 +0000 (14:40 +0200)
At various places we filter directory enumerations by inode type. Let's
add explicit support for that, so that the "struct dirent" array we
return already suppresses them.

This shortens code and makes things more robust.

src/basic/recurse-dir.c
src/basic/recurse-dir.h
src/debug-generator/debug-generator.c
src/kernel-install/kernel-install.c
src/libsystemd/sd-varlink/varlink-util.c
src/nsresourced/userns-registry.c
src/shared/creds-util.c
src/storage/storagectl.c
src/test/test-recurse-dir.c

index 8f691d922945f866a100b08efcaad193c1d147a5..6e0ce5cd4bbc59a1bdd847080c60b3f1b9ae2e50 100644 (file)
@@ -27,6 +27,35 @@ static bool ignore_dirent(const struct dirent *de, RecurseDirFlags flags) {
 
         /* Depending on flag either ignore everything starting with ".", or just "." itself and ".." */
 
+        if ((flags & _RECURSE_DIR_MUST_BE_MASK) != 0) {
+                RecurseDirFlags f;
+
+                switch (de->d_type) {
+
+                case DT_DIR:
+                        f = RECURSE_DIR_MUST_BE_DIRECTORY;
+                        break;
+
+                case DT_REG:
+                        f = RECURSE_DIR_MUST_BE_REGULAR;
+                        break;
+
+                case DT_LNK:
+                        f = RECURSE_DIR_MUST_BE_SYMLINK;
+                        break;
+
+                case DT_SOCK:
+                        f = RECURSE_DIR_MUST_BE_SOCKET;
+                        break;
+
+                default:
+                        return true;
+                }
+
+                if (!FLAGS_SET(flags, f))
+                        return true;
+        }
+
         return FLAGS_SET(flags, RECURSE_DIR_IGNORE_DOT) ?
                 de->d_name[0] == '.' :
                 dot_or_dot_dot(de->d_name);
@@ -39,6 +68,9 @@ int readdir_all(int dir_fd, RecurseDirFlags flags, DirectoryEntries **ret) {
 
         assert(dir_fd >= 0);
 
+        if ((flags & _RECURSE_DIR_MUST_BE_MASK) != 0) /* We need the type to validate it */
+                flags |= RECURSE_DIR_ENSURE_TYPE;
+
         /* Returns an array with pointers to "struct dirent" directory entries, optionally sorted.
          *
          * Start with space for up to 8 directory entries. We expect at least 2 ("." + ".."), hence hopefully
@@ -87,9 +119,6 @@ int readdir_all(int dir_fd, RecurseDirFlags flags, DirectoryEntries **ret) {
         de->n_entries = 0;
         struct dirent *entry;
         FOREACH_DIRENT_IN_BUFFER(entry, de->buffer, de->buffer_size) {
-                if (ignore_dirent(entry, flags))
-                        continue;
-
                 if (FLAGS_SET(flags, RECURSE_DIR_ENSURE_TYPE)) {
                         r = dirent_ensure_type(dir_fd, entry);
                         if (r == -ENOENT)
@@ -99,6 +128,9 @@ int readdir_all(int dir_fd, RecurseDirFlags flags, DirectoryEntries **ret) {
                                 return r;
                 }
 
+                if (ignore_dirent(entry, flags))
+                        continue;
+
                 de->n_entries++;
         }
 
@@ -117,14 +149,14 @@ int readdir_all(int dir_fd, RecurseDirFlags flags, DirectoryEntries **ret) {
 
         j = 0;
         FOREACH_DIRENT_IN_BUFFER(entry, de->buffer, de->buffer_size) {
-                if (ignore_dirent(entry, flags))
-                        continue;
-
                 /* If d_type == DT_UNKNOWN that means we failed to ensure the type in the earlier loop and
                  * didn't include the dentry in de->n_entries and as such should skip it here as well. */
                 if (FLAGS_SET(flags, RECURSE_DIR_ENSURE_TYPE) && entry->d_type == DT_UNKNOWN)
                         continue;
 
+                if (ignore_dirent(entry, flags))
+                        continue;
+
                 de->entries[j++] = entry;
         }
         assert(j == de->n_entries);
@@ -166,6 +198,9 @@ int recurse_dir(
         assert(dir_fd >= 0);
         assert(func);
 
+        /* We cannot descend into dirs if we are supposed to ignore them */
+        assert((flags & _RECURSE_DIR_MUST_BE_MASK) == 0 || FLAGS_SET(flags, RECURSE_DIR_MUST_BE_DIRECTORY));
+
         /* This is a lot like ftw()/nftw(), but a lot more modern, i.e. built around openat()/statx()/O_PATH,
          * and under the assumption that fds are not as 'expensive' as they used to be. */
 
index af33c239f7017166f51c5f62abd2734d5bc74512..6b6eac3639d9b60907e7a3793310b07b35778511 100644 (file)
@@ -7,14 +7,19 @@
 
 typedef enum RecurseDirFlags {
         /* Interpreted by readdir_all() */
-        RECURSE_DIR_SORT         = 1 << 0,  /* sort file directory entries before processing them */
-        RECURSE_DIR_IGNORE_DOT   = 1 << 1,  /* ignore all dot files ("." and ".." are always ignored) */
-        RECURSE_DIR_ENSURE_TYPE  = 1 << 2,  /* guarantees that 'd_type' field of 'de' is not DT_UNKNOWN */
+        RECURSE_DIR_SORT              = 1 << 0,  /* sort file directory entries before processing them */
+        RECURSE_DIR_IGNORE_DOT        = 1 << 1,  /* ignore all dot files ("." and ".." are always ignored) */
+        RECURSE_DIR_ENSURE_TYPE       = 1 << 2,  /* guarantees that 'd_type' field of 'de' is not DT_UNKNOWN */
+        RECURSE_DIR_MUST_BE_DIRECTORY = 1 << 3,  /* ignore all entries that aren't directories */
+        RECURSE_DIR_MUST_BE_REGULAR   = 1 << 4,  /* ignore all entries that aren't regular files */
+        RECURSE_DIR_MUST_BE_SYMLINK   = 1 << 5,  /* ignore all entries that aren't symlinks */
+        RECURSE_DIR_MUST_BE_SOCKET    = 1 << 6,  /* ignore all entries that aren't socket */
+        _RECURSE_DIR_MUST_BE_MASK     = RECURSE_DIR_MUST_BE_DIRECTORY|RECURSE_DIR_MUST_BE_REGULAR|RECURSE_DIR_MUST_BE_SYMLINK|RECURSE_DIR_MUST_BE_SOCKET,
 
         /* Interpreted by recurse_dir() */
-        RECURSE_DIR_SAME_MOUNT   = 1 << 3,  /* skips over subdirectories that are submounts */
-        RECURSE_DIR_INODE_FD     = 1 << 4,  /* passes an opened inode fd (O_DIRECTORY fd in case of dirs, O_PATH otherwise) */
-        RECURSE_DIR_TOPLEVEL     = 1 << 5,  /* call RECURSE_DIR_ENTER/RECURSE_DIR_LEAVE once for top-level dir, too, with dir_fd=-1 and NULL dirent */
+        RECURSE_DIR_SAME_MOUNT        = 1 << 7,  /* skips over subdirectories that are submounts */
+        RECURSE_DIR_INODE_FD          = 1 << 8,  /* passes an opened inode fd (O_DIRECTORY fd in case of dirs, O_PATH otherwise) */
+        RECURSE_DIR_TOPLEVEL          = 1 << 9,  /* call RECURSE_DIR_ENTER/RECURSE_DIR_LEAVE once for top-level dir, too, with dir_fd=-1 and NULL dirent */
 } RecurseDirFlags;
 
 typedef struct DirectoryEntries {
index 9ef271343cc596fd15afacd914cec0fcf5038b67..700ad528930b3ff86988d674cfab7ed114a40b39 100644 (file)
@@ -279,7 +279,7 @@ static int process_unit_credentials(const char *credentials_dir) {
 
         assert(credentials_dir);
 
-        r = readdir_all_at(AT_FDCWD, credentials_dir, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &des);
+        r = readdir_all_at(AT_FDCWD, credentials_dir, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_MUST_BE_REGULAR, &des);
         if (r < 0)
                 return log_error_errno(r, "Failed to enumerate credentials from credentials directory '%s': %m", credentials_dir);
 
@@ -287,9 +287,6 @@ static int process_unit_credentials(const char *credentials_dir) {
                 struct dirent *de = *i;
                 const char *unit, *dropin;
 
-                if (de->d_type != DT_REG)
-                        continue;
-
                 unit = startswith(de->d_name, "systemd.extra-unit.");
                 dropin = startswith(de->d_name, "systemd.unit-dropin.");
 
index c77b23d4bc0f8ada71d12841c57c51028b72acba..132ae803dfdf91252c85ab0333c461eba13ee240 100644 (file)
@@ -11,7 +11,6 @@
 #include "build.h"
 #include "chase.h"
 #include "conf-files.h"
-#include "dirent-util.h"
 #include "dissect-image.h"
 #include "env-file.h"
 #include "env-util.h"
@@ -1247,21 +1246,11 @@ static int verb_add_all(int argc, char *argv[], uintptr_t _data, void *userdata)
                 return log_error_errno(fd, "Failed to open %s/usr/lib/modules/: %m", strempty(arg_root));
 
         _cleanup_free_ DirectoryEntries *de = NULL;
-        r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT, &de);
+        r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_MUST_BE_DIRECTORY, &de);
         if (r < 0)
                 return log_error_errno(r, "Failed to numerate /usr/lib/modules/ contents: %m");
 
         FOREACH_ARRAY(d, de->entries, de->n_entries) {
-                r = dirent_ensure_type(fd, *d);
-                if (r < 0) {
-                        if (r != -ENOENT) /* don't log if just gone by now */
-                                log_debug_errno(r, "Failed to check if '%s/usr/lib/modules/%s' is a directory, ignoring: %m", strempty(arg_root), (*d)->d_name);
-                        continue;
-                }
-
-                if ((*d)->d_type != DT_DIR)
-                        continue;
-
                 _cleanup_free_ char *fn = path_join((*d)->d_name, "vmlinuz");
                 if (!fn)
                         return log_oom();
@@ -1483,7 +1472,7 @@ static int verb_list(int argc, char *argv[], uintptr_t _data, void *userdata) {
                 return log_error_errno(fd, "Failed to open %s/usr/lib/modules/: %m", strempty(arg_root));
 
         _cleanup_free_ DirectoryEntries *de = NULL;
-        r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT, &de);
+        r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_MUST_BE_DIRECTORY, &de);
         if (r < 0)
                 return log_error_errno(r, "Failed to numerate /usr/lib/modules/ contents: %m");
 
@@ -1501,16 +1490,6 @@ static int verb_list(int argc, char *argv[], uintptr_t _data, void *userdata) {
                 if (!j)
                         return log_oom();
 
-                r = dirent_ensure_type(fd, *d);
-                if (r < 0) {
-                        if (r != -ENOENT) /* don't log if just gone by now */
-                                log_debug_errno(r, "Failed to check if '%s/%s' is a directory, ignoring: %m", strempty(arg_root), j);
-                        continue;
-                }
-
-                if ((*d)->d_type != DT_DIR)
-                        continue;
-
                 _cleanup_free_ char *fn = path_join((*d)->d_name, "vmlinuz");
                 if (!fn)
                         return log_oom();
index 1eace7724ceb55f43e26a11c9e1a6966f1044d17..d454fc7f48255e2192e614d220ac5ac43d09bb2c 100644 (file)
@@ -272,7 +272,7 @@ ssize_t varlink_execute_directory(
                 return log_debug_errno(errno, "Failed to open '%s': %m", path);
 
         _cleanup_free_ DirectoryEntries *dentries = NULL;
-        r = readdir_all(fd, RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &dentries);
+        r = readdir_all(fd, RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_MUST_BE_SOCKET|RECURSE_DIR_MUST_BE_SYMLINK, &dentries);
         if (r < 0)
                 return log_debug_errno(r, "Failed to enumerate '%s': %m", path);
 
@@ -282,9 +282,6 @@ ssize_t varlink_execute_directory(
         FOREACH_ARRAY(dp, dentries->entries, dentries->n_entries) {
                 struct dirent *de = *dp;
 
-                if (!IN_SET(de->d_type, DT_SOCK, DT_LNK))
-                        continue;
-
                 t++;
 
                 _cleanup_free_ char *j = path_join(path, de->d_name);
index 0bfb1edbded6af58c3fa01e0ecba2d6939bcd874..0da6a6a7c692b98307e3c0d2911740b98ade33b2 100644 (file)
@@ -1219,7 +1219,7 @@ int userns_registry_per_uid(int dir_fd, uid_t owner) {
 
         _cleanup_free_ DirectoryEntries *de = NULL;
 
-        r = readdir_all_at(dir_fd, uid_fn, RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &de);
+        r = readdir_all_at(dir_fd, uid_fn, RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_MUST_BE_REGULAR, &de);
         if (r == -ENOENT)
                 return 0;
         if (r < 0)
@@ -1228,9 +1228,6 @@ int userns_registry_per_uid(int dir_fd, uid_t owner) {
         FOREACH_ARRAY(i, de->entries, de->n_entries) {
                 struct dirent *e = *i;
 
-                if (e->d_type != DT_REG)
-                        continue;
-
                 if (!startswith(e->d_name, "i") || !endswith(e->d_name, ".userns"))
                         continue;
 
index e20b64291ae2555ec55f8af8020d3791dfa62d5a..739973aa46130f9b8811472f03a17841a5cb3811 100644 (file)
@@ -1805,16 +1805,13 @@ int pick_up_credentials(const PickUpCredential *table, size_t n_table_entry) {
                 return log_error_errno(credential_dir_fd, "Failed to open credentials directory: %m");
 
         _cleanup_free_ DirectoryEntries *des = NULL;
-        r = readdir_all(credential_dir_fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &des);
+        r = readdir_all(credential_dir_fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_MUST_BE_REGULAR, &des);
         if (r < 0)
                 return log_error_errno(r, "Failed to enumerate credentials: %m");
 
         FOREACH_ARRAY(i, des->entries, des->n_entries) {
                 struct dirent *de = *i;
 
-                if (de->d_type != DT_REG)
-                        continue;
-
                 FOREACH_ARRAY(t, table, n_table_entry) {
                         r = pick_up_credential_one(credential_dir_fd, de->d_name, t);
                         if (r != 0) {
index 7e366c11a912b819e1350fd5b85d2910f0a810fb..b899513616dc52e19a8929ccc6d8287e7c18fa35 100644 (file)
@@ -348,16 +348,13 @@ static int verb_providers(int argc, char *argv[], uintptr_t data, void *userdata
                         return log_error_errno(errno, "Failed to open '%s': %m", socket_path);
         } else {
                 _cleanup_free_ DirectoryEntries *dentries = NULL;
-                r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &dentries);
+                r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_MUST_BE_SOCKET, &dentries);
                 if (r < 0)
                         return log_error_errno(r, "Failed to enumerate '%s': %m", socket_path);
 
                 FOREACH_ARRAY(dp, dentries->entries, dentries->n_entries) {
                         struct dirent *de = *dp;
 
-                        if (de->d_type != DT_SOCK)
-                                continue;
-
                         if (!storage_provider_name_is_valid(de->d_name))
                                 continue;
 
index b29a7eab9d0ab4d29572be7b065d1065f1e0407d..453b89d172498d21e0b2574339cdd4ab30c26e71 100644 (file)
@@ -2,14 +2,17 @@
 
 #include <ftw.h>
 #include <linux/magic.h>
+#include <sys/stat.h>
 
 #include "fd-util.h"
 #include "log.h"
 #include "recurse-dir.h"
+#include "rm-rf.h"
 #include "stat-util.h"
 #include "strv.h"
 #include "tests.h"
 #include "time-util.h"
+#include "tmpfile-util.h"
 
 static char **list_nftw = NULL;
 
@@ -119,6 +122,71 @@ static int recurse_dir_callback(
         return RECURSE_DIR_CONTINUE;
 }
 
+static void assert_entries(DirectoryEntries *de, char **expected) {
+        ASSERT_NOT_NULL(de);
+
+        /* Verifies that the directory entries enumerated in 'de' are exactly the ones listed in
+         * 'expected' (order is irrelevant). */
+
+        ASSERT_EQ(de->n_entries, strv_length(expected));
+
+        FOREACH_ARRAY(i, de->entries, de->n_entries)
+                ASSERT_TRUE(strv_contains(expected, (*i)->d_name));
+
+        STRV_FOREACH(e, expected) {
+                bool found = false;
+
+                FOREACH_ARRAY(i, de->entries, de->n_entries)
+                        if (streq((*i)->d_name, *e)) {
+                                found = true;
+                                break;
+                        }
+
+                ASSERT_TRUE(found);
+        }
+}
+
+static void check_readdir_all(int tfd, RecurseDirFlags flags, char **expected) {
+        _cleanup_free_ DirectoryEntries *de = NULL;
+        _cleanup_close_ int fd = -EBADF;
+
+        /* readdir_all() consumes the directory fd offset, hence reopen a fresh fd for each enumeration. */
+        ASSERT_OK(fd = fd_reopen(tfd, O_DIRECTORY|O_CLOEXEC));
+        ASSERT_OK(readdir_all(fd, flags, &de));
+        assert_entries(de, expected);
+}
+
+static void test_must_be_flags(void) {
+        _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+        _cleanup_close_ int tfd = -EBADF, reg_fd = -EBADF;
+
+        log_info("/* %s */", __func__);
+
+        /* Populate a temporary directory with one entry of each interesting type and verify that the
+         * RECURSE_DIR_MUST_BE_* flags select exactly the right subset. */
+
+        ASSERT_OK(tfd = mkdtemp_open(NULL, O_DIRECTORY|O_CLOEXEC, &t));
+
+        ASSERT_OK_ERRNO(mkdirat(tfd, "dir", 0777));
+        ASSERT_OK_ERRNO(reg_fd = openat(tfd, "reg", O_CREAT|O_EXCL|O_WRONLY|O_CLOEXEC, 0666));
+        reg_fd = safe_close(reg_fd);
+        ASSERT_OK_ERRNO(symlinkat("reg", tfd, "lnk"));
+        ASSERT_OK_ERRNO(mknodat(tfd, "sock", S_IFSOCK|0666, 0));
+
+        /* Without any MUST_BE flag we get all four entries. */
+        check_readdir_all(tfd, 0, STRV_MAKE("dir", "reg", "lnk", "sock"));
+
+        /* A single MUST_BE flag selects exactly the entries of the matching type. */
+        check_readdir_all(tfd, RECURSE_DIR_MUST_BE_DIRECTORY, STRV_MAKE("dir"));
+        check_readdir_all(tfd, RECURSE_DIR_MUST_BE_REGULAR, STRV_MAKE("reg"));
+        check_readdir_all(tfd, RECURSE_DIR_MUST_BE_SYMLINK, STRV_MAKE("lnk"));
+        check_readdir_all(tfd, RECURSE_DIR_MUST_BE_SOCKET, STRV_MAKE("sock"));
+
+        /* The flags may be combined, in which case we get the union of the matching entries. */
+        check_readdir_all(tfd, RECURSE_DIR_MUST_BE_REGULAR|RECURSE_DIR_MUST_BE_SYMLINK, STRV_MAKE("reg", "lnk"));
+        check_readdir_all(tfd, RECURSE_DIR_MUST_BE_DIRECTORY|RECURSE_DIR_MUST_BE_SOCKET, STRV_MAKE("dir", "sock"));
+}
+
 int main(int argc, char *argv[]) {
         _cleanup_strv_free_ char **list_recurse_dir = NULL;
         const char *p;
@@ -128,6 +196,8 @@ int main(int argc, char *argv[]) {
         log_show_color(true);
         test_setup_logging(LOG_INFO);
 
+        test_must_be_flags();
+
         if (argc > 1)
                 p = argv[1];
         else