]> git.ipfire.org Git - thirdparty/shadow.git/commitdiff
Avoid races in remove_tree()
authorChristian Göttsche <cgzones@googlemail.com>
Fri, 5 Aug 2022 15:57:22 +0000 (17:57 +0200)
committerSerge Hallyn <serge@hallyn.com>
Wed, 17 Aug 2022 17:34:01 +0000 (12:34 -0500)
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.

libmisc/remove_tree.c

index 04bc7fc4a0c796fef1b5c2db0dc93c71f756219e..3d76b95e010296167b96795be1afe2f798f0525a 100644 (file)
@@ -11,6 +11,7 @@
 
 #ident "$Id$"
 
+#include <fcntl.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #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 dirent *ent;
-       struct stat sb;
        DIR *dir;
+       const struct dirent *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);
+}