From: Lennart Poettering Date: Thu, 23 Jul 2020 13:24:54 +0000 (+0200) Subject: rm-rf: add new flag REMOVE_CHMOD X-Git-Tag: v247-rc1~362^2~10 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=2899fb024f066f1cb14989fb470e188de7d6dc88;p=thirdparty%2Fsystemd.git rm-rf: add new flag REMOVE_CHMOD When removing a directory tree as unprivileged user we might encounter files owned by us but not deletable since the containing directory might have the "r" bit missing in its access mode. Let's try to deal with this: optionally if we get EACCES try to set the bit and see if it works then. --- diff --git a/src/basic/rm-rf.c b/src/basic/rm-rf.c index 23cdfa46964..01ff6bb331f 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 40cbff21c04..0edf01ee1c0 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 132989f197e..835be6466ed 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 00000000000..d6e426c0fbe --- /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; +}