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