From: Lennart Poettering Date: Tue, 26 Jan 2021 16:47:07 +0100 Subject: rm-rf: fstatat() might fail if containing dir has limited access mode, patch that too (cherry picked from commit 1b55621dabf741dd963f59ac706ea62cd6e3e95c) (cherry picked from commit ce53b81a600e2162ee86e2f4d202e7f28eceb2c6) --- src/basic/rm-rf.c | 82 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 16 deletions(-) diff --git a/src/basic/rm-rf.c b/src/basic/rm-rf.c index 4c39ce8..2f2ebc3 100644 --- a/src/basic/rm-rf.c +++ b/src/basic/rm-rf.c @@ -23,13 +23,38 @@ static bool is_physical_fs(const struct statfs *sfs) { return !is_temporary_fs(sfs) && !is_cgroup_fs(sfs); } +static int patch_dirfd_mode( + int dfd, + mode_t *ret_old_mode) { + + struct stat st; + + assert(dfd >= 0); + assert(ret_old_mode); + + if (fstat(dfd, &st) < 0) + return -errno; + if (!S_ISDIR(st.st_mode)) + return -ENOTDIR; + if (FLAGS_SET(st.st_mode, 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; + + *ret_old_mode = st.st_mode; + return 0; +} + static int unlinkat_harder( int dfd, const char *filename, int unlink_flags, RemoveFlags remove_flags) { - struct stat st; + mode_t old_mode; int r; /* Like unlinkat(), but tries harder: if we get EACCESS we'll try to set the r/w/x bits on the @@ -41,22 +66,46 @@ static int unlinkat_harder( 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 (FLAGS_SET(st.st_mode, 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; + r = patch_dirfd_mode(dfd, &old_mode); + if (r < 0) + return r; 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); + (void) fchmod(dfd, old_mode); + return r; + } + + /* If this worked, we won't reset the old mode, since we'll need it for other entries too, and we + * should destroy the whole thing */ + return 0; +} + +static int fstatat_harder( + int dfd, + const char *filename, + struct stat *ret, + int fstatat_flags, + RemoveFlags remove_flags) { + + mode_t old_mode; + int r; + + /* Like unlink_harder() but does the same for fstatat() */ + + if (fstatat(dfd, filename, ret, fstatat_flags) >= 0) + return 0; + if (errno != EACCES || !FLAGS_SET(remove_flags, REMOVE_CHMOD)) + return -errno; + + r = patch_dirfd_mode(dfd, &old_mode); + if (r < 0) + return r; + + if (fstatat(dfd, filename, ret, fstatat_flags) < 0) { + r = -errno; + (void) fchmod(dfd, old_mode); return r; } @@ -112,9 +161,10 @@ int rm_rf_children(int fd, RemoveFlags flags, struct stat *root_dev) { if (de->d_type == DT_UNKNOWN || (de->d_type == DT_DIR && (root_dev || (flags & REMOVE_SUBVOLUME)))) { - if (fstatat(fd, de->d_name, &st, AT_SYMLINK_NOFOLLOW) < 0) { - if (ret == 0 && errno != ENOENT) - ret = -errno; + r = fstatat_harder(fd, de->d_name, &st, AT_SYMLINK_NOFOLLOW, flags); + if (r < 0) { + if (ret == 0 && r != -ENOENT) + ret = r; continue; }