]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/basic/recurse-dir.c
machine: do not allow unprivileged users to register other users' processes as machin...
[thirdparty/systemd.git] / src / basic / recurse-dir.c
CommitLineData
b5a07e52
LP
1/* SPDX-License-Identifier: LGPL-2.1-or-later */
2
0c15577a
DDM
3#include <sys/stat.h>
4
b5a07e52
LP
5#include "alloc-util.h"
6#include "dirent-util.h"
7#include "fd-util.h"
4d564427 8#include "fs-util.h"
93a1f792 9#include "log.h"
b5a07e52 10#include "mountpoint-util.h"
0c15577a 11#include "path-util.h"
b5a07e52
LP
12#include "recurse-dir.h"
13#include "sort-util.h"
14
15#define DEFAULT_RECURSION_MAX 100
16
17static int sort_func(struct dirent * const *a, struct dirent * const *b) {
18 return strcmp((*a)->d_name, (*b)->d_name);
19}
20
6393b847
LP
21static bool ignore_dirent(const struct dirent *de, RecurseDirFlags flags) {
22 assert(de);
b5a07e52 23
6393b847 24 /* Depending on flag either ignore everything starting with ".", or just "." itself and ".." */
b5a07e52 25
6393b847
LP
26 return FLAGS_SET(flags, RECURSE_DIR_IGNORE_DOT) ?
27 de->d_name[0] == '.' :
28 dot_or_dot_dot(de->d_name);
b5a07e52
LP
29}
30
90761f7f 31int readdir_all(int dir_fd, RecurseDirFlags flags, DirectoryEntries **ret) {
6393b847
LP
32 _cleanup_free_ DirectoryEntries *de = NULL;
33 DirectoryEntries *nde;
a0a4c578 34 int r;
b5a07e52 35
6393b847 36 assert(dir_fd >= 0);
b5a07e52
LP
37
38 /* Returns an array with pointers to "struct dirent" directory entries, optionally sorted. Free the
ca664db2
LP
39 * array with readdir_all_freep().
40 *
41 * Start with space for up to 8 directory entries. We expect at least 2 ("." + ".."), hence hopefully
6393b847
LP
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);
45 if (!de)
46 return -ENOMEM;
47
48 de->buffer_size = 0;
b5a07e52 49 for (;;) {
6393b847
LP
50 size_t bs;
51 ssize_t n;
b5a07e52 52
6393b847
LP
53 bs = MIN(MALLOC_SIZEOF_SAFE(de) - offsetof(DirectoryEntries, buffer), (size_t) SSIZE_MAX);
54 assert(bs > de->buffer_size);
b5a07e52 55
43aacae8 56 n = getdents64(dir_fd, (struct dirent*) ((uint8_t*) de->buffer + de->buffer_size), bs - de->buffer_size);
6393b847 57 if (n < 0)
b5a07e52 58 return -errno;
6393b847
LP
59 if (n == 0)
60 break;
b5a07e52 61
0dbce03c
LP
62 msan_unpoison((uint8_t*) de->buffer + de->buffer_size, n);
63
6393b847
LP
64 de->buffer_size += n;
65
66 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. */
b5a07e52
LP
68 continue;
69
6393b847
LP
70 if (bs >= SSIZE_MAX - offsetof(DirectoryEntries, buffer))
71 return -EFBIG;
72 bs = bs >= (SSIZE_MAX - offsetof(DirectoryEntries, buffer))/2 ? SSIZE_MAX - offsetof(DirectoryEntries, buffer) : bs * 2;
b5a07e52 73
6393b847
LP
74 nde = realloc(de, bs);
75 if (!nde)
b5a07e52 76 return -ENOMEM;
6393b847
LP
77 de = nde;
78 }
79
80 de->n_entries = 0;
90761f7f 81 struct dirent *entry;
a4e70ef7 82 FOREACH_DIRENT_IN_BUFFER(entry, de->buffer, de->buffer_size) {
6393b847
LP
83 if (ignore_dirent(entry, flags))
84 continue;
b5a07e52 85
a0a4c578
DDM
86 if (FLAGS_SET(flags, RECURSE_DIR_ENSURE_TYPE)) {
87 r = dirent_ensure_type(dir_fd, entry);
88 if (r == -ENOENT)
89 /* dentry gone by now? no problem, let's just suppress it */
90 continue;
91 if (r < 0)
92 return r;
93 }
94
6393b847
LP
95 de->n_entries++;
96 }
97
90761f7f
MY
98 size_t sz, j;
99
6393b847 100 sz = ALIGN(offsetof(DirectoryEntries, buffer) + de->buffer_size);
90761f7f 101 if (!INC_SAFE(&sz, sizeof(struct dirent*) * de->n_entries))
6393b847
LP
102 return -ENOMEM;
103
90761f7f 104 nde = realloc(de, sz);
6393b847
LP
105 if (!nde)
106 return -ENOMEM;
6393b847 107 de = nde;
90761f7f 108
6393b847
LP
109 de->entries = (struct dirent**) ((uint8_t*) de + ALIGN(offsetof(DirectoryEntries, buffer) + de->buffer_size));
110
111 j = 0;
a4e70ef7 112 FOREACH_DIRENT_IN_BUFFER(entry, de->buffer, de->buffer_size) {
6393b847
LP
113 if (ignore_dirent(entry, flags))
114 continue;
115
a0a4c578
DDM
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)
119 continue;
120
6393b847 121 de->entries[j++] = entry;
b5a07e52 122 }
a0a4c578 123 assert(j == de->n_entries);
b5a07e52
LP
124
125 if (FLAGS_SET(flags, RECURSE_DIR_SORT))
6393b847 126 typesafe_qsort(de->entries, de->n_entries, sort_func);
b5a07e52
LP
127
128 if (ret)
6393b847 129 *ret = TAKE_PTR(de);
b5a07e52 130
6393b847 131 return 0;
b5a07e52
LP
132}
133
4d564427
LP
134int readdir_all_at(int fd, const char *path, RecurseDirFlags flags, DirectoryEntries **ret) {
135 _cleanup_close_ int dir_fd = -EBADF;
136
137 assert(fd >= 0 || fd == AT_FDCWD);
138
e40b11be 139 dir_fd = xopenat(fd, path, O_DIRECTORY|O_CLOEXEC);
4d564427
LP
140 if (dir_fd < 0)
141 return dir_fd;
142
143 return readdir_all(dir_fd, flags, ret);
144}
145
b5a07e52 146int recurse_dir(
6393b847 147 int dir_fd,
b5a07e52
LP
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
6393b847 155 _cleanup_free_ DirectoryEntries *de = NULL;
4424e6c8 156 struct statx root_sx;
6393b847 157 int r;
b5a07e52 158
6393b847 159 assert(dir_fd >= 0);
b5a07e52
LP
160 assert(func);
161
6393b847
LP
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. */
b5a07e52
LP
164
165 if (n_depth_max == 0)
166 return -EOVERFLOW;
167 if (n_depth_max == UINT_MAX) /* special marker for "default" */
168 n_depth_max = DEFAULT_RECURSION_MAX;
169
b21ec07b
LP
170 if (FLAGS_SET(flags, RECURSE_DIR_TOPLEVEL)) {
171 if (statx_mask != 0) {
d5ddc0e0
YW
172 if (statx(dir_fd, "", AT_EMPTY_PATH, statx_mask, &root_sx) < 0)
173 return -errno;
b21ec07b
LP
174 }
175
176 r = func(RECURSE_DIR_ENTER,
177 path,
8d4b2689 178 -EBADF, /* we have no parent fd */
b21ec07b
LP
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))
184 return 0;
185 if (r != RECURSE_DIR_CONTINUE)
186 return r;
187 }
188
a0a4c578
DDM
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);
6393b847
LP
191 if (r < 0)
192 return r;
b5a07e52 193
ba010e14
MY
194 FOREACH_ARRAY(entry, de->entries, de->n_entries) {
195 struct dirent *i = *entry;
254d1313 196 _cleanup_close_ int inode_fd = -EBADF, subdir_fd = -EBADF;
b5a07e52 197 _cleanup_free_ char *joined = NULL;
4424e6c8 198 struct statx sx;
b5a07e52
LP
199 bool sx_valid = false;
200 const char *p;
201
202 /* For each directory entry we'll do one of the following:
203 *
6393b847
LP
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)
b5a07e52
LP
207 */
208
209 if (path) {
ba010e14 210 joined = path_join(path, i->d_name);
b5a07e52
LP
211 if (!joined)
212 return -ENOMEM;
213
214 p = joined;
215 } else
ba010e14 216 p = i->d_name;
b5a07e52 217
ba010e14
MY
218 if (IN_SET(i->d_type, DT_UNKNOWN, DT_DIR)) {
219 subdir_fd = openat(dir_fd, i->d_name, O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC);
6393b847 220 if (subdir_fd < 0) {
b5a07e52
LP
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,
6393b847 232 dir_fd,
8d4b2689 233 /* inode_fd = */ -EBADF,
ba010e14 234 i,
8d4b2689 235 /* sx = */ NULL,
b5a07e52
LP
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. */
ba010e14 249 i->d_type = DT_DIR;
b5a07e52
LP
250
251 if (statx_mask != 0 || (flags & RECURSE_DIR_SAME_MOUNT)) {
d5ddc0e0
YW
252 if (statx(subdir_fd, "", AT_EMPTY_PATH, statx_mask, &sx) < 0)
253 return -errno;
b5a07e52
LP
254
255 sx_valid = true;
256 }
257 }
258 }
259
6393b847 260 if (subdir_fd < 0) {
b5a07e52
LP
261 /* It's not a subdirectory. */
262
263 if (flags & RECURSE_DIR_INODE_FD) {
264
ba010e14 265 inode_fd = openat(dir_fd, i->d_name, O_PATH|O_NOFOLLOW|O_CLOEXEC);
b5a07e52
LP
266 if (inode_fd < 0) {
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,
6393b847 276 dir_fd,
8d4b2689 277 /* inode_fd = */ -EBADF,
ba010e14 278 i,
8d4b2689 279 /* sx = */ NULL,
b5a07e52
LP
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
d5ddc0e0
YW
293 if (statx(inode_fd, "", AT_EMPTY_PATH, statx_mask | STATX_TYPE, &sx) < 0)
294 return -errno;
b5a07e52
LP
295
296 assert(sx.stx_mask & STATX_TYPE);
297 sx_valid = true;
298
299 if (S_ISDIR(sx.stx_mode)) {
300 /* What? It's a directory now? Then someone must have quickly
301 * replaced it. Let's handle that gracefully: convert it to a
ba669952 302 * directory fd — which should be riskless now that we pinned the
b5a07e52
LP
303 * inode. */
304
739d9cae 305 subdir_fd = fd_reopen(inode_fd, O_DIRECTORY|O_CLOEXEC);
6393b847 306 if (subdir_fd < 0)
739d9cae 307 return subdir_fd;
b5a07e52
LP
308
309 inode_fd = safe_close(inode_fd);
310 }
311
ba010e14 312 } else if (statx_mask != 0 || (i->d_type == DT_UNKNOWN && (flags & RECURSE_DIR_ENSURE_TYPE))) {
b5a07e52 313
ba010e14 314 if (statx(dir_fd, i->d_name, AT_SYMLINK_NOFOLLOW, statx_mask | STATX_TYPE, &sx) < 0) {
d5ddc0e0
YW
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);
b5a07e52 319
d5ddc0e0 320 assert(errno <= RECURSE_DIR_SKIP_STAT_INODE_ERROR_MAX - RECURSE_DIR_SKIP_STAT_INODE_ERROR_BASE);
b5a07e52 321
d5ddc0e0 322 r = func(RECURSE_DIR_SKIP_STAT_INODE_ERROR_BASE + errno,
b5a07e52 323 p,
6393b847 324 dir_fd,
8d4b2689 325 /* inode_fd = */ -EBADF,
ba010e14 326 i,
8d4b2689 327 /* sx = */ NULL,
b5a07e52
LP
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
91421f83 350 log_debug("Non-directory entry '%s' suddenly became a directory.", p);
b5a07e52
LP
351
352 r = func(RECURSE_DIR_SKIP_STAT_INODE_ERROR_BASE + EISDIR,
353 p,
6393b847 354 dir_fd,
8d4b2689 355 /* inode_fd = */ -EBADF,
ba010e14 356 i,
8d4b2689 357 /* sx = */ NULL,
b5a07e52
LP
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) {
370 /* Copy over the data we acquired through statx() if we acquired any */
371 if (sx.stx_mask & STATX_TYPE) {
6393b847 372 assert((subdir_fd < 0) == !S_ISDIR(sx.stx_mode));
ba010e14 373 i->d_type = IFTODT(sx.stx_mode);
b5a07e52
LP
374 }
375
376 if (sx.stx_mask & STATX_INO)
ba010e14 377 i->d_ino = sx.stx_ino;
b5a07e52
LP
378 }
379
6393b847 380 if (subdir_fd >= 0) {
b5a07e52
LP
381 if (FLAGS_SET(flags, RECURSE_DIR_SAME_MOUNT)) {
382 bool is_mount;
383
384 if (sx_valid && FLAGS_SET(sx.stx_attributes_mask, STATX_ATTR_MOUNT_ROOT))
385 is_mount = FLAGS_SET(sx.stx_attributes, STATX_ATTR_MOUNT_ROOT);
386 else {
ba010e14 387 r = is_mount_point_at(dir_fd, i->d_name, /* flags = */ 0);
b5a07e52
LP
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) {
395 r = func(RECURSE_DIR_SKIP_MOUNT,
396 p,
6393b847
LP
397 dir_fd,
398 subdir_fd,
ba010e14 399 i,
b5a07e52
LP
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) {
412 /* When we reached max depth, generate a special event */
413
414 r = func(RECURSE_DIR_SKIP_DEPTH,
415 p,
6393b847
LP
416 dir_fd,
417 subdir_fd,
ba010e14 418 i,
b5a07e52
LP
419 statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
420 userdata);
421 if (r == RECURSE_DIR_LEAVE_DIRECTORY)
422 break;
423 if (!IN_SET(r, RECURSE_DIR_CONTINUE, RECURSE_DIR_SKIP_ENTRY))
424 return r;
425
426 continue;
427 }
428
429 r = func(RECURSE_DIR_ENTER,
430 p,
6393b847
LP
431 dir_fd,
432 subdir_fd,
ba010e14 433 i,
b5a07e52
LP
434 statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
435 userdata);
436 if (r == RECURSE_DIR_LEAVE_DIRECTORY)
437 break;
438 if (r == RECURSE_DIR_SKIP_ENTRY)
439 continue;
440 if (r != RECURSE_DIR_CONTINUE)
441 return r;
442
6393b847 443 r = recurse_dir(subdir_fd,
b5a07e52
LP
444 p,
445 statx_mask,
446 n_depth_max - 1,
ba010e14 447 flags & ~RECURSE_DIR_TOPLEVEL, /* we already called the callback for this entry */
b5a07e52
LP
448 func,
449 userdata);
450 if (r != 0)
451 return r;
452
453 r = func(RECURSE_DIR_LEAVE,
454 p,
6393b847
LP
455 dir_fd,
456 subdir_fd,
ba010e14 457 i,
b5a07e52
LP
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,
463 p,
6393b847 464 dir_fd,
b5a07e52 465 inode_fd,
ba010e14 466 i,
b5a07e52
LP
467 statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */
468 userdata);
469
b5a07e52
LP
470 if (r == RECURSE_DIR_LEAVE_DIRECTORY)
471 break;
472 if (!IN_SET(r, RECURSE_DIR_SKIP_ENTRY, RECURSE_DIR_CONTINUE))
473 return r;
474 }
475
b21ec07b
LP
476 if (FLAGS_SET(flags, RECURSE_DIR_TOPLEVEL)) {
477
478 r = func(RECURSE_DIR_LEAVE,
479 path,
8d4b2689 480 -EBADF, /* we have no parent fd */
b21ec07b 481 dir_fd,
8d4b2689 482 NULL, /* we have no dirent */
b21ec07b
LP
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))
486 return r;
487 }
488
b5a07e52
LP
489 return 0;
490}
491
492int recurse_dir_at(
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
254d1313 501 _cleanup_close_ int fd = -EBADF;
6393b847
LP
502
503 assert(atfd >= 0 || atfd == AT_FDCWD);
504 assert(func);
505
aa3cc58a 506 fd = openat(atfd, path ?: ".", O_DIRECTORY|O_CLOEXEC);
6393b847 507 if (fd < 0)
b5a07e52
LP
508 return -errno;
509
6393b847 510 return recurse_dir(fd, path, statx_mask, n_depth_max, flags, func, userdata);
b5a07e52 511}