]>
Commit | Line | Data |
---|---|---|
a032b68d | 1 | /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
e3bff60a | 2 | |
4c89c718 MP |
3 | #include <errno.h> |
4 | #include <fcntl.h> | |
5 | #include <stdbool.h> | |
6 | #include <stddef.h> | |
4c89c718 MP |
7 | #include <unistd.h> |
8 | ||
b012e921 | 9 | #include "alloc-util.h" |
e3bff60a | 10 | #include "btrfs-util.h" |
8a584da2 | 11 | #include "cgroup-util.h" |
2897b343 | 12 | #include "dirent-util.h" |
db2df898 | 13 | #include "fd-util.h" |
4c89c718 MP |
14 | #include "log.h" |
15 | #include "macro.h" | |
6e866b33 | 16 | #include "mountpoint-util.h" |
db2df898 | 17 | #include "path-util.h" |
e3bff60a | 18 | #include "rm-rf.h" |
db2df898 MP |
19 | #include "stat-util.h" |
20 | #include "string-util.h" | |
e3bff60a | 21 | |
8a584da2 MP |
22 | static bool is_physical_fs(const struct statfs *sfs) { |
23 | return !is_temporary_fs(sfs) && !is_cgroup_fs(sfs); | |
24 | } | |
25 | ||
3a6ce677 BR |
26 | static int patch_dirfd_mode( |
27 | int dfd, | |
28 | mode_t *ret_old_mode) { | |
29 | ||
30 | struct stat st; | |
31 | ||
32 | assert(dfd >= 0); | |
33 | assert(ret_old_mode); | |
34 | ||
35 | if (fstat(dfd, &st) < 0) | |
36 | return -errno; | |
37 | if (!S_ISDIR(st.st_mode)) | |
38 | return -ENOTDIR; | |
39 | if (FLAGS_SET(st.st_mode, 0700)) /* Already set? */ | |
40 | return -EACCES; /* original error */ | |
41 | if (st.st_uid != geteuid()) /* this only works if the UID matches ours */ | |
42 | return -EACCES; | |
43 | ||
44 | if (fchmod(dfd, (st.st_mode | 0700) & 07777) < 0) | |
45 | return -errno; | |
46 | ||
47 | *ret_old_mode = st.st_mode; | |
48 | return 0; | |
49 | } | |
50 | ||
8b3d4ff0 | 51 | int unlinkat_harder(int dfd, const char *filename, int unlink_flags, RemoveFlags remove_flags) { |
a032b68d | 52 | |
3a6ce677 | 53 | mode_t old_mode; |
a032b68d MB |
54 | int r; |
55 | ||
56 | /* Like unlinkat(), but tries harder: if we get EACCESS we'll try to set the r/w/x bits on the | |
57 | * directory. This is useful if we run unprivileged and have some files where the w bit is | |
58 | * missing. */ | |
59 | ||
60 | if (unlinkat(dfd, filename, unlink_flags) >= 0) | |
61 | return 0; | |
62 | if (errno != EACCES || !FLAGS_SET(remove_flags, REMOVE_CHMOD)) | |
63 | return -errno; | |
64 | ||
3a6ce677 BR |
65 | r = patch_dirfd_mode(dfd, &old_mode); |
66 | if (r < 0) | |
67 | return r; | |
a032b68d MB |
68 | |
69 | if (unlinkat(dfd, filename, unlink_flags) < 0) { | |
70 | r = -errno; | |
71 | /* Try to restore the original access mode if this didn't work */ | |
3a6ce677 BR |
72 | (void) fchmod(dfd, old_mode); |
73 | return r; | |
74 | } | |
75 | ||
8b3d4ff0 MB |
76 | if (FLAGS_SET(remove_flags, REMOVE_CHMOD_RESTORE) && fchmod(dfd, old_mode) < 0) |
77 | return -errno; | |
78 | ||
79 | /* If this worked, we won't reset the old mode by default, since we'll need it for other entries too, | |
80 | * and we should destroy the whole thing */ | |
3a6ce677 BR |
81 | return 0; |
82 | } | |
83 | ||
8b3d4ff0 | 84 | int fstatat_harder(int dfd, |
3a6ce677 BR |
85 | const char *filename, |
86 | struct stat *ret, | |
87 | int fstatat_flags, | |
88 | RemoveFlags remove_flags) { | |
89 | ||
90 | mode_t old_mode; | |
91 | int r; | |
92 | ||
93 | /* Like unlink_harder() but does the same for fstatat() */ | |
94 | ||
95 | if (fstatat(dfd, filename, ret, fstatat_flags) >= 0) | |
96 | return 0; | |
97 | if (errno != EACCES || !FLAGS_SET(remove_flags, REMOVE_CHMOD)) | |
98 | return -errno; | |
99 | ||
100 | r = patch_dirfd_mode(dfd, &old_mode); | |
101 | if (r < 0) | |
102 | return r; | |
103 | ||
104 | if (fstatat(dfd, filename, ret, fstatat_flags) < 0) { | |
105 | r = -errno; | |
106 | (void) fchmod(dfd, old_mode); | |
a032b68d MB |
107 | return r; |
108 | } | |
109 | ||
8b3d4ff0 MB |
110 | if (FLAGS_SET(remove_flags, REMOVE_CHMOD_RESTORE) && fchmod(dfd, old_mode) < 0) |
111 | return -errno; | |
112 | ||
a032b68d MB |
113 | return 0; |
114 | } | |
115 | ||
e3bff60a MP |
116 | int rm_rf_children(int fd, RemoveFlags flags, struct stat *root_dev) { |
117 | _cleanup_closedir_ DIR *d = NULL; | |
2897b343 | 118 | struct dirent *de; |
e3bff60a | 119 | int ret = 0, r; |
8a584da2 | 120 | struct statfs sfs; |
e3bff60a MP |
121 | |
122 | assert(fd >= 0); | |
123 | ||
f2dec872 BR |
124 | /* This returns the first error we run into, but nevertheless tries to go on. This closes the passed |
125 | * fd, in all cases, including on failure.. */ | |
e3bff60a MP |
126 | |
127 | if (!(flags & REMOVE_PHYSICAL)) { | |
128 | ||
8a584da2 | 129 | r = fstatfs(fd, &sfs); |
e3bff60a MP |
130 | if (r < 0) { |
131 | safe_close(fd); | |
8a584da2 | 132 | return -errno; |
e3bff60a MP |
133 | } |
134 | ||
8a584da2 | 135 | if (is_physical_fs(&sfs)) { |
b012e921 MB |
136 | /* We refuse to clean physical file systems with this call, |
137 | * unless explicitly requested. This is extra paranoia just | |
138 | * to be sure we never ever remove non-state data. */ | |
139 | _cleanup_free_ char *path = NULL; | |
140 | ||
141 | (void) fd_get_path(fd, &path); | |
142 | log_error("Attempted to remove disk file system under \"%s\", and we can't allow that.", | |
143 | strna(path)); | |
e3bff60a | 144 | |
e3bff60a MP |
145 | safe_close(fd); |
146 | return -EPERM; | |
147 | } | |
148 | } | |
149 | ||
150 | d = fdopendir(fd); | |
151 | if (!d) { | |
152 | safe_close(fd); | |
153 | return errno == ENOENT ? 0 : -errno; | |
154 | } | |
155 | ||
2897b343 | 156 | FOREACH_DIRENT_ALL(de, d, return -errno) { |
e3bff60a MP |
157 | bool is_dir; |
158 | struct stat st; | |
159 | ||
2897b343 | 160 | if (dot_or_dot_dot(de->d_name)) |
e3bff60a MP |
161 | continue; |
162 | ||
163 | if (de->d_type == DT_UNKNOWN || | |
164 | (de->d_type == DT_DIR && (root_dev || (flags & REMOVE_SUBVOLUME)))) { | |
3a6ce677 BR |
165 | r = fstatat_harder(fd, de->d_name, &st, AT_SYMLINK_NOFOLLOW, flags); |
166 | if (r < 0) { | |
167 | if (ret == 0 && r != -ENOENT) | |
168 | ret = r; | |
e3bff60a MP |
169 | continue; |
170 | } | |
171 | ||
172 | is_dir = S_ISDIR(st.st_mode); | |
173 | } else | |
174 | is_dir = de->d_type == DT_DIR; | |
175 | ||
176 | if (is_dir) { | |
f2dec872 | 177 | _cleanup_close_ int subdir_fd = -1; |
e3bff60a MP |
178 | |
179 | /* if root_dev is set, remove subdirectories only if device is same */ | |
180 | if (root_dev && st.st_dev != root_dev->st_dev) | |
181 | continue; | |
182 | ||
183 | subdir_fd = openat(fd, de->d_name, O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW|O_NOATIME); | |
184 | if (subdir_fd < 0) { | |
185 | if (ret == 0 && errno != ENOENT) | |
186 | ret = -errno; | |
187 | continue; | |
188 | } | |
189 | ||
190 | /* Stop at mount points */ | |
86f210e9 | 191 | r = fd_is_mount_point(fd, de->d_name, 0); |
e3bff60a MP |
192 | if (r < 0) { |
193 | if (ret == 0 && r != -ENOENT) | |
194 | ret = r; | |
195 | ||
e3bff60a MP |
196 | continue; |
197 | } | |
f2dec872 | 198 | if (r > 0) |
e3bff60a | 199 | continue; |
e3bff60a | 200 | |
3a6ce677 | 201 | if ((flags & REMOVE_SUBVOLUME) && btrfs_might_be_subvol(&st)) { |
e3bff60a MP |
202 | |
203 | /* This could be a subvolume, try to remove it */ | |
204 | ||
db2df898 | 205 | r = btrfs_subvol_remove_fd(fd, de->d_name, BTRFS_REMOVE_RECURSIVE|BTRFS_REMOVE_QUOTA); |
e3bff60a | 206 | if (r < 0) { |
f5e65279 | 207 | if (!IN_SET(r, -ENOTTY, -EINVAL)) { |
e3bff60a MP |
208 | if (ret == 0) |
209 | ret = r; | |
210 | ||
e3bff60a MP |
211 | continue; |
212 | } | |
213 | ||
f2dec872 BR |
214 | /* ENOTTY, then it wasn't a btrfs subvolume, continue below. */ |
215 | } else | |
e3bff60a | 216 | /* It was a subvolume, continue. */ |
e3bff60a | 217 | continue; |
e3bff60a MP |
218 | } |
219 | ||
f2dec872 | 220 | /* We pass REMOVE_PHYSICAL here, to avoid doing the fstatfs() to check the file |
e3bff60a | 221 | * system type again for each directory */ |
f2dec872 | 222 | r = rm_rf_children(TAKE_FD(subdir_fd), flags | REMOVE_PHYSICAL, root_dev); |
e3bff60a MP |
223 | if (r < 0 && ret == 0) |
224 | ret = r; | |
225 | ||
a032b68d MB |
226 | r = unlinkat_harder(fd, de->d_name, AT_REMOVEDIR, flags); |
227 | if (r < 0 && r != -ENOENT && ret == 0) | |
228 | ret = r; | |
e3bff60a MP |
229 | |
230 | } else if (!(flags & REMOVE_ONLY_DIRECTORIES)) { | |
231 | ||
a032b68d MB |
232 | r = unlinkat_harder(fd, de->d_name, 0, flags); |
233 | if (r < 0 && r != -ENOENT && ret == 0) | |
234 | ret = r; | |
e3bff60a MP |
235 | } |
236 | } | |
2897b343 | 237 | return ret; |
e3bff60a MP |
238 | } |
239 | ||
240 | int rm_rf(const char *path, RemoveFlags flags) { | |
241 | int fd, r; | |
242 | struct statfs s; | |
243 | ||
244 | assert(path); | |
245 | ||
bb4f798a MB |
246 | /* For now, don't support dropping subvols when also only dropping directories, since we can't do |
247 | * this race-freely. */ | |
248 | if (FLAGS_SET(flags, REMOVE_ONLY_DIRECTORIES|REMOVE_SUBVOLUME)) | |
249 | return -EINVAL; | |
250 | ||
f2dec872 BR |
251 | /* We refuse to clean the root file system with this call. This is extra paranoia to never cause a |
252 | * really seriously broken system. */ | |
6e866b33 MB |
253 | if (path_equal_or_files_same(path, "/", AT_SYMLINK_NOFOLLOW)) |
254 | return log_error_errno(SYNTHETIC_ERRNO(EPERM), | |
255 | "Attempted to remove entire root file system (\"%s\"), and we can't allow that.", | |
256 | path); | |
e3bff60a | 257 | |
b012e921 | 258 | if (FLAGS_SET(flags, REMOVE_SUBVOLUME | REMOVE_ROOT | REMOVE_PHYSICAL)) { |
e3bff60a | 259 | /* Try to remove as subvolume first */ |
db2df898 | 260 | r = btrfs_subvol_remove(path, BTRFS_REMOVE_RECURSIVE|BTRFS_REMOVE_QUOTA); |
e3bff60a MP |
261 | if (r >= 0) |
262 | return r; | |
263 | ||
f2dec872 BR |
264 | if (FLAGS_SET(flags, REMOVE_MISSING_OK) && r == -ENOENT) |
265 | return 0; | |
266 | ||
f5e65279 | 267 | if (!IN_SET(r, -ENOTTY, -EINVAL, -ENOTDIR)) |
e3bff60a MP |
268 | return r; |
269 | ||
270 | /* Not btrfs or not a subvolume */ | |
271 | } | |
272 | ||
273 | fd = open(path, O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW|O_NOATIME); | |
274 | if (fd < 0) { | |
f2dec872 BR |
275 | if (FLAGS_SET(flags, REMOVE_MISSING_OK) && errno == ENOENT) |
276 | return 0; | |
277 | ||
f5e65279 | 278 | if (!IN_SET(errno, ENOTDIR, ELOOP)) |
e3bff60a MP |
279 | return -errno; |
280 | ||
f2dec872 BR |
281 | if (FLAGS_SET(flags, REMOVE_ONLY_DIRECTORIES)) |
282 | return 0; | |
e3bff60a | 283 | |
f2dec872 BR |
284 | if (FLAGS_SET(flags, REMOVE_ROOT)) { |
285 | ||
286 | if (!FLAGS_SET(flags, REMOVE_PHYSICAL)) { | |
287 | if (statfs(path, &s) < 0) | |
288 | return -errno; | |
289 | ||
290 | if (is_physical_fs(&s)) | |
291 | return log_error_errno(SYNTHETIC_ERRNO(EPERM), | |
292 | "Attempted to remove files from a disk file system under \"%s\", refusing.", | |
293 | path); | |
294 | } | |
295 | ||
296 | if (unlink(path) < 0) { | |
297 | if (FLAGS_SET(flags, REMOVE_MISSING_OK) && errno == ENOENT) | |
298 | return 0; | |
e3bff60a | 299 | |
e3bff60a | 300 | return -errno; |
f2dec872 BR |
301 | } |
302 | } | |
e3bff60a MP |
303 | |
304 | return 0; | |
305 | } | |
306 | ||
307 | r = rm_rf_children(fd, flags, NULL); | |
308 | ||
f2dec872 BR |
309 | if (FLAGS_SET(flags, REMOVE_ROOT) && |
310 | rmdir(path) < 0 && | |
311 | r >= 0 && | |
312 | (!FLAGS_SET(flags, REMOVE_MISSING_OK) || errno != ENOENT)) | |
313 | r = -errno; | |
e3bff60a MP |
314 | |
315 | return r; | |
316 | } |