• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

systemd / systemd / 16280725298

14 Jul 2025 08:16PM UTC coverage: 72.166% (-0.006%) from 72.172%
16280725298

push

github

web-flow
Two fixlets for coverage test (#38183)

302135 of 418667 relevant lines covered (72.17%)

773261.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.63
/src/basic/recurse-dir.c
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
#define DEFAULT_RECURSION_MAX 100
16

17
static int sort_func(struct dirent * const *a, struct dirent * const *b) {
341,872✔
18
        return strcmp((*a)->d_name, (*b)->d_name);
341,872✔
19
}
20

21
static bool ignore_dirent(const struct dirent *de, RecurseDirFlags flags) {
712,392✔
22
        assert(de);
712,392✔
23

24
        /* Depending on flag either ignore everything starting with ".", or just "." itself and ".." */
25

26
        return FLAGS_SET(flags, RECURSE_DIR_IGNORE_DOT) ?
712,392✔
27
                de->d_name[0] == '.' :
228,146✔
28
                dot_or_dot_dot(de->d_name);
712,392✔
29
}
30

31
int readdir_all(int dir_fd, RecurseDirFlags flags, DirectoryEntries **ret) {
15,263✔
32
        _cleanup_free_ DirectoryEntries *de = NULL;
15,263✔
33
        DirectoryEntries *nde;
15,263✔
34
        int r;
15,263✔
35

36
        assert(dir_fd >= 0);
15,263✔
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
        de = malloc(offsetof(DirectoryEntries, buffer) + DIRENT_SIZE_MAX * 8);
15,263✔
45
        if (!de)
15,263✔
46
                return -ENOMEM;
47

48
        de->buffer_size = 0;
15,263✔
49
        for (;;) {
31,726✔
50
                size_t bs;
31,726✔
51
                ssize_t n;
31,726✔
52

53
                bs = MIN(MALLOC_SIZEOF_SAFE(de) - offsetof(DirectoryEntries, buffer), (size_t) SSIZE_MAX);
31,726✔
54
                assert(bs > de->buffer_size);
31,726✔
55

56
                n = getdents64(dir_fd, (struct dirent*) ((uint8_t*) de->buffer + de->buffer_size), bs - de->buffer_size);
31,726✔
57
                if (n < 0)
31,726✔
58
                        return -errno;
×
59
                if (n == 0)
31,726✔
60
                        break;
61

62
                msan_unpoison((uint8_t*) de->buffer + de->buffer_size, n);
16,463✔
63

64
                de->buffer_size += n;
16,463✔
65

66
                if (de->buffer_size < bs - DIRENT_SIZE_MAX) /* Still room for one more entry, then try to
16,463✔
67
                                                             * fill it up without growing the structure. */
68
                        continue;
15,203✔
69

70
                if (bs >= SSIZE_MAX - offsetof(DirectoryEntries, buffer))
1,260✔
71
                        return -EFBIG;
72
                bs = bs >= (SSIZE_MAX - offsetof(DirectoryEntries, buffer))/2 ? SSIZE_MAX - offsetof(DirectoryEntries, buffer) : bs * 2;
1,260✔
73

74
                nde = realloc(de, bs);
1,260✔
75
                if (!nde)
1,260✔
76
                        return -ENOMEM;
77
                de = nde;
1,260✔
78
        }
79

80
        de->n_entries = 0;
15,263✔
81
        struct dirent *entry;
15,263✔
82
        FOREACH_DIRENT_IN_BUFFER(entry, de->buffer, de->buffer_size) {
371,459✔
83
                if (ignore_dirent(entry, flags))
356,196✔
84
                        continue;
30,526✔
85

86
                if (FLAGS_SET(flags, RECURSE_DIR_ENSURE_TYPE)) {
325,670✔
87
                        r = dirent_ensure_type(dir_fd, entry);
256✔
88
                        if (r == -ENOENT)
256✔
89
                                /* dentry gone by now? no problem, let's just suppress it */
90
                                continue;
×
91
                        if (r < 0)
256✔
92
                                return r;
93
                }
94

95
                de->n_entries++;
325,670✔
96
        }
97

98
        size_t sz, j;
15,263✔
99

100
        sz = ALIGN(offsetof(DirectoryEntries, buffer) + de->buffer_size);
15,263✔
101
        if (!INC_SAFE(&sz, sizeof(struct dirent*) * de->n_entries))
15,263✔
102
                return -ENOMEM;
103

104
        nde = realloc(de, sz);
15,263✔
105
        if (!nde)
15,263✔
106
                return -ENOMEM;
107
        de = nde;
15,263✔
108

109
        de->entries = (struct dirent**) ((uint8_t*) de + ALIGN(offsetof(DirectoryEntries, buffer) + de->buffer_size));
15,263✔
110

111
        j = 0;
15,263✔
112
        FOREACH_DIRENT_IN_BUFFER(entry, de->buffer, de->buffer_size) {
371,459✔
113
                if (ignore_dirent(entry, flags))
356,196✔
114
                        continue;
30,526✔
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
                if (FLAGS_SET(flags, RECURSE_DIR_ENSURE_TYPE) && entry->d_type == DT_UNKNOWN)
325,670✔
119
                        continue;
×
120

121
                de->entries[j++] = entry;
325,670✔
122
        }
123
        assert(j == de->n_entries);
15,263✔
124

125
        if (FLAGS_SET(flags, RECURSE_DIR_SORT))
15,263✔
126
                typesafe_qsort(de->entries, de->n_entries, sort_func);
3,323✔
127

128
        if (ret)
15,263✔
129
                *ret = TAKE_PTR(de);
15,263✔
130

131
        return 0;
132
}
133

134
int readdir_all_at(int fd, const char *path, RecurseDirFlags flags, DirectoryEntries **ret) {
41✔
135
        _cleanup_close_ int dir_fd = -EBADF;
41✔
136

137
        assert(fd >= 0 || fd == AT_FDCWD);
41✔
138

139
        dir_fd = xopenat(fd, path, O_DIRECTORY|O_CLOEXEC);
41✔
140
        if (dir_fd < 0)
41✔
141
                return dir_fd;
142

143
        return readdir_all(dir_fd, flags, ret);
40✔
144
}
145

146
int recurse_dir(
14,488✔
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
                void *userdata) {
154

155
        _cleanup_free_ DirectoryEntries *de = NULL;
14,488✔
156
        struct statx root_sx;
14,488✔
157
        int r;
14,488✔
158

159
        assert(dir_fd >= 0);
14,488✔
160
        assert(func);
14,488✔
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
        if (n_depth_max == 0)
14,488✔
166
                return -EOVERFLOW;
167
        if (n_depth_max == UINT_MAX) /* special marker for "default" */
14,488✔
168
                n_depth_max = DEFAULT_RECURSION_MAX;
3,225✔
169

170
        if (FLAGS_SET(flags, RECURSE_DIR_TOPLEVEL)) {
14,488✔
171
                if (statx_mask != 0) {
196✔
172
                        if (statx(dir_fd, "", AT_EMPTY_PATH, statx_mask, &root_sx) < 0)
10✔
173
                                return -errno;
×
174
                }
175

176
                r = func(RECURSE_DIR_ENTER,
196✔
177
                         path,
178
                         -EBADF, /* we have no parent fd */
179
                         dir_fd,
180
                         NULL, /* we have no dirent */
181
                         statx_mask != 0 ? &root_sx : NULL,
182
                         userdata);
183
                if (IN_SET(r, RECURSE_DIR_LEAVE_DIRECTORY, RECURSE_DIR_SKIP_ENTRY))
196✔
184
                        return 0;
185
                if (r != RECURSE_DIR_CONTINUE)
196✔
186
                        return r;
187
        }
188

189
        /* Mask out RECURSE_DIR_ENSURE_TYPE so we can do it ourselves and avoid an extra statx() call. */
190
        r = readdir_all(dir_fd, flags & ~RECURSE_DIR_ENSURE_TYPE, &de);
14,488✔
191
        if (r < 0)
14,488✔
192
                return r;
193

194
        FOREACH_ARRAY(entry, de->entries, de->n_entries) {
314,967✔
195
                struct dirent *i = *entry;
302,685✔
196
                _cleanup_close_ int inode_fd = -EBADF, subdir_fd = -EBADF;
605,370✔
197
                _cleanup_free_ char *joined = NULL;
302,685✔
198
                struct statx sx;
302,685✔
199
                bool sx_valid = false;
302,685✔
200
                const char *p;
302,685✔
201

202
                /* For each directory entry we'll do one of the following:
203
                 *
204
                 * 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)
205
                 * 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)
206
                 * 3) Otherwise, we'll statx() the directory entry via the directory fd we are currently looking at (if requested)
207
                 */
208

209
                if (path) {
302,685✔
210
                        joined = path_join(path, i->d_name);
302,647✔
211
                        if (!joined)
302,647✔
212
                                return -ENOMEM;
213

214
                        p = joined;
215
                } else
216
                        p = i->d_name;
38✔
217

218
                if (IN_SET(i->d_type, DT_UNKNOWN, DT_DIR)) {
302,685✔
219
                        subdir_fd = openat(dir_fd, i->d_name, O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC);
13,226✔
220
                        if (subdir_fd < 0) {
13,226✔
221
                                if (errno == ENOENT) /* Vanished by now, go for next file immediately */
×
222
                                        continue;
×
223

224
                                /* If it is a subdir but we failed to open it, then fail */
225
                                if (!IN_SET(errno, ENOTDIR, ELOOP)) {
×
226
                                        log_debug_errno(errno, "Failed to open directory '%s': %m", p);
×
227

228
                                        assert(errno <= RECURSE_DIR_SKIP_OPEN_DIR_ERROR_MAX - RECURSE_DIR_SKIP_OPEN_DIR_ERROR_BASE);
×
229

230
                                        r = func(RECURSE_DIR_SKIP_OPEN_DIR_ERROR_BASE + errno,
×
231
                                                 p,
232
                                                 dir_fd,
233
                                                 /* inode_fd = */ -EBADF,
234
                                                 i,
235
                                                 /* sx = */ NULL,
236
                                                 userdata);
237
                                        if (r == RECURSE_DIR_LEAVE_DIRECTORY)
×
238
                                                break;
239
                                        if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
×
240
                                                return r;
241

242
                                        continue;
×
243
                                }
244

245
                                /* If it's not a subdir, then let's handle it like a regular inode below */
246

247
                        } else {
248
                                /* If we managed to get a DIR* off the inode, it's definitely a directory. */
249
                                i->d_type = DT_DIR;
13,226✔
250

251
                                if (statx_mask != 0 || (flags & RECURSE_DIR_SAME_MOUNT)) {
13,226✔
252
                                        if (statx(subdir_fd, "", AT_EMPTY_PATH, statx_mask, &sx) < 0)
2,331✔
253
                                                return -errno;
×
254

255
                                        sx_valid = true;
256
                                }
257
                        }
258
                }
259

260
                if (subdir_fd < 0) {
13,226✔
261
                        /* It's not a subdirectory. */
262

263
                        if (flags & RECURSE_DIR_INODE_FD) {
289,459✔
264

265
                                inode_fd = openat(dir_fd, i->d_name, O_PATH|O_NOFOLLOW|O_CLOEXEC);
39,822✔
266
                                if (inode_fd < 0) {
39,822✔
267
                                        if (errno == ENOENT) /* Vanished by now, go for next file immediately */
×
268
                                                continue;
×
269

270
                                        log_debug_errno(errno, "Failed to open directory entry '%s': %m", p);
×
271

272
                                        assert(errno <= RECURSE_DIR_SKIP_OPEN_INODE_ERROR_MAX - RECURSE_DIR_SKIP_OPEN_INODE_ERROR_BASE);
×
273

274
                                        r = func(RECURSE_DIR_SKIP_OPEN_INODE_ERROR_BASE + errno,
×
275
                                                 p,
276
                                                 dir_fd,
277
                                                 /* inode_fd = */ -EBADF,
278
                                                 i,
279
                                                 /* sx = */ NULL,
280
                                                 userdata);
281
                                        if (r == RECURSE_DIR_LEAVE_DIRECTORY)
×
282
                                                break;
283
                                        if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
×
284
                                                return r;
285

286
                                        continue;
×
287
                                }
288

289
                                /* If we open the inode, then verify it's actually a non-directory, like we
290
                                 * assume. Let's guarantee that we never pass statx data of a directory where
291
                                 * caller expects a non-directory */
292

293
                                if (statx(inode_fd, "", AT_EMPTY_PATH, statx_mask | STATX_TYPE, &sx) < 0)
39,822✔
294
                                        return -errno;
×
295

296
                                assert(sx.stx_mask & STATX_TYPE);
39,822✔
297
                                sx_valid = true;
39,822✔
298

299
                                if (S_ISDIR(sx.stx_mode)) {
39,822✔
300
                                        /* What? It's a directory now? Then someone must have quickly
301
                                         * replaced it. Let's handle that gracefully: convert it to a
302
                                         * directory fd — which should be riskless now that we pinned the
303
                                         * inode. */
304

305
                                        subdir_fd = fd_reopen(inode_fd, O_DIRECTORY|O_CLOEXEC);
×
306
                                        if (subdir_fd < 0)
×
307
                                                return subdir_fd;
308

309
                                        inode_fd = safe_close(inode_fd);
×
310
                                }
311

312
                        } else if (statx_mask != 0 || (i->d_type == DT_UNKNOWN && (flags & RECURSE_DIR_ENSURE_TYPE))) {
249,637✔
313

314
                                if (statx(dir_fd, i->d_name, AT_SYMLINK_NOFOLLOW, statx_mask | STATX_TYPE, &sx) < 0) {
×
315
                                        if (errno == ENOENT) /* Vanished by now? Go for next file immediately */
×
316
                                                continue;
×
317

318
                                        log_debug_errno(errno, "Failed to stat directory entry '%s': %m", p);
×
319

320
                                        assert(errno <= RECURSE_DIR_SKIP_STAT_INODE_ERROR_MAX - RECURSE_DIR_SKIP_STAT_INODE_ERROR_BASE);
×
321

322
                                        r = func(RECURSE_DIR_SKIP_STAT_INODE_ERROR_BASE + errno,
×
323
                                                 p,
324
                                                 dir_fd,
325
                                                 /* inode_fd = */ -EBADF,
326
                                                 i,
327
                                                 /* sx = */ NULL,
328
                                                 userdata);
329
                                        if (r == RECURSE_DIR_LEAVE_DIRECTORY)
×
330
                                                break;
331
                                        if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
×
332
                                                return r;
333

334
                                        continue;
×
335
                                }
336

337
                                assert(sx.stx_mask & STATX_TYPE);
×
338
                                sx_valid = true;
×
339

340
                                if (S_ISDIR(sx.stx_mode)) {
×
341
                                        /* So it suddenly is a directory, but we couldn't open it as such
342
                                         * earlier?  That is weird, and probably means somebody is racing
343
                                         * against us. We could of course retry and open it as a directory
344
                                         * again, but the chance to win here is limited. Hence, let's
345
                                         * propagate this as EISDIR error instead. That way we make this
346
                                         * something that can be reasonably handled, even though we give the
347
                                         * guarantee that RECURSE_DIR_ENTRY is strictly issued for
348
                                         * non-directory dirents. */
349

350
                                        log_debug("Non-directory entry '%s' suddenly became a directory.", p);
×
351

352
                                        r = func(RECURSE_DIR_SKIP_STAT_INODE_ERROR_BASE + EISDIR,
×
353
                                                 p,
354
                                                 dir_fd,
355
                                                 /* inode_fd = */ -EBADF,
356
                                                 i,
357
                                                 /* sx = */ NULL,
358
                                                 userdata);
359
                                        if (r == RECURSE_DIR_LEAVE_DIRECTORY)
×
360
                                                break;
361
                                        if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
×
362
                                                return r;
363

364
                                        continue;
×
365
                                }
366
                        }
367
                }
368

369
                if (sx_valid) {
262,863✔
370
                        /* Copy over the data we acquired through statx() if we acquired any */
371
                        if (sx.stx_mask & STATX_TYPE) {
42,153✔
372
                                assert((subdir_fd < 0) == !S_ISDIR(sx.stx_mode));
42,153✔
373
                                i->d_type = IFTODT(sx.stx_mode);
42,153✔
374
                        }
375

376
                        if (sx.stx_mask & STATX_INO)
42,153✔
377
                                i->d_ino = sx.stx_ino;
42,153✔
378
                }
379

380
                if (subdir_fd >= 0) {
302,685✔
381
                        if (FLAGS_SET(flags, RECURSE_DIR_SAME_MOUNT)) {
13,226✔
382
                                bool is_mount;
142✔
383

384
                                if (sx_valid && FLAGS_SET(sx.stx_attributes_mask, STATX_ATTR_MOUNT_ROOT))
142✔
385
                                        is_mount = FLAGS_SET(sx.stx_attributes, STATX_ATTR_MOUNT_ROOT);
142✔
386
                                else {
387
                                        r = is_mount_point_at(dir_fd, i->d_name, /* flags = */ 0);
×
388
                                        if (r < 0)
×
389
                                                log_debug_errno(r, "Failed to determine whether %s is a submount, assuming not: %m", p);
×
390

391
                                        is_mount = r > 0;
×
392
                                }
393

394
                                if (is_mount) {
142✔
395
                                        r = func(RECURSE_DIR_SKIP_MOUNT,
×
396
                                                 p,
397
                                                 dir_fd,
398
                                                 subdir_fd,
399
                                                 i,
400
                                                 statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
401
                                                 userdata);
402
                                        if (r == RECURSE_DIR_LEAVE_DIRECTORY)
×
403
                                                break;
404
                                        if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
×
405
                                                return r;
406

407
                                        continue;
×
408
                                }
409
                        }
410

411
                        if (n_depth_max <= 1) {
13,226✔
412
                                /* When we reached max depth, generate a special event */
413

414
                                r = func(RECURSE_DIR_SKIP_DEPTH,
4,062✔
415
                                         p,
416
                                         dir_fd,
417
                                         subdir_fd,
418
                                         i,
419
                                         statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
420
                                         userdata);
421
                                if (r == RECURSE_DIR_LEAVE_DIRECTORY)
2,031✔
422
                                        break;
423
                                if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
2,031✔
424
                                        return r;
425

426
                                continue;
2,031✔
427
                        }
428

429
                        r = func(RECURSE_DIR_ENTER,
20,201✔
430
                                 p,
431
                                 dir_fd,
432
                                 subdir_fd,
433
                                 i,
434
                                 statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
435
                                 userdata);
436
                        if (r == RECURSE_DIR_LEAVE_DIRECTORY)
11,195✔
437
                                break;
438
                        if (r == RECURSE_DIR_SKIP_ENTRY)
11,195✔
439
                                continue;
×
440
                        if (r != RECURSE_DIR_CONTINUE)
11,195✔
441
                                return r;
442

443
                        r = recurse_dir(subdir_fd,
22,390✔
444
                                        p,
445
                                        statx_mask,
446
                                        n_depth_max - 1,
447
                                        flags & ~RECURSE_DIR_TOPLEVEL, /* we already called the callback for this entry */
11,195✔
448
                                        func,
449
                                        userdata);
450
                        if (r != 0)
11,195✔
451
                                return r;
452

453
                        r = func(RECURSE_DIR_LEAVE,
10,164✔
454
                                 p,
455
                                 dir_fd,
456
                                 subdir_fd,
457
                                 i,
458
                                 statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
459
                                 userdata);
460
                } else
461
                        /* Non-directory inode */
462
                        r = func(RECURSE_DIR_ENTRY,
542,346✔
463
                                 p,
464
                                 dir_fd,
465
                                 inode_fd,
466
                                 i,
467
                                 statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
468
                                 userdata);
469

470
                if (r == RECURSE_DIR_LEAVE_DIRECTORY)
299,623✔
471
                        break;
472
                if (!IN_SET(r, RECURSE_DIR_SKIP_ENTRY, RECURSE_DIR_CONTINUE))
298,995✔
473
                        return r;
474
        }
475

476
        if (FLAGS_SET(flags, RECURSE_DIR_TOPLEVEL)) {
12,910✔
477

478
                r = func(RECURSE_DIR_LEAVE,
382✔
479
                         path,
480
                         -EBADF, /* we have no parent fd */
481
                         dir_fd,
482
                         NULL, /* we have no dirent */
483
                         statx_mask != 0 ? &root_sx : NULL,
484
                         userdata);
485
                if (!IN_SET(r, RECURSE_DIR_LEAVE_DIRECTORY, RECURSE_DIR_SKIP_ENTRY, RECURSE_DIR_CONTINUE))
196✔
486
                        return r;
×
487
        }
488

489
        return 0;
490
}
491

492
int recurse_dir_at(
3,607✔
493
                int atfd,
494
                const char *path,
495
                unsigned statx_mask,
496
                unsigned n_depth_max,
497
                RecurseDirFlags flags,
498
                recurse_dir_func_t func,
499
                void *userdata) {
500

501
        _cleanup_close_ int fd = -EBADF;
3,607✔
502

503
        assert(atfd >= 0 || atfd == AT_FDCWD);
3,607✔
504
        assert(func);
3,607✔
505

506
        fd = openat(atfd, path ?: ".", O_DIRECTORY|O_CLOEXEC);
3,607✔
507
        if (fd < 0)
3,607✔
508
                return -errno;
514✔
509

510
        return recurse_dir(fd, path, statx_mask, n_depth_max, flags, func, userdata);
3,093✔
511
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc