Mercurial > dovecot > core-2.2
changeset 1243:478471737964 HEAD
unlink_directory() is now (hopefully) race-condition free.
author | Timo Sirainen <tss@iki.fi> |
---|---|
date | Mon, 24 Feb 2003 03:16:30 +0200 |
parents | 4c5f325df4ac |
children | 40bae47bdba1 |
files | src/lib/unlink-directory.c |
diffstat | 1 files changed, 136 insertions(+), 21 deletions(-) [+] |
line wrap: on
line diff
--- a/src/lib/unlink-directory.c Mon Feb 24 02:29:20 2003 +0200 +++ b/src/lib/unlink-directory.c Mon Feb 24 03:16:30 2003 +0200 @@ -1,7 +1,7 @@ /* - unlink-directory.c : Unlink directory with everything under it. + unlink-directory.c : Safely unlink directory with everything under it. - Copyright (c) 2002 Timo Sirainen + Copyright (c) 2002-2003 Timo Sirainen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -23,24 +23,104 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +#define _GNU_SOURCE /* for O_NOFOLLOW with Linux */ + #include "lib.h" #include "unlink-directory.h" +#include <fcntl.h> #include <unistd.h> #include <dirent.h> #include <sys/stat.h> -int unlink_directory(const char *dir, int unlink_dir) +#define close_save_errno(fd) \ + STMT_START { \ + old_errno = errno; \ + (void)close(fd); \ + errno = old_errno; \ + } STMT_END + +static int unlink_directory_r(const char *dir) { DIR *dirp; struct dirent *d; struct stat st; - char path[PATH_MAX]; + int dir_fd, old_errno; + + /* There's a bit tricky race condition with recursive deletion. + Suppose this happens: + + lstat(dir, ..) -> OK, it's a directory + // attacker deletes dir, replaces it with symlink to / + opendir(dir) -> it actually opens / + + Most portable solution is to lstat() the dir, chdir() there, then + check that "." points to same device/inode as we originally + lstat()ed. This assumes that the device has usable inodes, most + should except for some NFS implementations. + + Filesystems may also reassign a deleted inode to another file + immediately after it's deleted. That in theory makes it possible to + exploit this race to delete the new directory. However, the new + inode is quite unlikely to be any important directory, and attacker + is quite unlikely to find out which directory even got the inode. + Maybe with some setuid program or daemon interaction something could + come out of it though. + + Another less portable solution is to fchdir(open(dir, O_NOFOLLOW)). + This should be completely safe. + + The actual file deletion also has to be done relative to current + directory, to make sure that the whole directory structure isn't + replaced with another one while we're deleting it. Going back to + parent directory isn't too easy either - safest (and easiest) way + again is to open() the directory and fchdir() back there. + */ - dirp = opendir(dir); - if (dirp == NULL) - return errno == ENOENT ? 0 : -1; +#ifdef O_NOFOLLOW + dir_fd = open(dir, O_RDONLY | O_NOFOLLOW); + if (dir_fd == -1) + return -1; +#else + struct stat st2; + + if (lstat(dir, &st) < 0) + return -1; + + if (!S_ISDIR(st.st_mode)) { + errno = ENOTDIR; + return -1; + } + + dir_fd = open(dir, O_RDONLY); + if (dir_fd == -1) + return -1; + if (fstat(dir_fd, &st2) < 0) { + close_save_errno(dir_fd); + return -1; + } + + if (st.st_ino != st2.st_ino || + !CMP_DEV_T(st.st_dev, st2.st_dev)) { + /* directory was just replaced with something else. */ + (void)close(dir_fd); + errno = ENOTDIR; + return -1; + } +#endif + if (fchdir(dir_fd) < 0) { + close_save_errno(dir_fd); + return -1; + } + + dirp = opendir("."); + if (dirp == NULL) { + close_save_errno(dir_fd); + return -1; + } + + errno = 0; while ((d = readdir(dirp)) != NULL) { if (d->d_name[0] == '.' && (d->d_name[1] == '\0' || @@ -49,31 +129,66 @@ continue; } - if (str_path(path, sizeof(path), dir, d->d_name) < 0) - return -1; - - if (unlink(path) == -1 && errno != ENOENT) { - int old_errno = errno; + if (unlink(d->d_name) == -1 && errno != ENOENT) { + old_errno = errno; - if (lstat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - if (unlink_directory(path, TRUE) < 0) - return -1; + if (lstat(d->d_name, &st) == 0 && S_ISDIR(st.st_mode)) { + if (unlink_directory_r(d->d_name) < 0) { + if (errno != ENOENT) + break; + errno = 0; + } + if (fchdir(dir_fd) < 0) + break; + + if (rmdir(d->d_name) < 0) { + if (errno != ENOENT) + break; + errno = 0; + } } else { - /* so it wasn't a directory, unlink() again - to get correct errno */ + /* so it wasn't a directory */ errno = old_errno; - return -1; + break; } } } + (void)close(dir_fd); + + old_errno = errno; if (closedir(dirp) < 0) return -1; - if (unlink_dir) { - if (rmdir(dir) == -1 && errno != ENOENT) - return -1; + if (old_errno != 0) { + errno = old_errno; + return -1; } return 0; } + +int unlink_directory(const char *dir, int unlink_dir) +{ + int fd, ret; + + fd = open(".", O_RDONLY); + if (fd == -1) + return -1; + + ret = unlink_directory_r(dir); + if (ret < 0 && errno == ENOENT) + ret = 0; + + if (fchdir(fd) < 0) { + i_fatal("unlink_directory(%s): " + "Can't fchdir() back to our original dir: %m", dir); + } + + if (unlink_dir) { + if (rmdir(dir) < 0 && errno != ENOENT) + return -1; + } + + return ret; +}