]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
rm-rf: add new flag REMOVE_CHMOD
authorLennart Poettering <lennart@poettering.net>
Thu, 23 Jul 2020 13:24:54 +0000 (15:24 +0200)
committerLennart Poettering <lennart@poettering.net>
Tue, 25 Aug 2020 16:39:45 +0000 (18:39 +0200)
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
src/basic/rm-rf.h
src/test/meson.build
src/test/test-rm-rf.c [new file with mode: 0644]

index 23cdfa469643ab08bfbb7d51a9c150a8f0f531df..01ff6bb331fd277d6c7de18eb35f5ea88257c52b 100644 (file)
@@ -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;
index 40cbff21c04e50697b19244df6dc73955b0b3243..0edf01ee1c05408b3e0ec321f9dd0fddf16d5dc9 100644 (file)
@@ -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);
index 132989f197eabc4845cfdcf16c5ce043e7f50ad8..835be6466edabe2db901cab598874ecc0716f9e0 100644 (file)
@@ -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 (file)
index 0000000..d6e426c
--- /dev/null
@@ -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;
+}