[dovecot-cvs] dovecot/src/lib unlink-directory.c,1.5,1.6
cras at procontrol.fi
cras at procontrol.fi
Mon Feb 24 03:16:32 EET 2003
Update of /home/cvs/dovecot/src/lib
In directory danu:/tmp/cvs-serv24622
Modified Files:
unlink-directory.c
Log Message:
unlink_directory() is now (hopefully) race-condition free.
Index: unlink-directory.c
===================================================================
RCS file: /home/cvs/dovecot/src/lib/unlink-directory.c,v
retrieving revision 1.5
retrieving revision 1.6
diff -u -d -r1.5 -r1.6
--- unlink-directory.c 21 Dec 2002 12:13:58 -0000 1.5
+++ unlink-directory.c 24 Feb 2003 01:16:30 -0000 1.6
@@ -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;
- dirp = opendir(dir);
- if (dirp == NULL)
- return errno == ENOENT ? 0 : -1;
+ /* 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.
+ */
+
+#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(d->d_name) == -1 && errno != ENOENT) {
+ old_errno = errno;
- if (unlink(path) == -1 && errno != ENOENT) {
- int old_errno = errno;
+ 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 (lstat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
- if (unlink_directory(path, TRUE) < 0)
- return -1;
+ 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 (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) == -1 && errno != ENOENT)
+ if (rmdir(dir) < 0 && errno != ENOENT)
return -1;
}
- return 0;
+ return ret;
}
More information about the dovecot-cvs
mailing list