mirror of
https://github.com/systemd/systemd.git
synced 2025-02-14 05:57:40 +03:00
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:
parent
45374f6503
commit
2899fb024f
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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
74
src/test/test-rm-rf.c
Normal 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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user