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.
This commit is contained in:
Lennart Poettering 2020-07-23 15:24:54 +02:00
parent 45374f6503
commit 2899fb024f
4 changed files with 125 additions and 8 deletions

View 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;

View 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);

View File

@ -658,6 +658,10 @@ tests += [
[],
[]],
[['src/test/test-rm-rf.c'],
[],
[]],
[['src/test/test-chase-symlinks.c'],
[],
[],

74
src/test/test-rm-rf.c Normal file
View File

@ -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;
}