]>
git.ipfire.org Git - thirdparty/systemd.git/blob - src/shared/rm-rf.c
e99c321418df3f613d79dd2210e3cb1ab19675ed
1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
9 #include "alloc-util.h"
10 #include "btrfs-util.h"
11 #include "cgroup-util.h"
12 #include "dirent-util.h"
17 #include "mountpoint-util.h"
18 #include "path-util.h"
20 #include "stat-util.h"
21 #include "string-util.h"
23 /* We treat tmpfs/ramfs + cgroupfs as non-physical file systems. cgroupfs is similar to tmpfs in a way
24 * after all: we can create arbitrary directory hierarchies in it, and hence can also use rm_rf() on it
25 * to remove those again. */
26 static bool is_physical_fs(const struct statfs
*sfs
) {
27 return !is_temporary_fs(sfs
) && !is_cgroup_fs(sfs
);
30 static int patch_dirfd_mode(
32 bool refuse_already_set
,
33 mode_t
*ret_old_mode
) {
41 if (fstat(dfd
, &st
) < 0)
43 if (!S_ISDIR(st
.st_mode
))
46 if (FLAGS_SET(st
.st_mode
, 0700)) { /* Already set? */
47 if (refuse_already_set
)
48 return -EACCES
; /* original error */
50 *ret_old_mode
= st
.st_mode
;
54 if (st
.st_uid
!= geteuid()) /* this only works if the UID matches ours */
57 r
= fchmod_opath(dfd
, (st
.st_mode
| 0700) & 07777);
61 *ret_old_mode
= st
.st_mode
;
65 int unlinkat_harder(int dfd
, const char *filename
, int unlink_flags
, RemoveFlags remove_flags
) {
69 /* Like unlinkat(), but tries harder: if we get EACCESS we'll try to set the r/w/x bits on the
70 * directory. This is useful if we run unprivileged and have some files where the w bit is
73 if (unlinkat(dfd
, filename
, unlink_flags
) >= 0)
75 if (errno
!= EACCES
|| !FLAGS_SET(remove_flags
, REMOVE_CHMOD
))
78 r
= patch_dirfd_mode(dfd
, /* refuse_already_set = */ true, &old_mode
);
82 if (unlinkat(dfd
, filename
, unlink_flags
) < 0) {
84 /* Try to restore the original access mode if this didn't work */
85 (void) fchmod(dfd
, old_mode
& 07777);
89 if (FLAGS_SET(remove_flags
, REMOVE_CHMOD_RESTORE
) && fchmod(dfd
, old_mode
& 07777) < 0)
92 /* If this worked, we won't reset the old mode by default, since we'll need it for other entries too,
93 * and we should destroy the whole thing */
97 int fstatat_harder(int dfd
,
101 RemoveFlags remove_flags
) {
106 /* Like unlink_harder() but does the same for fstatat() */
108 if (fstatat(dfd
, filename
, ret
, fstatat_flags
) >= 0)
110 if (errno
!= EACCES
|| !FLAGS_SET(remove_flags
, REMOVE_CHMOD
))
113 r
= patch_dirfd_mode(dfd
, /* refuse_already_set = */ true, &old_mode
);
117 if (fstatat(dfd
, filename
, ret
, fstatat_flags
) < 0) {
119 (void) fchmod(dfd
, old_mode
& 07777);
123 if (FLAGS_SET(remove_flags
, REMOVE_CHMOD_RESTORE
) && fchmod(dfd
, old_mode
& 07777) < 0)
129 static int openat_harder(int dfd
, const char *path
, int open_flags
, RemoveFlags remove_flags
, mode_t
*ret_old_mode
) {
130 _cleanup_close_
int pfd
= -EBADF
, fd
= -EBADF
;
131 bool chmod_done
= false;
135 assert(dfd
>= 0 || dfd
== AT_FDCWD
);
138 /* Unlike unlink_harder() and fstatat_harder(), this chmod the specified path. */
140 if (FLAGS_SET(open_flags
, O_PATH
) ||
141 !FLAGS_SET(open_flags
, O_DIRECTORY
) ||
142 !FLAGS_SET(remove_flags
, REMOVE_CHMOD
)) {
144 fd
= RET_NERRNO(openat(dfd
, path
, open_flags
));
151 if (fstat(fd
, &st
) < 0)
154 *ret_old_mode
= st
.st_mode
;
160 pfd
= RET_NERRNO(openat(dfd
, path
, (open_flags
& (O_CLOEXEC
|O_DIRECTORY
|O_NOFOLLOW
)) | O_PATH
));
164 if (FLAGS_SET(remove_flags
, REMOVE_CHMOD
)) {
165 r
= patch_dirfd_mode(pfd
, /* refuse_already_set = */ false, &old_mode
);
172 fd
= fd_reopen(pfd
, open_flags
& ~O_NOFOLLOW
);
175 (void) fchmod_opath(pfd
, old_mode
& 07777);
180 *ret_old_mode
= old_mode
;
185 static int rm_rf_children_impl(
188 const struct stat
*root_dev
,
191 static int rm_rf_inner_child(
196 const struct stat
*root_dev
,
197 bool allow_recursion
) {
207 (is_dir
> 0 && (root_dev
|| (flags
& REMOVE_SUBVOLUME
)))) {
209 r
= fstatat_harder(fd
, fname
, &st
, AT_SYMLINK_NOFOLLOW
, flags
);
213 is_dir
= S_ISDIR(st
.st_mode
);
217 /* If root_dev is set, remove subdirectories only if device is same */
218 if (root_dev
&& st
.st_dev
!= root_dev
->st_dev
)
221 /* Stop at mount points */
222 r
= fd_is_mount_point(fd
, fname
, 0);
228 if ((flags
& REMOVE_SUBVOLUME
) && btrfs_might_be_subvol(&st
)) {
229 /* This could be a subvolume, try to remove it */
231 r
= btrfs_subvol_remove_fd(fd
, fname
, BTRFS_REMOVE_RECURSIVE
|BTRFS_REMOVE_QUOTA
);
233 if (!IN_SET(r
, -ENOTTY
, -EINVAL
))
236 /* ENOTTY, then it wasn't a btrfs subvolume, continue below. */
238 /* It was a subvolume, done. */
242 if (!allow_recursion
)
246 int subdir_fd
= openat_harder(fd
, fname
,
247 O_RDONLY
|O_NONBLOCK
|O_DIRECTORY
|O_CLOEXEC
|O_NOFOLLOW
|O_NOATIME
,
252 /* We pass REMOVE_PHYSICAL here, to avoid doing the fstatfs() to check the file system type
253 * again for each directory */
254 q
= rm_rf_children_impl(subdir_fd
, flags
| REMOVE_PHYSICAL
, root_dev
, old_mode
);
256 } else if (flags
& REMOVE_ONLY_DIRECTORIES
)
259 r
= unlinkat_harder(fd
, fname
, is_dir
? AT_REMOVEDIR
: 0, flags
);
267 typedef struct TodoEntry
{
268 DIR *dir
; /* A directory that we were operating on. */
269 char *dirname
; /* The filename of that directory itself. */
270 mode_t old_mode
; /* The original file mode. */
273 static void free_todo_entries(TodoEntry
**todos
) {
274 for (TodoEntry
*x
= *todos
; x
&& x
->dir
; x
++) {
285 const struct stat
*root_dev
) {
291 if (fstat(fd
, &st
) < 0)
294 return rm_rf_children_impl(fd
, flags
, root_dev
, st
.st_mode
);
297 static int rm_rf_children_impl(
300 const struct stat
*root_dev
,
303 _cleanup_(free_todo_entries
) TodoEntry
*todos
= NULL
;
305 _cleanup_free_
char *dirname
= NULL
; /* Set when we are recursing and want to delete ourselves */
308 /* Return the first error we run into, but nevertheless try to go on.
309 * The passed fd is closed in all cases, including on failure. */
311 for (;;) { /* This loop corresponds to the directory nesting level. */
312 _cleanup_closedir_
DIR *d
= NULL
;
315 /* We know that we are in recursion here, because n_todo is set.
316 * We need to remove the inner directory we were operating on. */
318 r
= unlinkat_harder(dirfd(todos
[n_todo
-1].dir
), dirname
, AT_REMOVEDIR
, flags
);
319 if (r
< 0 && r
!= -ENOENT
) {
323 if (FLAGS_SET(flags
, REMOVE_CHMOD_RESTORE
))
324 (void) fchmodat(dirfd(todos
[n_todo
-1].dir
), dirname
, old_mode
& 07777, 0);
326 dirname
= mfree(dirname
);
328 /* And now let's back out one level up */
330 d
= TAKE_PTR(todos
[n_todo
].dir
);
331 dirname
= TAKE_PTR(todos
[n_todo
].dirname
);
332 old_mode
= todos
[n_todo
].old_mode
;
335 fd
= dirfd(d
); /* Retrieve the file descriptor from the DIR object */
345 fd
= dirfd(d
); /* We donated the fd to fdopendir(). Let's make sure we sure we have
346 * the right descriptor even if it were to internally invalidate the
349 if (!(flags
& REMOVE_PHYSICAL
)) {
352 if (fstatfs(fd
, &sfs
) < 0)
355 if (is_physical_fs(&sfs
)) {
356 /* We refuse to clean physical file systems with this call, unless
357 * explicitly requested. This is extra paranoia just to be sure we
358 * never ever remove non-state data. */
360 _cleanup_free_
char *path
= NULL
;
362 (void) fd_get_path(fd
, &path
);
363 return log_error_errno(SYNTHETIC_ERRNO(EPERM
),
364 "Attempted to remove disk file system under \"%s\", and we can't allow that.",
370 FOREACH_DIRENT_ALL(de
, d
, return -errno
) {
373 if (dot_or_dot_dot(de
->d_name
))
376 is_dir
= de
->d_type
== DT_UNKNOWN
? -1 : de
->d_type
== DT_DIR
;
378 r
= rm_rf_inner_child(fd
, de
->d_name
, is_dir
, flags
, root_dev
, false);
380 /* Push the current working state onto the todo list */
382 if (!GREEDY_REALLOC0(todos
, n_todo
+ 2))
385 _cleanup_free_
char *newdirname
= strdup(de
->d_name
);
390 int newfd
= openat_harder(fd
, de
->d_name
,
391 O_RDONLY
|O_NONBLOCK
|O_DIRECTORY
|O_CLOEXEC
|O_NOFOLLOW
|O_NOATIME
,
394 todos
[n_todo
++] = (TodoEntry
) {
396 .dirname
= TAKE_PTR(dirname
),
401 dirname
= TAKE_PTR(newdirname
);
406 } else if (newfd
!= -ENOENT
&& ret
== 0)
409 } else if (r
< 0 && r
!= -ENOENT
&& ret
== 0)
413 if (FLAGS_SET(flags
, REMOVE_SYNCFS
) && syncfs(fd
) < 0 && ret
>= 0)
417 if (FLAGS_SET(flags
, REMOVE_CHMOD_RESTORE
) &&
418 fchmod(fd
, old_mode
& 07777) < 0 && ret
>= 0)
428 int rm_rf(const char *path
, RemoveFlags flags
) {
434 /* For now, don't support dropping subvols when also only dropping directories, since we can't do
435 * this race-freely. */
436 if (FLAGS_SET(flags
, REMOVE_ONLY_DIRECTORIES
|REMOVE_SUBVOLUME
))
439 /* We refuse to clean the root file system with this call. This is extra paranoia to never cause a
440 * really seriously broken system. */
441 if (path_equal_or_files_same(path
, "/", AT_SYMLINK_NOFOLLOW
))
442 return log_error_errno(SYNTHETIC_ERRNO(EPERM
),
443 "Attempted to remove entire root file system (\"%s\"), and we can't allow that.",
446 if (FLAGS_SET(flags
, REMOVE_SUBVOLUME
| REMOVE_ROOT
| REMOVE_PHYSICAL
)) {
447 /* Try to remove as subvolume first */
448 r
= btrfs_subvol_remove(path
, BTRFS_REMOVE_RECURSIVE
|BTRFS_REMOVE_QUOTA
);
452 if (FLAGS_SET(flags
, REMOVE_MISSING_OK
) && r
== -ENOENT
)
455 if (!IN_SET(r
, -ENOTTY
, -EINVAL
, -ENOTDIR
))
458 /* Not btrfs or not a subvolume */
461 fd
= openat_harder(AT_FDCWD
, path
, O_RDONLY
|O_NONBLOCK
|O_DIRECTORY
|O_CLOEXEC
|O_NOFOLLOW
|O_NOATIME
, flags
, &old_mode
);
464 r
= rm_rf_children_impl(fd
, flags
, NULL
, old_mode
);
466 if (FLAGS_SET(flags
, REMOVE_ROOT
))
467 q
= RET_NERRNO(rmdir(path
));
470 if (FLAGS_SET(flags
, REMOVE_MISSING_OK
) && r
== -ENOENT
)
473 if (!IN_SET(r
, -ENOTDIR
, -ELOOP
))
476 if (FLAGS_SET(flags
, REMOVE_ONLY_DIRECTORIES
) || !FLAGS_SET(flags
, REMOVE_ROOT
))
479 if (!FLAGS_SET(flags
, REMOVE_PHYSICAL
)) {
482 if (statfs(path
, &s
) < 0)
484 if (is_physical_fs(&s
))
485 return log_error_errno(SYNTHETIC_ERRNO(EPERM
),
486 "Attempted to remove files from a disk file system under \"%s\", refusing.",
491 q
= RET_NERRNO(unlink(path
));
496 if (q
< 0 && (q
!= -ENOENT
|| !FLAGS_SET(flags
, REMOVE_MISSING_OK
)))
501 int rm_rf_child(int fd
, const char *name
, RemoveFlags flags
) {
503 /* Removes one specific child of the specified directory */
508 if (!filename_is_valid(name
))
511 if ((flags
& (REMOVE_ROOT
|REMOVE_MISSING_OK
)) != 0) /* Doesn't really make sense here, we are not supposed to remove 'fd' anyway */
514 if (FLAGS_SET(flags
, REMOVE_ONLY_DIRECTORIES
|REMOVE_SUBVOLUME
))
517 return rm_rf_inner_child(fd
, name
, -1, flags
, NULL
, true);