Skip to content

Commit

Permalink
Avoid races in remove_tree()
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cgzones authored and hallyn committed Aug 17, 2022
1 parent e9ae247 commit f6f8bcd
Showing 1 changed file with 39 additions and 48 deletions.
87 changes: 39 additions & 48 deletions libmisc/remove_tree.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

#ident "$Id$"

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
Expand All @@ -21,90 +22,80 @@
#include "prototypes.h"
#include "defines.h"

/*
* remove_tree - delete a directory tree
*
* remove_tree() walks a directory tree and deletes all the files
* and directories.
* At the end, it deletes the root directory itself.
*/

int remove_tree (const char *root, bool remove_root)
static int remove_tree_at (int at_fd, const char *path, bool remove_root)
{
char *new_name = NULL;
int err = 0;
struct dirent *ent;
struct stat sb;
DIR *dir;
const struct dirent *ent;
int dir_fd, rc = 0;

/*
* 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) {
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))) {
size_t new_len = strlen (root) + strlen (ent->d_name) + 2;
struct stat ent_sb;

/*
* 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;
rc = fstatat (dirfd(dir), ent->d_name, &ent_sb, AT_SYMLINK_NOFOLLOW);
if (rc < 0) {
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)) {
if (S_ISDIR (ent_sb.st_mode)) {
/*
* Recursively delete this directory.
*/
if (remove_tree (new_name, true) != 0) {
err = -1;
if (remove_tree_at (dirfd(dir), ent->d_name, true) != 0) {
rc = -1;
break;
}
} else {
/*
* Delete the file.
*/
if (unlink (new_name) != 0) {
err = -1;
if (unlinkat (dirfd(dir), ent->d_name, 0) != 0) {
rc = -1;
break;
}
}
}
if (NULL != new_name) {
free (new_name);
}

(void) closedir (dir);

if (remove_root && (0 == err)) {
if (rmdir (root) != 0) {
err = -1;
if (remove_root && (0 == rc)) {
if (unlinkat (at_fd, path, AT_REMOVEDIR) != 0) {
rc = -1;
}
}

return err;
return rc;
}

/*
* remove_tree - delete a directory tree
*
* remove_tree() walks a directory tree and deletes all the files
* and directories.
* At the end, it deletes the root directory itself.
*/
int remove_tree (const char *root, bool remove_root)
{
return remove_tree_at (AT_FDCWD, root, remove_root);
}

0 comments on commit f6f8bcd

Please sign in to comment.