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.
This commit is contained in:
Christian Göttsche 2022-08-05 17:57:22 +02:00 committed by Serge Hallyn
parent e9ae247cb1
commit f6f8bcd2a5

View File

@ -11,6 +11,7 @@
#ident "$Id$" #ident "$Id$"
#include <fcntl.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -21,6 +22,72 @@
#include "prototypes.h" #include "prototypes.h"
#include "defines.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 * remove_tree - delete a directory tree
* *
@ -28,83 +95,7 @@
* and directories. * and directories.
* At the end, it deletes the root directory itself. * At the end, it deletes the root directory itself.
*/ */
int remove_tree (const char *root, bool remove_root) int remove_tree (const char *root, bool remove_root)
{ {
char *new_name = NULL; return remove_tree_at (AT_FDCWD, root, remove_root);
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;
}