From f6f8bcd2a57c06983296485cc028ebdf467ebfd7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Christian=20G=C3=B6ttsche?= <cgzones@googlemail.com>
Date: Fri, 5 Aug 2022 17:57:22 +0200
Subject: [PATCH] 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.

Conflict: NA
Reference: https://github.com/shadow-maint/shadow/commit/f6f8bcd2a57c06983296485cc028ebdf467ebfd7

---
 libmisc/remove_tree.c | 87 +++++++++++++++++++------------------------
 1 file changed, 39 insertions(+), 48 deletions(-)

diff --git a/libmisc/remove_tree.c b/libmisc/remove_tree.c
index 04bc7fc4a..3d76b95e0 100644
--- a/libmisc/remove_tree.c
+++ b/libmisc/remove_tree.c
@@ -11,6 +11,7 @@
 
 #ident "$Id$"
 
+#include <fcntl.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -21,90 +22,80 @@
 #include "prototypes.h"
 #include "defines.h"
 
-/*
- * remove_tree - delete a directory tree
- *
- *	remove_tree() walks a directory tree and deletes all the files
- *	and directories.
- *	At the end, it deletes the root directory itself.
- */
-
-int remove_tree (const char *root, bool remove_root)
+static int remove_tree_at (int at_fd, const char *path, bool remove_root)
 {
-	char *new_name = NULL;
-	int err = 0;
-	struct DIRECT *ent;
-	struct stat sb;
 	DIR *dir;
+	const struct DIRECT *ent;
+	int dir_fd, rc = 0;
 
-	/*
-	 * 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) {
+	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))) {
-		size_t new_len = strlen (root) + strlen (ent->d_name) + 2;
+		struct stat ent_sb;
 
 		/*
 		 * 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;
+		rc = fstatat (dirfd(dir), ent->d_name, &ent_sb, AT_SYMLINK_NOFOLLOW);
+		if (rc < 0) {
 			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)) {
+		if (S_ISDIR (ent_sb.st_mode)) {
 			/*
 			 * Recursively delete this directory.
 			 */
-			if (remove_tree (new_name, true) != 0) {
-				err = -1;
+			if (remove_tree_at (dirfd(dir), ent->d_name, true) != 0) {
+				rc = -1;
 				break;
 			}
 		} else {
 			/*
 			 * Delete the file.
 			 */
-			if (unlink (new_name) != 0) {
-				err = -1;
+			if (unlinkat (dirfd(dir), ent->d_name, 0) != 0) {
+				rc = -1;
 				break;
 			}
 		}
 	}
-	if (NULL != new_name) {
-		free (new_name);
-	}
+
 	(void) closedir (dir);
 
-	if (remove_root && (0 == err)) {
-		if (rmdir (root) != 0) {
-			err = -1;
+	if (remove_root && (0 == rc)) {
+		if (unlinkat (at_fd, path, AT_REMOVEDIR) != 0) {
+			rc = -1;
 		}
 	}
 
-	return err;
+	return rc;
 }
 
+/*
+ * remove_tree - delete a directory tree
+ *
+ *	remove_tree() walks a directory tree and deletes all the files
+ *	and directories.
+ *	At the end, it deletes the root directory itself.
+ */
+int remove_tree (const char *root, bool remove_root)
+{
+	return remove_tree_at (AT_FDCWD, root, remove_root);
+}
