Avoid TOCTOU in sudo_mkdir_parents() using openat(2) and mkdirat(2).

This also allows us to make path const as it should be.
This commit is contained in:
Todd C. Miller
2021-12-11 08:35:18 -07:00
parent 55db239243
commit c13b21c199
12 changed files with 192 additions and 43 deletions

View File

@@ -1,7 +1,7 @@
/*
* SPDX-License-Identifier: ISC
*
* Copyright (c) 2009-2017 Todd C. Miller <Todd.Miller@sudo.ws>
* Copyright (c) 2009-2021 Todd C. Miller <Todd.Miller@sudo.ws>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@@ -33,6 +33,7 @@
#endif /* HAVE_STDBOOL_H */
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
@@ -44,68 +45,111 @@
/*
* Create any parent directories needed by path (but not path itself).
* Note that path is modified but is restored before it returns.
*/
bool
sudo_mkdir_parents_v1(char *path, uid_t uid, gid_t gid, mode_t mode, bool quiet)
sudo_mkdir_parents_v1(const char *path, uid_t uid, gid_t gid, mode_t mode, bool quiet)
{
char *slash = path;
const char *cp, *ep, *pathend;
bool ret = false;
int parentfd;
debug_decl(sudo_mkdir_parents, SUDO_DEBUG_UTIL);
while ((slash = strchr(slash + 1, '/')) != NULL) {
struct stat sb;
int dfd;
/* Starting parent dir is either root or cwd. */
cp = path;
if (*cp == '/') {
do {
cp++;
} while (*cp == '/');
parentfd = open("/", O_RDONLY|O_NONBLOCK);
} else {
parentfd = open(".", O_RDONLY|O_NONBLOCK);
}
if (parentfd == -1) {
if (!quiet)
sudo_warn(U_("unable to open %s"), *path == '/' ? "/" : ".");
debug_return_bool(false);
}
/* Iterate over path components, skipping the last one. */
pathend = cp + strlen(cp);
for (cp = sudo_strsplit(cp, pathend, "/", &ep); cp != NULL && ep != NULL;
cp = sudo_strsplit(NULL, pathend, "/", &ep)) {
struct stat sb;
char name[MAXNAMLEN + 1];
int dfd, len;
*slash = '\0';
sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
"mkdir %s, mode 0%o, uid %d, gid %d", path, (unsigned int)mode,
(int)uid, (int)gid);
"mkdir %.*s, mode 0%o, uid %d, gid %d", (int)(ep - path),
path, (unsigned int)mode, (int)uid, (int)gid);
len = snprintf(name, sizeof(name), "%.*s", (int)(ep - cp), cp);
if (len >= ssizeof(name)) {
errno = ENAMETOOLONG;
if (!quiet)
sudo_warn(U_("unable to open %.*s"), (int)(ep - path), path);
goto done;
}
reopen:
dfd = open(path, O_RDONLY|O_NONBLOCK);
dfd = openat(parentfd, name, O_RDONLY|O_NONBLOCK, 0);
if (dfd == -1) {
if (errno != ENOENT) {
if (!quiet)
sudo_warn(U_("unable to open %s"), path);
goto bad;
if (!quiet) {
sudo_warn(U_("unable to open %.*s"),
(int)(ep - path), path);
}
goto done;
}
if (mkdir(path, mode) == 0) {
if (mkdirat(parentfd, name, mode) == 0) {
dfd = openat(parentfd, name, O_RDONLY|O_NONBLOCK, 0);
if (dfd == -1) {
if (!quiet) {
sudo_warn(U_("unable to open %.*s"),
(int)(ep - path), path);
}
goto done;
}
if (uid != (uid_t)-1 && gid != (gid_t)-1) {
if (chown(path, uid, gid) != 0) {
if (fchown(dfd, uid, gid) != 0) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
"%s: unable to chown %d:%d %s", __func__,
(int)uid, (int)gid, path);
"%s: unable to chown %d:%d %.*s", __func__,
(int)uid, (int)gid, (int)(ep - path), path);
}
}
} else {
if (errno == EEXIST)
goto reopen;
if (!quiet)
sudo_warn(U_("unable to mkdir %s"), path);
goto bad;
if (!quiet) {
sudo_warn(U_("unable to mkdir %.*s"),
(int)(ep - path), path);
}
goto done;
}
} else {
/* Already exists, make sure it is a directory. */
int rc = fstat(dfd, &sb);
close(dfd);
if (rc != 0) {
if (!quiet)
sudo_warn(U_("unable to stat %s"), path);
goto bad;
if (fstat(dfd, &sb) != 0) {
if (!quiet) {
sudo_warn(U_("unable to stat %.*s"),
(int)(ep - path), path);
}
close(dfd);
goto done;
}
if (!S_ISDIR(sb.st_mode)) {
if (!quiet)
sudo_warnx(U_("%s exists but is not a directory (0%o)"),
path, (unsigned int) sb.st_mode);
goto bad;
if (!quiet) {
sudo_warnx(U_("%.*s exists but is not a directory (0%o)"),
(int)(ep - path), path, (unsigned int) sb.st_mode);
}
close(dfd);
goto done;
}
}
*slash = '/';
close(parentfd);
parentfd = dfd;
}
ret = true;
debug_return_bool(true);
bad:
/* We must restore the path before we return. */
/* cppcheck-suppress nullPointerRedundantCheck */
*slash = '/';
debug_return_bool(false);
done:
if (parentfd != -1)
close(parentfd);
debug_return_bool(ret);
}