diff --git a/src/basic/rm-rf.c b/src/basic/rm-rf.c index 23cdfa4696..01ff6bb331 100644 --- a/src/basic/rm-rf.c +++ b/src/basic/rm-rf.c @@ -23,6 +23,46 @@ static bool is_physical_fs(const struct statfs *sfs) { return !is_temporary_fs(sfs) && !is_cgroup_fs(sfs); } +static int unlinkat_harder( + int dfd, + const char *filename, + int unlink_flags, + RemoveFlags remove_flags) { + + struct stat st; + int r; + + /* Like unlinkat(), but tries harder: if we get EACCESS we'll try to set the r/w/x bits on the + * directory. This is useful if we run unprivileged and have some files where the w bit is + * missing. */ + + if (unlinkat(dfd, filename, unlink_flags) >= 0) + return 0; + if (errno != EACCES || !FLAGS_SET(remove_flags, REMOVE_CHMOD)) + return -errno; + + if (fstat(dfd, &st) < 0) + return -errno; + if (!S_ISDIR(st.st_mode)) + return -ENOTDIR; + if ((st.st_mode & 0700) == 0700) /* Already set? */ + return -EACCES; /* original error */ + if (st.st_uid != geteuid()) /* this only works if the UID matches ours */ + return -EACCES; + + if (fchmod(dfd, (st.st_mode | 0700) & 07777) < 0) + return -errno; + + if (unlinkat(dfd, filename, unlink_flags) < 0) { + r = -errno; + /* Try to restore the original access mode if this didn't work */ + (void) fchmod(dfd, st.st_mode & 07777); + return r; + } + + return 0; +} + int rm_rf_children(int fd, RemoveFlags flags, struct stat *root_dev) { _cleanup_closedir_ DIR *d = NULL; struct dirent *de; @@ -132,17 +172,15 @@ int rm_rf_children(int fd, RemoveFlags flags, struct stat *root_dev) { if (r < 0 && ret == 0) ret = r; - if (unlinkat(fd, de->d_name, AT_REMOVEDIR) < 0) { - if (ret == 0 && errno != ENOENT) - ret = -errno; - } + r = unlinkat_harder(fd, de->d_name, AT_REMOVEDIR, flags); + if (r < 0 && r != -ENOENT && ret == 0) + ret = r; } else if (!(flags & REMOVE_ONLY_DIRECTORIES)) { - if (unlinkat(fd, de->d_name, 0) < 0) { - if (ret == 0 && errno != ENOENT) - ret = -errno; - } + r = unlinkat_harder(fd, de->d_name, 0, flags); + if (r < 0 && r != -ENOENT && ret == 0) + ret = r; } } return ret; diff --git a/src/basic/rm-rf.h b/src/basic/rm-rf.h index 40cbff21c0..0edf01ee1c 100644 --- a/src/basic/rm-rf.h +++ b/src/basic/rm-rf.h @@ -11,6 +11,7 @@ typedef enum RemoveFlags { REMOVE_PHYSICAL = 1 << 2, /* If not set, only removes files on tmpfs, never physical file systems */ REMOVE_SUBVOLUME = 1 << 3, /* Drop btrfs subvolumes in the tree too */ REMOVE_MISSING_OK = 1 << 4, /* If the top-level directory is missing, ignore the ENOENT for it */ + REMOVE_CHMOD = 1 << 5, /* chmod() for write access if we cannot delete something */ } RemoveFlags; int rm_rf_children(int fd, RemoveFlags flags, struct stat *root_dev); diff --git a/src/test/meson.build b/src/test/meson.build index 132989f197..835be6466e 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -658,6 +658,10 @@ tests += [ [], []], + [['src/test/test-rm-rf.c'], + [], + []], + [['src/test/test-chase-symlinks.c'], [], [], diff --git a/src/test/test-rm-rf.c b/src/test/test-rm-rf.c new file mode 100644 index 0000000000..d6e426c0fb --- /dev/null +++ b/src/test/test-rm-rf.c @@ -0,0 +1,74 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "alloc-util.h" +#include "process-util.h" +#include "rm-rf.h" +#include "string-util.h" +#include "tests.h" +#include "tmpfile-util.h" + +static void test_rm_rf_chmod_inner(void) { + _cleanup_free_ char *d = NULL; + const char *x, *y; + + assert_se(getuid() != 0); + + assert_se(mkdtemp_malloc(NULL, &d) >= 0); + + x = strjoina(d, "/d"); + assert_se(mkdir(x, 0700) >= 0); + y = strjoina(x, "/f"); + assert_se(mknod(y, S_IFREG | 0600, 0) >= 0); + + assert_se(chmod(y, 0400) >= 0); + assert_se(chmod(x, 0500) >= 0); + assert_se(chmod(d, 0500) >= 0); + + assert_se(rm_rf(d, REMOVE_PHYSICAL|REMOVE_ROOT) == -EACCES); + + assert_se(access(d, F_OK) >= 0); + assert_se(access(x, F_OK) >= 0); + assert_se(access(y, F_OK) >= 0); + + assert_se(rm_rf(d, REMOVE_PHYSICAL|REMOVE_ROOT|REMOVE_CHMOD) >= 0); + + errno = 0; + assert_se(access(d, F_OK) < 0 && errno == ENOENT); +} + +static void test_rm_rf_chmod(void) { + int r; + + log_info("/* %s */", __func__); + + if (getuid() == 0) { + /* This test only works unpriv (as only then the access mask for the owning user matters), + * hence drop privs here */ + + r = safe_fork("(setresuid)", FORK_DEATHSIG|FORK_WAIT, NULL); + assert_se(r >= 0); + + if (r == 0) { + /* child */ + + assert_se(setresuid(1, 1, 1) >= 0); + + test_rm_rf_chmod_inner(); + _exit(EXIT_SUCCESS); + } + + return; + } + + test_rm_rf_chmod_inner(); +} + +int main(int argc, char **argv) { + test_setup_logging(LOG_DEBUG); + + test_rm_rf_chmod(); + + return 0; +}