Coverage Report

Created: 2025-06-13 06:27

/src/systemd/src/basic/recurse-dir.c
Line
Count
Source (jump to first uncovered line)
1
/* SPDX-License-Identifier: LGPL-2.1-or-later */
2
3
#include <sys/stat.h>
4
5
#include "alloc-util.h"
6
#include "dirent-util.h"
7
#include "fd-util.h"
8
#include "fs-util.h"
9
#include "log.h"
10
#include "mountpoint-util.h"
11
#include "path-util.h"
12
#include "recurse-dir.h"
13
#include "sort-util.h"
14
15
0
#define DEFAULT_RECURSION_MAX 100
16
17
0
static int sort_func(struct dirent * const *a, struct dirent * const *b) {
18
0
        return strcmp((*a)->d_name, (*b)->d_name);
19
0
}
20
21
0
static bool ignore_dirent(const struct dirent *de, RecurseDirFlags flags) {
22
0
        assert(de);
23
24
        /* Depending on flag either ignore everything starting with ".", or just "." itself and ".." */
25
26
0
        return FLAGS_SET(flags, RECURSE_DIR_IGNORE_DOT) ?
27
0
                de->d_name[0] == '.' :
28
0
                dot_or_dot_dot(de->d_name);
29
0
}
30
31
0
int readdir_all(int dir_fd, RecurseDirFlags flags, DirectoryEntries **ret) {
32
0
        _cleanup_free_ DirectoryEntries *de = NULL;
33
0
        DirectoryEntries *nde;
34
0
        int r;
35
36
0
        assert(dir_fd >= 0);
37
38
        /* Returns an array with pointers to "struct dirent" directory entries, optionally sorted. Free the
39
         * array with readdir_all_freep().
40
         *
41
         * Start with space for up to 8 directory entries. We expect at least 2 ("." + ".."), hence hopefully
42
         * 8 will cover most cases comprehensively. (Note that most likely a lot more entries will actually
43
         * fit in the buffer, given we calculate maximum file name length here.) */
44
0
        de = malloc(offsetof(DirectoryEntries, buffer) + DIRENT_SIZE_MAX * 8);
45
0
        if (!de)
46
0
                return -ENOMEM;
47
48
0
        de->buffer_size = 0;
49
0
        for (;;) {
50
0
                size_t bs;
51
0
                ssize_t n;
52
53
0
                bs = MIN(MALLOC_SIZEOF_SAFE(de) - offsetof(DirectoryEntries, buffer), (size_t) SSIZE_MAX);
54
0
                assert(bs > de->buffer_size);
55
56
0
                n = posix_getdents(dir_fd, (uint8_t*) de->buffer + de->buffer_size, bs - de->buffer_size, /* flags = */ 0);
57
0
                if (n < 0)
58
0
                        return -errno;
59
0
                if (n == 0)
60
0
                        break;
61
62
0
                msan_unpoison((uint8_t*) de->buffer + de->buffer_size, n);
63
64
0
                de->buffer_size += n;
65
66
0
                if (de->buffer_size < bs - DIRENT_SIZE_MAX) /* Still room for one more entry, then try to
67
                                                             * fill it up without growing the structure. */
68
0
                        continue;
69
70
0
                if (bs >= SSIZE_MAX - offsetof(DirectoryEntries, buffer))
71
0
                        return -EFBIG;
72
0
                bs = bs >= (SSIZE_MAX - offsetof(DirectoryEntries, buffer))/2 ? SSIZE_MAX - offsetof(DirectoryEntries, buffer) : bs * 2;
73
74
0
                nde = realloc(de, bs);
75
0
                if (!nde)
76
0
                        return -ENOMEM;
77
0
                de = nde;
78
0
        }
79
80
0
        de->n_entries = 0;
81
0
        struct dirent *entry;
82
0
        FOREACH_DIRENT_IN_BUFFER(entry, de->buffer, de->buffer_size) {
83
0
                if (ignore_dirent(entry, flags))
84
0
                        continue;
85
86
0
                if (FLAGS_SET(flags, RECURSE_DIR_ENSURE_TYPE)) {
87
0
                        r = dirent_ensure_type(dir_fd, entry);
88
0
                        if (r == -ENOENT)
89
                                /* dentry gone by now? no problem, let's just suppress it */
90
0
                                continue;
91
0
                        if (r < 0)
92
0
                                return r;
93
0
                }
94
95
0
                de->n_entries++;
96
0
        }
97
98
0
        size_t sz, j;
99
100
0
        sz = ALIGN(offsetof(DirectoryEntries, buffer) + de->buffer_size);
101
0
        if (!INC_SAFE(&sz, sizeof(struct dirent*) * de->n_entries))
102
0
                return -ENOMEM;
103
104
0
        nde = realloc(de, sz);
105
0
        if (!nde)
106
0
                return -ENOMEM;
107
0
        de = nde;
108
109
0
        de->entries = (struct dirent**) ((uint8_t*) de + ALIGN(offsetof(DirectoryEntries, buffer) + de->buffer_size));
110
111
0
        j = 0;
112
0
        FOREACH_DIRENT_IN_BUFFER(entry, de->buffer, de->buffer_size) {
113
0
                if (ignore_dirent(entry, flags))
114
0
                        continue;
115
116
                /* If d_type == DT_UNKNOWN that means we failed to ensure the type in the earlier loop and
117
                 * didn't include the dentry in de->n_entries and as such should skip it here as well. */
118
0
                if (FLAGS_SET(flags, RECURSE_DIR_ENSURE_TYPE) && entry->d_type == DT_UNKNOWN)
119
0
                        continue;
120
121
0
                de->entries[j++] = entry;
122
0
        }
123
0
        assert(j == de->n_entries);
124
125
0
        if (FLAGS_SET(flags, RECURSE_DIR_SORT))
126
0
                typesafe_qsort(de->entries, de->n_entries, sort_func);
127
128
0
        if (ret)
129
0
                *ret = TAKE_PTR(de);
130
131
0
        return 0;
132
0
}
133
134
0
int readdir_all_at(int fd, const char *path, RecurseDirFlags flags, DirectoryEntries **ret) {
135
0
        _cleanup_close_ int dir_fd = -EBADF;
136
137
0
        assert(fd >= 0 || fd == AT_FDCWD);
138
139
0
        dir_fd = xopenat(fd, path, O_DIRECTORY|O_CLOEXEC);
140
0
        if (dir_fd < 0)
141
0
                return dir_fd;
142
143
0
        return readdir_all(dir_fd, flags, ret);
144
0
}
145
146
int recurse_dir(
147
                int dir_fd,
148
                const char *path,
149
                unsigned statx_mask,
150
                unsigned n_depth_max,
151
                RecurseDirFlags flags,
152
                recurse_dir_func_t func,
153
0
                void *userdata) {
154
155
0
        _cleanup_free_ DirectoryEntries *de = NULL;
156
0
        struct statx root_sx;
157
0
        int r;
158
159
0
        assert(dir_fd >= 0);
160
0
        assert(func);
161
162
        /* This is a lot like ftw()/nftw(), but a lot more modern, i.e. built around openat()/statx()/O_PATH,
163
         * and under the assumption that fds are not as 'expensive' as they used to be. */
164
165
0
        if (n_depth_max == 0)
166
0
                return -EOVERFLOW;
167
0
        if (n_depth_max == UINT_MAX) /* special marker for "default" */
168
0
                n_depth_max = DEFAULT_RECURSION_MAX;
169
170
0
        if (FLAGS_SET(flags, RECURSE_DIR_TOPLEVEL)) {
171
0
                if (statx_mask != 0) {
172
0
                        if (statx(dir_fd, "", AT_EMPTY_PATH, statx_mask, &root_sx) < 0)
173
0
                                return -errno;
174
0
                }
175
176
0
                r = func(RECURSE_DIR_ENTER,
177
0
                         path,
178
0
                         -1, /* we have no parent fd */
179
0
                         dir_fd,
180
0
                         NULL, /* we have no dirent */
181
0
                         statx_mask != 0 ? &root_sx : NULL,
182
0
                         userdata);
183
0
                if (IN_SET(r, RECURSE_DIR_LEAVE_DIRECTORY, RECURSE_DIR_SKIP_ENTRY))
184
0
                        return 0;
185
0
                if (r != RECURSE_DIR_CONTINUE)
186
0
                        return r;
187
0
        }
188
189
        /* Mask out RECURSE_DIR_ENSURE_TYPE so we can do it ourselves and avoid an extra statx() call. */
190
0
        r = readdir_all(dir_fd, flags & ~RECURSE_DIR_ENSURE_TYPE, &de);
191
0
        if (r < 0)
192
0
                return r;
193
194
0
        for (size_t i = 0; i < de->n_entries; i++) {
195
0
                _cleanup_close_ int inode_fd = -EBADF, subdir_fd = -EBADF;
196
0
                _cleanup_free_ char *joined = NULL;
197
0
                struct statx sx;
198
0
                bool sx_valid = false;
199
0
                const char *p;
200
201
                /* For each directory entry we'll do one of the following:
202
                 *
203
                 * 1) If the entry refers to a directory, we'll open it as O_DIRECTORY 'subdir_fd' and then statx() the opened directory via that new fd (if requested)
204
                 * 2) Otherwise, if RECURSE_DIR_INODE_FD is set we'll open it as O_PATH 'inode_fd' and then statx() the opened inode via that new fd (if requested)
205
                 * 3) Otherwise, we'll statx() the directory entry via the directory fd we are currently looking at (if requested)
206
                 */
207
208
0
                if (path) {
209
0
                        joined = path_join(path, de->entries[i]->d_name);
210
0
                        if (!joined)
211
0
                                return -ENOMEM;
212
213
0
                        p = joined;
214
0
                } else
215
0
                        p = de->entries[i]->d_name;
216
217
0
                if (IN_SET(de->entries[i]->d_type, DT_UNKNOWN, DT_DIR)) {
218
0
                        subdir_fd = openat(dir_fd, de->entries[i]->d_name, O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC);
219
0
                        if (subdir_fd < 0) {
220
0
                                if (errno == ENOENT) /* Vanished by now, go for next file immediately */
221
0
                                        continue;
222
223
                                /* If it is a subdir but we failed to open it, then fail */
224
0
                                if (!IN_SET(errno, ENOTDIR, ELOOP)) {
225
0
                                        log_debug_errno(errno, "Failed to open directory '%s': %m", p);
226
227
0
                                        assert(errno <= RECURSE_DIR_SKIP_OPEN_DIR_ERROR_MAX - RECURSE_DIR_SKIP_OPEN_DIR_ERROR_BASE);
228
229
0
                                        r = func(RECURSE_DIR_SKIP_OPEN_DIR_ERROR_BASE + errno,
230
0
                                                 p,
231
0
                                                 dir_fd,
232
0
                                                 -1,
233
0
                                                 de->entries[i],
234
0
                                                 NULL,
235
0
                                                 userdata);
236
0
                                        if (r == RECURSE_DIR_LEAVE_DIRECTORY)
237
0
                                                break;
238
0
                                        if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
239
0
                                                return r;
240
241
0
                                        continue;
242
0
                                }
243
244
                                /* If it's not a subdir, then let's handle it like a regular inode below */
245
246
0
                        } else {
247
                                /* If we managed to get a DIR* off the inode, it's definitely a directory. */
248
0
                                de->entries[i]->d_type = DT_DIR;
249
250
0
                                if (statx_mask != 0 || (flags & RECURSE_DIR_SAME_MOUNT)) {
251
0
                                        if (statx(subdir_fd, "", AT_EMPTY_PATH, statx_mask, &sx) < 0)
252
0
                                                return -errno;
253
254
0
                                        sx_valid = true;
255
0
                                }
256
0
                        }
257
0
                }
258
259
0
                if (subdir_fd < 0) {
260
                        /* It's not a subdirectory. */
261
262
0
                        if (flags & RECURSE_DIR_INODE_FD) {
263
264
0
                                inode_fd = openat(dir_fd, de->entries[i]->d_name, O_PATH|O_NOFOLLOW|O_CLOEXEC);
265
0
                                if (inode_fd < 0) {
266
0
                                        if (errno == ENOENT) /* Vanished by now, go for next file immediately */
267
0
                                                continue;
268
269
0
                                        log_debug_errno(errno, "Failed to open directory entry '%s': %m", p);
270
271
0
                                        assert(errno <= RECURSE_DIR_SKIP_OPEN_INODE_ERROR_MAX - RECURSE_DIR_SKIP_OPEN_INODE_ERROR_BASE);
272
273
0
                                        r = func(RECURSE_DIR_SKIP_OPEN_INODE_ERROR_BASE + errno,
274
0
                                                 p,
275
0
                                                 dir_fd,
276
0
                                                 -1,
277
0
                                                 de->entries[i],
278
0
                                                 NULL,
279
0
                                                 userdata);
280
0
                                        if (r == RECURSE_DIR_LEAVE_DIRECTORY)
281
0
                                                break;
282
0
                                        if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
283
0
                                                return r;
284
285
0
                                        continue;
286
0
                                }
287
288
                                /* If we open the inode, then verify it's actually a non-directory, like we
289
                                 * assume. Let's guarantee that we never pass statx data of a directory where
290
                                 * caller expects a non-directory */
291
292
0
                                if (statx(inode_fd, "", AT_EMPTY_PATH, statx_mask | STATX_TYPE, &sx) < 0)
293
0
                                        return -errno;
294
295
0
                                assert(sx.stx_mask & STATX_TYPE);
296
0
                                sx_valid = true;
297
298
0
                                if (S_ISDIR(sx.stx_mode)) {
299
                                        /* What? It's a directory now? Then someone must have quickly
300
                                         * replaced it. Let's handle that gracefully: convert it to a
301
                                         * directory fd — which should be riskless now that we pinned the
302
                                         * inode. */
303
304
0
                                        subdir_fd = fd_reopen(inode_fd, O_DIRECTORY|O_CLOEXEC);
305
0
                                        if (subdir_fd < 0)
306
0
                                                return subdir_fd;
307
308
0
                                        inode_fd = safe_close(inode_fd);
309
0
                                }
310
311
0
                        } else if (statx_mask != 0 || (de->entries[i]->d_type == DT_UNKNOWN && (flags & RECURSE_DIR_ENSURE_TYPE))) {
312
313
0
                                if (statx(dir_fd, de->entries[i]->d_name, AT_SYMLINK_NOFOLLOW, statx_mask | STATX_TYPE, &sx) < 0) {
314
0
                                        if (errno == ENOENT) /* Vanished by now? Go for next file immediately */
315
0
                                                continue;
316
317
0
                                        log_debug_errno(errno, "Failed to stat directory entry '%s': %m", p);
318
319
0
                                        assert(errno <= RECURSE_DIR_SKIP_STAT_INODE_ERROR_MAX - RECURSE_DIR_SKIP_STAT_INODE_ERROR_BASE);
320
321
0
                                        r = func(RECURSE_DIR_SKIP_STAT_INODE_ERROR_BASE + errno,
322
0
                                                 p,
323
0
                                                 dir_fd,
324
0
                                                 -1,
325
0
                                                 de->entries[i],
326
0
                                                 NULL,
327
0
                                                 userdata);
328
0
                                        if (r == RECURSE_DIR_LEAVE_DIRECTORY)
329
0
                                                break;
330
0
                                        if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
331
0
                                                return r;
332
333
0
                                        continue;
334
0
                                }
335
336
0
                                assert(sx.stx_mask & STATX_TYPE);
337
0
                                sx_valid = true;
338
339
0
                                if (S_ISDIR(sx.stx_mode)) {
340
                                        /* So it suddenly is a directory, but we couldn't open it as such
341
                                         * earlier?  That is weird, and probably means somebody is racing
342
                                         * against us. We could of course retry and open it as a directory
343
                                         * again, but the chance to win here is limited. Hence, let's
344
                                         * propagate this as EISDIR error instead. That way we make this
345
                                         * something that can be reasonably handled, even though we give the
346
                                         * guarantee that RECURSE_DIR_ENTRY is strictly issued for
347
                                         * non-directory dirents. */
348
349
0
                                        log_debug("Non-directory entry '%s' suddenly became a directory.", p);
350
351
0
                                        r = func(RECURSE_DIR_SKIP_STAT_INODE_ERROR_BASE + EISDIR,
352
0
                                                 p,
353
0
                                                 dir_fd,
354
0
                                                 -1,
355
0
                                                 de->entries[i],
356
0
                                                 NULL,
357
0
                                                 userdata);
358
0
                                        if (r == RECURSE_DIR_LEAVE_DIRECTORY)
359
0
                                                break;
360
0
                                        if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
361
0
                                                return r;
362
363
0
                                        continue;
364
0
                                }
365
0
                        }
366
0
                }
367
368
0
                if (sx_valid) {
369
                        /* Copy over the data we acquired through statx() if we acquired any */
370
0
                        if (sx.stx_mask & STATX_TYPE) {
371
0
                                assert((subdir_fd < 0) == !S_ISDIR(sx.stx_mode));
372
0
                                de->entries[i]->d_type = IFTODT(sx.stx_mode);
373
0
                        }
374
375
0
                        if (sx.stx_mask & STATX_INO)
376
0
                                de->entries[i]->d_ino = sx.stx_ino;
377
0
                }
378
379
0
                if (subdir_fd >= 0) {
380
0
                        if (FLAGS_SET(flags, RECURSE_DIR_SAME_MOUNT)) {
381
0
                                bool is_mount;
382
383
0
                                if (sx_valid && FLAGS_SET(sx.stx_attributes_mask, STATX_ATTR_MOUNT_ROOT))
384
0
                                        is_mount = FLAGS_SET(sx.stx_attributes, STATX_ATTR_MOUNT_ROOT);
385
0
                                else {
386
0
                                        r = is_mount_point_at(dir_fd, de->entries[i]->d_name, 0);
387
0
                                        if (r < 0)
388
0
                                                log_debug_errno(r, "Failed to determine whether %s is a submount, assuming not: %m", p);
389
390
0
                                        is_mount = r > 0;
391
0
                                }
392
393
0
                                if (is_mount) {
394
0
                                        r = func(RECURSE_DIR_SKIP_MOUNT,
395
0
                                                 p,
396
0
                                                 dir_fd,
397
0
                                                 subdir_fd,
398
0
                                                 de->entries[i],
399
0
                                                 statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
400
0
                                                 userdata);
401
0
                                        if (r == RECURSE_DIR_LEAVE_DIRECTORY)
402
0
                                                break;
403
0
                                        if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
404
0
                                                return r;
405
406
0
                                        continue;
407
0
                                }
408
0
                        }
409
410
0
                        if (n_depth_max <= 1) {
411
                                /* When we reached max depth, generate a special event */
412
413
0
                                r = func(RECURSE_DIR_SKIP_DEPTH,
414
0
                                         p,
415
0
                                         dir_fd,
416
0
                                         subdir_fd,
417
0
                                         de->entries[i],
418
0
                                         statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
419
0
                                         userdata);
420
0
                                if (r == RECURSE_DIR_LEAVE_DIRECTORY)
421
0
                                        break;
422
0
                                if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
423
0
                                        return r;
424
425
0
                                continue;
426
0
                        }
427
428
0
                        r = func(RECURSE_DIR_ENTER,
429
0
                                 p,
430
0
                                 dir_fd,
431
0
                                 subdir_fd,
432
0
                                 de->entries[i],
433
0
                                 statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
434
0
                                 userdata);
435
0
                        if (r == RECURSE_DIR_LEAVE_DIRECTORY)
436
0
                                break;
437
0
                        if (r == RECURSE_DIR_SKIP_ENTRY)
438
0
                                continue;
439
0
                        if (r != RECURSE_DIR_CONTINUE)
440
0
                                return r;
441
442
0
                        r = recurse_dir(subdir_fd,
443
0
                                        p,
444
0
                                        statx_mask,
445
0
                                        n_depth_max - 1,
446
0
                                        flags &~ RECURSE_DIR_TOPLEVEL, /* we already called the callback for this entry */
447
0
                                        func,
448
0
                                        userdata);
449
0
                        if (r != 0)
450
0
                                return r;
451
452
0
                        r = func(RECURSE_DIR_LEAVE,
453
0
                                 p,
454
0
                                 dir_fd,
455
0
                                 subdir_fd,
456
0
                                 de->entries[i],
457
0
                                 statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
458
0
                                 userdata);
459
0
                } else
460
                        /* Non-directory inode */
461
0
                        r = func(RECURSE_DIR_ENTRY,
462
0
                                 p,
463
0
                                 dir_fd,
464
0
                                 inode_fd,
465
0
                                 de->entries[i],
466
0
                                 statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
467
0
                                 userdata);
468
469
0
                if (r == RECURSE_DIR_LEAVE_DIRECTORY)
470
0
                        break;
471
0
                if (!IN_SET(r, RECURSE_DIR_SKIP_ENTRY, RECURSE_DIR_CONTINUE))
472
0
                        return r;
473
0
        }
474
475
0
        if (FLAGS_SET(flags, RECURSE_DIR_TOPLEVEL)) {
476
477
0
                r = func(RECURSE_DIR_LEAVE,
478
0
                         path,
479
0
                         -1,
480
0
                         dir_fd,
481
0
                         NULL,
482
0
                         statx_mask != 0 ? &root_sx : NULL,
483
0
                         userdata);
484
0
                if (!IN_SET(r, RECURSE_DIR_LEAVE_DIRECTORY, RECURSE_DIR_SKIP_ENTRY, RECURSE_DIR_CONTINUE))
485
0
                        return r;
486
0
        }
487
488
0
        return 0;
489
0
}
490
491
int recurse_dir_at(
492
                int atfd,
493
                const char *path,
494
                unsigned statx_mask,
495
                unsigned n_depth_max,
496
                RecurseDirFlags flags,
497
                recurse_dir_func_t func,
498
0
                void *userdata) {
499
500
0
        _cleanup_close_ int fd = -EBADF;
501
502
0
        assert(atfd >= 0 || atfd == AT_FDCWD);
503
0
        assert(func);
504
505
0
        fd = openat(atfd, path ?: ".", O_DIRECTORY|O_CLOEXEC);
506
0
        if (fd < 0)
507
0
                return -errno;
508
509
0
        return recurse_dir(fd, path, statx_mask, n_depth_max, flags, func, userdata);
510
0
}