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