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