From f6f8bcd2a57c06983296485cc028ebdf467ebfd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6ttsche?= Date: Fri, 5 Aug 2022 17:57:22 +0200 Subject: [PATCH] Avoid races in remove_tree() Use *at() functions to pin the directory operating in to avoid being redirected by unprivileged users replacing parts of paths by symlinks to privileged files. --- libmisc/remove_tree.c | 145 ++++++++++++++++++++---------------------- 1 file changed, 68 insertions(+), 77 deletions(-) diff --git a/libmisc/remove_tree.c b/libmisc/remove_tree.c index 04bc7fc4..3d76b95e 100644 --- a/libmisc/remove_tree.c +++ b/libmisc/remove_tree.c @@ -11,6 +11,7 @@ #ident "$Id$" +#include #include #include #include @@ -21,6 +22,72 @@ #include "prototypes.h" #include "defines.h" +static int remove_tree_at (int at_fd, const char *path, bool remove_root) +{ + DIR *dir; + const struct dirent *ent; + int dir_fd, rc = 0; + + dir_fd = openat (at_fd, path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (dir_fd < 0) { + return -1; + } + + dir = fdopendir (dir_fd); + if (!dir) { + (void) close (dir_fd); + return -1; + } + + /* + * Open the source directory and delete each entry. + */ + while ((ent = readdir (dir))) { + struct stat ent_sb; + + /* + * Skip the "." and ".." entries + */ + if (strcmp (ent->d_name, ".") == 0 || + strcmp (ent->d_name, "..") == 0) { + continue; + } + + rc = fstatat (dirfd(dir), ent->d_name, &ent_sb, AT_SYMLINK_NOFOLLOW); + if (rc < 0) { + break; + } + + if (S_ISDIR (ent_sb.st_mode)) { + /* + * Recursively delete this directory. + */ + if (remove_tree_at (dirfd(dir), ent->d_name, true) != 0) { + rc = -1; + break; + } + } else { + /* + * Delete the file. + */ + if (unlinkat (dirfd(dir), ent->d_name, 0) != 0) { + rc = -1; + break; + } + } + } + + (void) closedir (dir); + + if (remove_root && (0 == rc)) { + if (unlinkat (at_fd, path, AT_REMOVEDIR) != 0) { + rc = -1; + } + } + + return rc; +} + /* * remove_tree - delete a directory tree * @@ -28,83 +95,7 @@ * and directories. * At the end, it deletes the root directory itself. */ - int remove_tree (const char *root, bool remove_root) { - char *new_name = NULL; - int err = 0; - struct dirent *ent; - struct stat sb; - DIR *dir; - - /* - * Open the source directory and read each entry. Every file - * entry in the directory is copied with the UID and GID set - * to the provided values. As an added security feature only - * regular files (and directories ...) are copied, and no file - * is made set-ID. - */ - dir = opendir (root); - if (NULL == dir) { - return -1; - } - - while ((ent = readdir (dir))) { - size_t new_len = strlen (root) + strlen (ent->d_name) + 2; - - /* - * Skip the "." and ".." entries - */ - - if (strcmp (ent->d_name, ".") == 0 || - strcmp (ent->d_name, "..") == 0) { - continue; - } - - /* - * Make the filename for the current entry. - */ - - free (new_name); - new_name = (char *) malloc (new_len); - if (NULL == new_name) { - err = -1; - break; - } - (void) snprintf (new_name, new_len, "%s/%s", root, ent->d_name); - if (LSTAT (new_name, &sb) == -1) { - continue; - } - - if (S_ISDIR (sb.st_mode)) { - /* - * Recursively delete this directory. - */ - if (remove_tree (new_name, true) != 0) { - err = -1; - break; - } - } else { - /* - * Delete the file. - */ - if (unlink (new_name) != 0) { - err = -1; - break; - } - } - } - if (NULL != new_name) { - free (new_name); - } - (void) closedir (dir); - - if (remove_root && (0 == err)) { - if (rmdir (root) != 0) { - err = -1; - } - } - - return err; + return remove_tree_at (AT_FDCWD, root, remove_root); } -