1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
2 
3 #include <errno.h>
4 #include <fcntl.h>
5 #include <stdbool.h>
6 #include <stddef.h>
7 #include <unistd.h>
8 
9 #include "alloc-util.h"
10 #include "btrfs-util.h"
11 #include "cgroup-util.h"
12 #include "dirent-util.h"
13 #include "fd-util.h"
14 #include "log.h"
15 #include "macro.h"
16 #include "mountpoint-util.h"
17 #include "path-util.h"
18 #include "rm-rf.h"
19 #include "stat-util.h"
20 #include "string-util.h"
21 
22 /* We treat tmpfs/ramfs + cgroupfs as non-physical file systems. cgroupfs is similar to tmpfs in a way
23  * after all: we can create arbitrary directory hierarchies in it, and hence can also use rm_rf() on it
24  * to remove those again. */
is_physical_fs(const struct statfs * sfs)25 static bool is_physical_fs(const struct statfs *sfs) {
26         return !is_temporary_fs(sfs) && !is_cgroup_fs(sfs);
27 }
28 
patch_dirfd_mode(int dfd,mode_t * ret_old_mode)29 static int patch_dirfd_mode(
30                 int dfd,
31                 mode_t *ret_old_mode) {
32 
33         struct stat st;
34 
35         assert(dfd >= 0);
36         assert(ret_old_mode);
37 
38         if (fstat(dfd, &st) < 0)
39                 return -errno;
40         if (!S_ISDIR(st.st_mode))
41                 return -ENOTDIR;
42         if (FLAGS_SET(st.st_mode, 0700)) /* Already set? */
43                 return -EACCES; /* original error */
44         if (st.st_uid != geteuid())  /* this only works if the UID matches ours */
45                 return -EACCES;
46 
47         if (fchmod(dfd, (st.st_mode | 0700) & 07777) < 0)
48                 return -errno;
49 
50         *ret_old_mode = st.st_mode;
51         return 0;
52 }
53 
unlinkat_harder(int dfd,const char * filename,int unlink_flags,RemoveFlags remove_flags)54 int unlinkat_harder(int dfd, const char *filename, int unlink_flags, RemoveFlags remove_flags) {
55         mode_t old_mode;
56         int r;
57 
58         /* Like unlinkat(), but tries harder: if we get EACCESS we'll try to set the r/w/x bits on the
59          * directory. This is useful if we run unprivileged and have some files where the w bit is
60          * missing. */
61 
62         if (unlinkat(dfd, filename, unlink_flags) >= 0)
63                 return 0;
64         if (errno != EACCES || !FLAGS_SET(remove_flags, REMOVE_CHMOD))
65                 return -errno;
66 
67         r = patch_dirfd_mode(dfd, &old_mode);
68         if (r < 0)
69                 return r;
70 
71         if (unlinkat(dfd, filename, unlink_flags) < 0) {
72                 r = -errno;
73                 /* Try to restore the original access mode if this didn't work */
74                 (void) fchmod(dfd, old_mode);
75                 return r;
76         }
77 
78         if (FLAGS_SET(remove_flags, REMOVE_CHMOD_RESTORE) && fchmod(dfd, old_mode) < 0)
79                 return -errno;
80 
81         /* If this worked, we won't reset the old mode by default, since we'll need it for other entries too,
82          * and we should destroy the whole thing */
83         return 0;
84 }
85 
fstatat_harder(int dfd,const char * filename,struct stat * ret,int fstatat_flags,RemoveFlags remove_flags)86 int fstatat_harder(int dfd,
87                 const char *filename,
88                 struct stat *ret,
89                 int fstatat_flags,
90                 RemoveFlags remove_flags) {
91 
92         mode_t old_mode;
93         int r;
94 
95         /* Like unlink_harder() but does the same for fstatat() */
96 
97         if (fstatat(dfd, filename, ret, fstatat_flags) >= 0)
98                 return 0;
99         if (errno != EACCES || !FLAGS_SET(remove_flags, REMOVE_CHMOD))
100                 return -errno;
101 
102         r = patch_dirfd_mode(dfd, &old_mode);
103         if (r < 0)
104                 return r;
105 
106         if (fstatat(dfd, filename, ret, fstatat_flags) < 0) {
107                 r = -errno;
108                 (void) fchmod(dfd, old_mode);
109                 return r;
110         }
111 
112         if (FLAGS_SET(remove_flags, REMOVE_CHMOD_RESTORE) && fchmod(dfd, old_mode) < 0)
113                 return -errno;
114 
115         return 0;
116 }
117 
rm_rf_inner_child(int fd,const char * fname,int is_dir,RemoveFlags flags,const struct stat * root_dev,bool allow_recursion)118 static int rm_rf_inner_child(
119                 int fd,
120                 const char *fname,
121                 int is_dir,
122                 RemoveFlags flags,
123                 const struct stat *root_dev,
124                 bool allow_recursion) {
125 
126         struct stat st;
127         int r, q = 0;
128 
129         assert(fd >= 0);
130         assert(fname);
131 
132         if (is_dir < 0 ||
133             root_dev ||
134             (is_dir > 0 && (root_dev || (flags & REMOVE_SUBVOLUME)))) {
135 
136                 r = fstatat_harder(fd, fname, &st, AT_SYMLINK_NOFOLLOW, flags);
137                 if (r < 0)
138                         return r;
139 
140                 is_dir = S_ISDIR(st.st_mode);
141         }
142 
143         if (is_dir) {
144                 /* If root_dev is set, remove subdirectories only if device is same */
145                 if (root_dev && st.st_dev != root_dev->st_dev)
146                         return 0;
147 
148                 /* Stop at mount points */
149                 r = fd_is_mount_point(fd, fname, 0);
150                 if (r < 0)
151                         return r;
152                 if (r > 0)
153                         return 0;
154 
155                 if ((flags & REMOVE_SUBVOLUME) && btrfs_might_be_subvol(&st)) {
156                         /* This could be a subvolume, try to remove it */
157 
158                         r = btrfs_subvol_remove_fd(fd, fname, BTRFS_REMOVE_RECURSIVE|BTRFS_REMOVE_QUOTA);
159                         if (r < 0) {
160                                 if (!IN_SET(r, -ENOTTY, -EINVAL))
161                                         return r;
162 
163                                 /* ENOTTY, then it wasn't a btrfs subvolume, continue below. */
164                         } else
165                                 /* It was a subvolume, done. */
166                                 return 1;
167                 }
168 
169                 if (!allow_recursion)
170                         return -EISDIR;
171 
172                 int subdir_fd = openat(fd, fname, O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW|O_NOATIME);
173                 if (subdir_fd < 0)
174                         return -errno;
175 
176                 /* We pass REMOVE_PHYSICAL here, to avoid doing the fstatfs() to check the file system type
177                  * again for each directory */
178                 q = rm_rf_children(subdir_fd, flags | REMOVE_PHYSICAL, root_dev);
179 
180         } else if (flags & REMOVE_ONLY_DIRECTORIES)
181                 return 0;
182 
183         r = unlinkat_harder(fd, fname, is_dir ? AT_REMOVEDIR : 0, flags);
184         if (r < 0)
185                 return r;
186         if (q < 0)
187                 return q;
188         return 1;
189 }
190 
191 typedef struct TodoEntry {
192         DIR *dir;         /* A directory that we were operating on. */
193         char *dirname;    /* The filename of that directory itself. */
194 } TodoEntry;
195 
free_todo_entries(TodoEntry ** todos)196 static void free_todo_entries(TodoEntry **todos) {
197         for (TodoEntry *x = *todos; x && x->dir; x++) {
198                 closedir(x->dir);
199                 free(x->dirname);
200         }
201 
202         freep(todos);
203 }
204 
rm_rf_children(int fd,RemoveFlags flags,const struct stat * root_dev)205 int rm_rf_children(
206                 int fd,
207                 RemoveFlags flags,
208                 const struct stat *root_dev) {
209 
210         _cleanup_(free_todo_entries) TodoEntry *todos = NULL;
211         size_t n_todo = 0;
212         _cleanup_free_ char *dirname = NULL; /* Set when we are recursing and want to delete ourselves */
213         int ret = 0, r;
214 
215         /* Return the first error we run into, but nevertheless try to go on.
216          * The passed fd is closed in all cases, including on failure. */
217 
218         for (;;) {  /* This loop corresponds to the directory nesting level. */
219                 _cleanup_closedir_ DIR *d = NULL;
220 
221                 if (n_todo > 0) {
222                         /* We know that we are in recursion here, because n_todo is set.
223                          * We need to remove the inner directory we were operating on. */
224                         assert(dirname);
225                         r = unlinkat_harder(dirfd(todos[n_todo-1].dir), dirname, AT_REMOVEDIR, flags);
226                         if (r < 0 && r != -ENOENT && ret == 0)
227                                 ret = r;
228                         dirname = mfree(dirname);
229 
230                         /* And now let's back out one level up */
231                         n_todo --;
232                         d = TAKE_PTR(todos[n_todo].dir);
233                         dirname = TAKE_PTR(todos[n_todo].dirname);
234 
235                         assert(d);
236                         fd = dirfd(d); /* Retrieve the file descriptor from the DIR object */
237                         assert(fd >= 0);
238                 } else {
239         next_fd:
240                         assert(fd >= 0);
241                         d = fdopendir(fd);
242                         if (!d) {
243                                 safe_close(fd);
244                                 return -errno;
245                         }
246                         fd = dirfd(d); /* We donated the fd to fdopendir(). Let's make sure we sure we have
247                                         * the right descriptor even if it were to internally invalidate the
248                                         * one we passed. */
249 
250                         if (!(flags & REMOVE_PHYSICAL)) {
251                                 struct statfs sfs;
252 
253                                 if (fstatfs(fd, &sfs) < 0)
254                                         return -errno;
255 
256                                 if (is_physical_fs(&sfs)) {
257                                         /* We refuse to clean physical file systems with this call, unless
258                                          * explicitly requested. This is extra paranoia just to be sure we
259                                          * never ever remove non-state data. */
260 
261                                         _cleanup_free_ char *path = NULL;
262 
263                                         (void) fd_get_path(fd, &path);
264                                         return log_error_errno(SYNTHETIC_ERRNO(EPERM),
265                                                                "Attempted to remove disk file system under \"%s\", and we can't allow that.",
266                                                                strna(path));
267                                 }
268                         }
269                 }
270 
271                 FOREACH_DIRENT_ALL(de, d, return -errno) {
272                         int is_dir;
273 
274                         if (dot_or_dot_dot(de->d_name))
275                                 continue;
276 
277                         is_dir = de->d_type == DT_UNKNOWN ? -1 : de->d_type == DT_DIR;
278 
279                         r = rm_rf_inner_child(fd, de->d_name, is_dir, flags, root_dev, false);
280                         if (r == -EISDIR) {
281                                 /* Push the current working state onto the todo list */
282 
283                                  if (!GREEDY_REALLOC0(todos, n_todo + 2))
284                                          return log_oom();
285 
286                                  _cleanup_free_ char *newdirname = strdup(de->d_name);
287                                  if (!newdirname)
288                                          return log_oom();
289 
290                                  int newfd = openat(fd, de->d_name,
291                                                     O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW|O_NOATIME);
292                                  if (newfd >= 0) {
293                                          todos[n_todo++] = (TodoEntry) { TAKE_PTR(d), TAKE_PTR(dirname) };
294                                          fd = newfd;
295                                          dirname = TAKE_PTR(newdirname);
296 
297                                          goto next_fd;
298 
299                                  } else if (errno != -ENOENT && ret == 0)
300                                          ret = -errno;
301 
302                         } else if (r < 0 && r != -ENOENT && ret == 0)
303                                 ret = r;
304                 }
305 
306                 if (FLAGS_SET(flags, REMOVE_SYNCFS) && syncfs(fd) < 0 && ret >= 0)
307                         ret = -errno;
308 
309                 if (n_todo == 0)
310                         break;
311         }
312 
313         return ret;
314 }
315 
rm_rf(const char * path,RemoveFlags flags)316 int rm_rf(const char *path, RemoveFlags flags) {
317         int fd, r, q = 0;
318 
319         assert(path);
320 
321         /* For now, don't support dropping subvols when also only dropping directories, since we can't do
322          * this race-freely. */
323         if (FLAGS_SET(flags, REMOVE_ONLY_DIRECTORIES|REMOVE_SUBVOLUME))
324                 return -EINVAL;
325 
326         /* We refuse to clean the root file system with this call. This is extra paranoia to never cause a
327          * really seriously broken system. */
328         if (path_equal_or_files_same(path, "/", AT_SYMLINK_NOFOLLOW))
329                 return log_error_errno(SYNTHETIC_ERRNO(EPERM),
330                                        "Attempted to remove entire root file system (\"%s\"), and we can't allow that.",
331                                        path);
332 
333         if (FLAGS_SET(flags, REMOVE_SUBVOLUME | REMOVE_ROOT | REMOVE_PHYSICAL)) {
334                 /* Try to remove as subvolume first */
335                 r = btrfs_subvol_remove(path, BTRFS_REMOVE_RECURSIVE|BTRFS_REMOVE_QUOTA);
336                 if (r >= 0)
337                         return r;
338 
339                 if (FLAGS_SET(flags, REMOVE_MISSING_OK) && r == -ENOENT)
340                         return 0;
341 
342                 if (!IN_SET(r, -ENOTTY, -EINVAL, -ENOTDIR))
343                         return r;
344 
345                 /* Not btrfs or not a subvolume */
346         }
347 
348         fd = open(path, O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW|O_NOATIME);
349         if (fd >= 0) {
350                 /* We have a dir */
351                 r = rm_rf_children(fd, flags, NULL);
352 
353                 if (FLAGS_SET(flags, REMOVE_ROOT))
354                         q = RET_NERRNO(rmdir(path));
355         } else {
356                 if (FLAGS_SET(flags, REMOVE_MISSING_OK) && errno == ENOENT)
357                         return 0;
358 
359                 if (!IN_SET(errno, ENOTDIR, ELOOP))
360                         return -errno;
361 
362                 if (FLAGS_SET(flags, REMOVE_ONLY_DIRECTORIES) || !FLAGS_SET(flags, REMOVE_ROOT))
363                         return 0;
364 
365                 if (!FLAGS_SET(flags, REMOVE_PHYSICAL)) {
366                         struct statfs s;
367 
368                         if (statfs(path, &s) < 0)
369                                 return -errno;
370                         if (is_physical_fs(&s))
371                                 return log_error_errno(SYNTHETIC_ERRNO(EPERM),
372                                                        "Attempted to remove files from a disk file system under \"%s\", refusing.",
373                                                        path);
374                 }
375 
376                 r = 0;
377                 q = RET_NERRNO(unlink(path));
378         }
379 
380         if (r < 0)
381                 return r;
382         if (q < 0 && (q != -ENOENT || !FLAGS_SET(flags, REMOVE_MISSING_OK)))
383                 return q;
384         return 0;
385 }
386 
rm_rf_child(int fd,const char * name,RemoveFlags flags)387 int rm_rf_child(int fd, const char *name, RemoveFlags flags) {
388 
389         /* Removes one specific child of the specified directory */
390 
391         if (fd < 0)
392                 return -EBADF;
393 
394         if (!filename_is_valid(name))
395                 return -EINVAL;
396 
397         if ((flags & (REMOVE_ROOT|REMOVE_MISSING_OK)) != 0) /* Doesn't really make sense here, we are not supposed to remove 'fd' anyway */
398                 return -EINVAL;
399 
400         if (FLAGS_SET(flags, REMOVE_ONLY_DIRECTORIES|REMOVE_SUBVOLUME))
401                 return -EINVAL;
402 
403         return rm_rf_inner_child(fd, name, -1, flags, NULL, true);
404 }
405