From 4d16bbb4de8cbcea2476bd8433be42c9d080e1f6 Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
Date: Thu, 23 Jul 2020 15:24:54 +0200
Subject: [PATCH 1/9] rm-rf: add new flag REMOVE_CHMOD

Conflict:NA
Reference:https://github.com/systemd/systemd/commit/2899fb024f066f1cb14989fb470e188de7d6dc88

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.
---
 src/basic/rm-rf.c     | 54 ++++++++++++++++++++++++++-----
 src/basic/rm-rf.h     |  1 +
 src/test/meson.build  |  4 +++
 src/test/test-rm-rf.c | 74 +++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 125 insertions(+), 8 deletions(-)
 create mode 100644 src/test/test-rm-rf.c

diff --git a/src/basic/rm-rf.c b/src/basic/rm-rf.c
index 796eb93..03b41f3 100644
--- a/src/basic/rm-rf.c
+++ b/src/basic/rm-rf.c
@@ -25,6 +25,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;
@@ -134,17 +174,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 40cbff2..0edf01e 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 3fcfa9f..255b00b 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -631,6 +631,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 0000000..d6e426c
--- /dev/null
+++ b/src/test/test-rm-rf.c
@@ -0,0 +1,74 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <unistd.h>
+
+#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;
+}
-- 
2.23.0

