CVE-2021-35939: Improper Link Resolution Before File Access ('Link Following') (CWE-59)
value: It was found that the fix for CVE-2017-7500 and CVE-2017-7501 was incomplete: the check was only implemented for the parent directory of the file to be created. A local unprivileged user who owns another ancestor directory could potentially use this flaw to gain root privileges. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.
CWE description: The product attempts to access a file based on the filename, but it does not properly prevent that filename from identifying a link or shortcut that resolves to an unintended resource.
CVSS v3 base score: 6.7
Published: 2022-08-26T16:15Z
Repository: https://github.com/rpm-software-management/rpm
Language: Other
Files changed: INSTALL, configure.ac, lib/fsm.c
Commit date: 2022-02-16 10:57:18 +0200
Commit message: Validate intermediate symlinks during installation, CVE-2021-35939

Whenever directory changes during unpacking, walk the entire tree from
starting from / and validate any symlinks crossed, fail the install
on invalid links.

This is the first of step of many towards securing our file operations
against local tamperers and besides plugging that one CVE, paves the way
for the next step by adding the necessary directory fd tracking.
This also bumps the rpm OS requirements to a whole new level by requiring
the *at() family of calls from POSIX-1.2008.

This necessarily does a whole lot of huffing and puffing we previously
did not do. It should be possible to cache secure (ie root-owned)
directory structures to avoid validating everything a million times
but for now, just keeping things simple.

Diff: diff --git a/INSTALL b/INSTALL
index 677ef88c42..961a160e0d 100644
--- a/INSTALL
+++ b/INSTALL
@@ -101,10 +101,12 @@ OpenMP multithreading support is automatically enabled if your C compiler has
 support for OpenMP version 4.5 or higher (to disable, pass the --disable-openmp
 option to configure).  For GCC, OpenMP 4.5 is fully supported since GCC 6.1,
 which is available from
     http://www.gnu.org/
 
+Rpm requires a POSIX.1-2008 level operating system.
+
 To compile RPM:
 --------------
 
 RPM uses a small shell script to run: libtool, autoconf,
 automake. This step should not be necessary if you are running a
diff --git a/configure.ac b/configure.ac
index 3ee3407268..0099e5f34e 100644
--- a/configure.ac
+++ b/configure.ac
@@ -578,11 +578,12 @@ AC_REPLACE_FUNCS(stpcpy stpncpy)
 
 AC_CHECK_FUNCS([secure_getenv __secure_getenv])
 
 AC_CHECK_FUNCS(
    [mkstemp getcwd basename dirname realpath setenv unsetenv regcomp lchown \
-    utimes getline localtime_r statvfs getaddrinfo ],
+    utimes getline localtime_r statvfs getaddrinfo \
+    openat mkdirat fstatat ],
    [], [AC_MSG_ERROR([function required by rpm])])
 
 AC_LIBOBJ(fnmatch)
 
 dnl check if python is requested
diff --git a/lib/fsm.c b/lib/fsm.c
index 9118983479..b6b152ab10 100644
--- a/lib/fsm.c
+++ b/lib/fsm.c
@@ -6,10 +6,11 @@
 #include "system.h"
 
 #include <inttypes.h>
 #include <utime.h>
 #include <errno.h>
+#include <fcntl.h>
 #if WITH_CAP
 #include <sys/capability.h>
 #endif
 
 #include <rpm/rpmte.h>
@@ -18,10 +19,11 @@
 #include <rpm/rpmmacro.h>
 
 #include "rpmio/rpmio_internal.h"	/* fdInit/FiniDigest */
 #include "lib/fsm.h"
 #include "lib/rpmte_internal.h"	/* XXX rpmfs */
+#include "lib/rpmfi_internal.h" /* rpmfiSetOnChdir */
 #include "lib/rpmplugins.h"	/* rpm plugins hooks */
 #include "lib/rpmug.h"
 
 #include "debug.h"
 
@@ -404,21 +406,122 @@ static int fsmRmdir(const char *path)
 	default:            rc = RPMERR_RMDIR_FAILED; break;
 	}
     return rc;
 }
 
-static int fsmMkdir(const char *path, mode_t mode)
+static int fsmMkdir(int dirfd, const char *path, mode_t mode)
 {
-    int rc = mkdir(path, (mode & 07777));
+    int rc = mkdirat(dirfd, path, (mode & 07777));
     if (_fsm_debug)
-	rpmlog(RPMLOG_DEBUG, " %8s (%s, 0%04o) %s\n", __func__,
-	       path, (unsigned)(mode & 07777),
+	rpmlog(RPMLOG_DEBUG, " %8s (%d %s, 0%04o) %s\n", __func__,
+	       dirfd, path, (unsigned)(mode & 07777),
 	       (rc < 0 ? strerror(errno) : ""));
     if (rc < 0)	rc = RPMERR_MKDIR_FAILED;
     return rc;
 }
 
+static int fsmOpenat(int dirfd, const char *path, int flags)
+{
+    struct stat lsb, sb;
+    int sflags = flags | O_NOFOLLOW;
+    int fd = openat(dirfd, path, sflags);
+
+    /*
+     * Only ever follow symlinks by root or target owner. Since we can't
+     * open the symlink itself, the order matters: we stat the link *after*
+     * opening the target, and if the link ownership changed between the calls
+     * it could've only been the link owner or root.
+     */
+    if (fd < 0 && errno == ELOOP && flags != sflags) {
+	int ffd = openat(dirfd, path, flags);
+	if (ffd >= 0 && fstatat(dirfd, path, &lsb, AT_SYMLINK_NOFOLLOW) == 0) {
+	    if (fstat(ffd, &sb) == 0) {
+		if (lsb.st_uid == 0 || lsb.st_uid == sb.st_uid) {
+		    fd = ffd;
+		} else {
+		    close(ffd);
+		}
+	    }
+	}
+    }
+    return fd;
+}
+
+static int fsmDoMkDir(rpmPlugins plugins, int dirfd, const char *dn,
+			int owned, mode_t mode)
+{
+    int rc;
+    rpmFsmOp op = (FA_CREATE);
+    if (!owned)
+	op |= FAF_UNOWNED;
+
+    /* Run fsm file pre hook for all plugins */
+    rc = rpmpluginsCallFsmFilePre(plugins, NULL, dn, mode, op);
+
+    if (!rc)
+	rc = fsmMkdir(dirfd, dn, mode);
+
+    if (!rc) {
+	rc = rpmpluginsCallFsmFilePrepare(plugins, NULL, dn, dn, mode, op);
+    }
+
+    /* Run fsm file post hook for all plugins */
+    rpmpluginsCallFsmFilePost(plugins, NULL, dn, mode, op, rc);
+
+    if (!rc) {
+	rpmlog(RPMLOG_DEBUG,
+		"%s directory created with perms %04o\n",
+		dn, (unsigned)(mode & 07777));
+    }
+
+    return rc;
+}
+
+static int ensureDir(rpmPlugins plugins, const char *p, int owned, int create)
+{
+    char *path = xstrdup(p);
+    char *dp = path;
+    char *sp = NULL, *bn;
+    int oflags = O_RDONLY;
+
+    int dirfd = fsmOpenat(-1, "/", oflags);
+    int fd = dirfd; /* special case of "/" */
+
+    while ((bn = strtok_r(dp, "/", &sp)) != NULL) {
+	struct stat sb;
+	fd = fsmOpenat(dirfd, bn, oflags);
+
+	if (fd < 0 && errno == ENOENT && create) {
+	    mode_t mode = S_IFDIR | (_dirPerms & 07777);
+	    if (fsmDoMkDir(plugins, dirfd, bn, owned, mode) == 0) {
+		fd = fsmOpenat(dirfd, bn, oflags|O_NOFOLLOW);
+	    }
+	}
+
+	if (fd >= 0 && fstat(fd, &sb) == 0 && !S_ISDIR(sb.st_mode)) {
+	    close(fd);
+	    errno = ENOTDIR;
+	    fd = -1;
+	}
+
+	close(dirfd);
+	if (fd >= 0) {
+	    dirfd = fd;
+	} else {
+	    dirfd = -1;
+	    rpmlog(RPMLOG_ERR, _("failed to open dir %s of %s: %s\n"),
+				bn, p, strerror(errno));
+	    break;
+	}
+
+	dp = NULL;
+    }
+
+    free(path);
+    return dirfd;
+}
+
 static int fsmMkfifo(const char *path, mode_t mode)
 {
     int rc = mkfifo(path, (mode & 07777));
 
     if (_fsm_debug) {
@@ -505,11 +608,11 @@ static int fsmMkdirs(rpmfiles files, rpmfs fs, rpmPlugins plugins)
 
 		/* Run fsm file pre hook for all plugins */
 		rc = rpmpluginsCallFsmFilePre(plugins, NULL, dn, mode, op);
 
 		if (!rc)
-		    rc = fsmMkdir(dn, mode);
+		    rc = fsmMkdir(-1, dn, mode);
 
 		if (!rc) {
 		    rc = rpmpluginsCallFsmFilePrepare(plugins, NULL, dn, dn,
 						      mode, op);
 		}
@@ -872,10 +975,25 @@ static void setFileState(rpmfs fs, int i)
     default:
 	break;
     }
 }
 
+struct diriter_s {
+    int dirfd;
+};
+
+static int onChdir(rpmfi fi, void *data)
+{
+    struct diriter_s *di = data;
+
+    if (di->dirfd >= 0) {
+	close(di->dirfd);
+	di->dirfd = -1;
+    }
+    return 0;
+}
+
 int rpmPackageFilesInstall(rpmts ts, rpmte te, rpmfiles files,
               rpmpsm psm, char ** failedFile)
 {
     FD_t payload = rpmtePayload(te);
     rpmfi fi = NULL;
@@ -888,10 +1006,11 @@ int rpmPackageFilesInstall(rpmts ts, rpmte te, rpmfiles files,
     int nofcaps = (rpmtsFlags(ts) & RPMTRANS_FLAG_NOCAPS) ? 1 : 0;
     FD_t firstlinkfile = NULL;
     char *tid = NULL;
     struct filedata_s *fdata = xcalloc(fc, sizeof(*fdata));
     struct filedata_s *firstlink = NULL;
+    struct diriter_s di = { -1 };
 
     /* transaction id used for temporary path suffix while installing */
     rasprintf(&tid, ";%08x", (unsigned)rpmtsGetTid(ts));
 
     /* Collect state data for the whole operation */
@@ -930,10 +1049,11 @@ int rpmPackageFilesInstall(rpmts ts, rpmte te, rpmfiles files,
 	fi = rpmfilesIter(files, RPMFI_ITER_FWD);
     if (fi == NULL) {
         rc = RPMERR_BAD_MAGIC;
         goto exit;
     }
+    rpmfiSetOnChdir(fi, onChdir, &di);
 
     /* Detect and create directories not explicitly in package. */
     if (!rc)
 	rc = fsmMkdirs(files, fs, plugins);
 
@@ -944,10 +1064,20 @@ int rpmPackageFilesInstall(rpmts ts, rpmte te, rpmfiles files,
         if (!fp->skip) {
 	    /* Directories replacing something need early backup */
 	    if (!fp->suffix) {
 		rc = fsmBackup(fi, fp->action);
 	    }
+
+	    if (di.dirfd == -1) {
+		di.dirfd = ensureDir(plugins, rpmfiDN(fi), 0,
+				    (fp->action == FA_CREATE));
+		if (di.dirfd == -1) {
+		    rc = RPMERR_OPEN_FAILED;
+		    break;
+		}
+	    }
+
 	    /* Assume file does't exist when tmp suffix is in use */
 	    if (!fp->suffix) {
 		if (fp->action == FA_TOUCH) {
 		    struct stat sb;
 		    rc = fsmStat(fp->fpath, 1, &sb);
@@ -978,11 +1108,11 @@ int rpmPackageFilesInstall(rpmts ts, rpmte te, rpmfiles files,
             } else if (S_ISDIR(fp->sb.st_mode)) {
                 if (rc == RPMERR_ENOENT) {
                     mode_t mode = fp->sb.st_mode;
                     mode &= ~07777;
                     mode |=  00700;
-                    rc = fsmMkdir(fp->fpath, mode);
+                    rc = fsmMkdir(di.dirfd, fp->fpath, mode);
                 }
             } else if (S_ISLNK(fp->sb.st_mode)) {
 		if (rc == RPMERR_ENOENT) {
 		    rc = fsmSymlink(rpmfiFLink(fi), fp->fpath);
 		}
@@ -1020,10 +1150,12 @@ int rpmPackageFilesInstall(rpmts ts, rpmte te, rpmfiles files,
 	else
 	    rpmpsmNotify(psm, RPMCALLBACK_INST_PROGRESS, rpmfiArchiveTell(fi));
 	fp->stage = FILE_UNPACK;
     }
     fi = rpmfiFree(fi);
+    close(di.dirfd);
+    di.dirfd = -1;
 
     if (!rc && fx < 0 && fx != RPMERR_ITER_END)
 	rc = fx;
 
     /* Set permissions, timestamps etc for non-hardlink entries */

Vulnerable code:     utimes getline localtime_r statvfs getaddrinfo ],
static int fsmMkdir(const char *path, mode_t mode)
    int rc = mkdir(path, (mode & 07777));
	rpmlog(RPMLOG_DEBUG, " %8s (%s, 0%04o) %s\n", __func__,
	       path, (unsigned)(mode & 07777),
		    rc = fsmMkdir(dn, mode);
                    rc = fsmMkdir(fp->fpath, mode);
Fixed code: Rpm requires a POSIX.1-2008 level operating system.

    utimes getline localtime_r statvfs getaddrinfo \
    openat mkdirat fstatat ],
#include <fcntl.h>
#include "lib/rpmfi_internal.h" /* rpmfiSetOnChdir */
static int fsmMkdir(int dirfd, const char *path, mode_t mode)
    int rc = mkdirat(dirfd, path, (mode & 07777));
	rpmlog(RPMLOG_DEBUG, " %8s (%d %s, 0%04o) %s\n", __func__,
	       dirfd, path, (unsigned)(mode & 07777),
static int fsmOpenat(int dirfd, const char *path, int flags)
{
    struct stat lsb, sb;
    int sflags = flags | O_NOFOLLOW;
    int fd = openat(dirfd, path, sflags);

    /*
     * Only ever follow symlinks by root or target owner. Since we can't
     * open the symlink itself, the order matters: we stat the link *after*
     * opening the target, and if the link ownership changed between the calls
     * it could've only been the link owner or root.
     */
    if (fd < 0 && errno == ELOOP && flags != sflags) {
	int ffd = openat(dirfd, path, flags);
	if (ffd >= 0 && fstatat(dirfd, path, &lsb, AT_SYMLINK_NOFOLLOW) == 0) {
	    if (fstat(ffd, &sb) == 0) {
		if (lsb.st_uid == 0 || lsb.st_uid == sb.st_uid) {
		    fd = ffd;
		} else {
		    close(ffd);
		}
	    }
	}
    }
    return fd;
}

static int fsmDoMkDir(rpmPlugins plugins, int dirfd, const char *dn,
			int owned, mode_t mode)
{
    int rc;
    rpmFsmOp op = (FA_CREATE);
    if (!owned)
	op |= FAF_UNOWNED;

    /* Run fsm file pre hook for all plugins */
    rc = rpmpluginsCallFsmFilePre(plugins, NULL, dn, mode, op);

    if (!rc)
	rc = fsmMkdir(dirfd, dn, mode);

    if (!rc) {
	rc = rpmpluginsCallFsmFilePrepare(plugins, NULL, dn, dn, mode, op);
    }

    /* Run fsm file post hook for all plugins */
    rpmpluginsCallFsmFilePost(plugins, NULL, dn, mode, op, rc);

    if (!rc) {
	rpmlog(RPMLOG_DEBUG,
		"%s directory created with perms %04o\n",
		dn, (unsigned)(mode & 07777));
    }

    return rc;
}

static int ensureDir(rpmPlugins plugins, const char *p, int owned, int create)
{
    char *path = xstrdup(p);
    char *dp = path;
    char *sp = NULL, *bn;
    int oflags = O_RDONLY;

    int dirfd = fsmOpenat(-1, "/", oflags);
    int fd = dirfd; /* special case of "/" */

    while ((bn = strtok_r(dp, "/", &sp)) != NULL) {
	struct stat sb;
	fd = fsmOpenat(dirfd, bn, oflags);

	if (fd < 0 && errno == ENOENT && create) {
	    mode_t mode = S_IFDIR | (_dirPerms & 07777);
	    if (fsmDoMkDir(plugins, dirfd, bn, owned, mode) == 0) {
		fd = fsmOpenat(dirfd, bn, oflags|O_NOFOLLOW);
	    }
	}

	if (fd >= 0 && fstat(fd, &sb) == 0 && !S_ISDIR(sb.st_mode)) {
	    close(fd);
	    errno = ENOTDIR;
	    fd = -1;
	}

	close(dirfd);
	if (fd >= 0) {
	    dirfd = fd;
	} else {
	    dirfd = -1;
	    rpmlog(RPMLOG_ERR, _("failed to open dir %s of %s: %s\n"),
				bn, p, strerror(errno));
	    break;
	}

	dp = NULL;
    }

    free(path);
    return dirfd;
}

		    rc = fsmMkdir(-1, dn, mode);
struct diriter_s {
    int dirfd;
};

static int onChdir(rpmfi fi, void *data)
{
    struct diriter_s *di = data;

    if (di->dirfd >= 0) {
	close(di->dirfd);
	di->dirfd = -1;
    }
    return 0;
}

    struct diriter_s di = { -1 };
    rpmfiSetOnChdir(fi, onChdir, &di);

	    if (di.dirfd == -1) {
		di.dirfd = ensureDir(plugins, rpmfiDN(fi), 0,
				    (fp->action == FA_CREATE));
		if (di.dirfd == -1) {
		    rc = RPMERR_OPEN_FAILED;
		    break;
		}
	    }

                    rc = fsmMkdir(di.dirfd, fp->fpath, mode);
    close(di.dirfd);
    di.dirfd = -1;