]>
Commit | Line | Data |
---|---|---|
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 | ||
14 | static int sort_func(struct dirent * const *a, struct dirent * const *b) { | |
15 | return strcmp((*a)->d_name, (*b)->d_name); | |
16 | } | |
17 | ||
6393b847 LP |
18 | static 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 | ||
31 | int 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 | ||
140 | int 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 | ||
454 | int 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 | } |