]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
copy: walk source directories in sorted order in copy_tree()
authorPaul Meyer <katexochen0@gmail.com>
Tue, 2 Jun 2026 10:18:45 +0000 (12:18 +0200)
committerPaul Meyer <katexochen0@gmail.com>
Sun, 7 Jun 2026 11:41:18 +0000 (13:41 +0200)
fd_copy_directory() iterates the source directory with FOREACH_DIRENT_ALL,
i.e. plain readdir(). For ext4 sources (and similar) that order depends on
the directory hash and varies between hosts, which leaks into destinations
that record entries in insertion order. systemd-repart's
partition_populate_directory() stages CopyFiles= into a temp directory and
then later builds a filesystem from it; with the unsorted walk the staging
order ended up baked into the output (most visibly when the destination is
vfat, where dir entries are stored in insertion order).

Replace the raw readdir loop with readdir_all(RECURSE_DIR_SORT), which
collects and qsort()s the entries by name. Dot/dot-dot are filtered by
readdir_all() so the explicit dot_or_dot_dot() guard is dropped.

Co-developed-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
src/shared/copy.c

index fbcb8e2a30fd253ec1b88795add2e2c3514788a3..f61b16caee0b4d7f651df4b32b06f0ef6925f3a8 100644 (file)
@@ -25,6 +25,7 @@
 #include "mountpoint-util.h"
 #include "nulstr-util.h"
 #include "path-util.h"
+#include "recurse-dir.h"
 #include "rm-rf.h"
 #include "selinux-util.h"
 #include "signal-util.h"
@@ -1044,6 +1045,7 @@ static int fd_copy_directory(
                 .parent_fd = -EBADF,
         };
 
+        _cleanup_free_ DirectoryEntries *des = NULL;
         _cleanup_close_ int fdf = -EBADF, fdt = -EBADF;
         _cleanup_closedir_ DIR *d = NULL;
         struct stat dt_st;
@@ -1107,14 +1109,20 @@ static int fd_copy_directory(
                 goto finish;
         }
 
-        FOREACH_DIRENT_ALL(de, d, return -errno) {
+        /* Walk children in deterministic (alphabetical) order. The natural readdir() order depends on the
+         * source filesystem's directory storage (e.g. ext4 dir hash) and varies across hosts, which leaks
+         * into the destination when it records entries in insertion order (e.g. vfat). Sorting here keeps
+         * copy_tree() reproducible regardless of the source filesystem layout. */
+        r = readdir_all(dirfd(d), RECURSE_DIR_SORT, &des);
+        if (r < 0)
+                return r;
+
+        FOREACH_ARRAY(i, des->entries, des->n_entries) {
+                struct dirent *de = *i;
                 const char *child_display_path = NULL;
                 _cleanup_free_ char *dp = NULL;
                 struct stat buf;
 
-                if (dot_or_dot_dot(de->d_name))
-                        continue;
-
                 r = look_for_signals(copy_flags);
                 if (r < 0)
                         return r;