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