Index: Lib/shutil.py =================================================================== --- Lib/shutil.py (revision 87759) +++ Lib/shutil.py (working copy) @@ -238,6 +238,12 @@ if errors: raise Error(errors) +def close(fd): + try: + os.close(fd) + except: + pass + def rmtree(path, ignore_errors=False, onerror=None): """Recursively delete a directory tree. @@ -255,7 +261,61 @@ elif onerror is None: def onerror(*args): raise + + if hasattr(os, 'openat') and hasattr(os, 'fdlistdir') and hasattr(os, + 'unlinkat') and hasattr(os, 'fstatat'): + try: + if os.path.islink(path): + # symlinks to directories are forbidden, see bug #1669 + raise OSError("Cannot call rmtree on a symbolic link") + except OSError: + onerror(os.path.islink, path, sys.exc_info()) + # can't continue even if onerror hook returns + return + fd = os.open(path, os.O_RDONLY) + _rmtree_safe(fd, ignore_errors, onerror) + try: + os.rmdir(path) + except os.error: + onerror(os.rmdir, path, sys.exc_info()) + else: + _rmtree_unsafe(path, ignore_errors, onerror) + +def _rmtree_safe(dirfd, ignore_errors=False, onerror=None): + names = [] try: + fd = os.dup(dirfd) + names = os.fdlistdir(fd) + except os.error as err: + onerror(os.fdlistdir, fd, sys.exc_info()) + close(fd) + for name in names: + try: + mode = os.fstatat(dirfd, name, os.AT_SYMLINK_NOFOLLOW).st_mode + except os.error: + mode = 0 + if stat.S_ISDIR(mode): + if stat.S_ISLNK(mode): + try: + raise OSError("Cannot call rmtree on a symbolic link") + except OSError: + onerror(os.fstatat, (dirfd, name), sys.exc_info()) + else: + newfd = os.openat(dirfd, name, os.O_RDONLY) + _rmtree_safe(newfd, ignore_errors, onerror) + try: + os.unlinkat(dirfd, name, os.AT_REMOVEDIR) + except os.error as err: + onerror(os.unlinkat, (dirfd, name), sys.exc_info()) + else: + try: + os.unlinkat(dirfd, name) + except os.error as err: + onerror(os.unlinkat, (dirfd, name), sys.exc_info()) + close(dirfd) + +def _rmtree_unsafe(path, ignore_errors=False, onerror=None): + try: if os.path.islink(path): # symlinks to directories are forbidden, see bug #1669 raise OSError("Cannot call rmtree on a symbolic link") @@ -275,7 +335,7 @@ except os.error: mode = 0 if stat.S_ISDIR(mode): - rmtree(fullname, ignore_errors, onerror) + _rmtree_unsafe(fullname, ignore_errors, onerror) else: try: os.remove(fullname) Index: Lib/test/test_shutil.py =================================================================== --- Lib/test/test_shutil.py (revision 87759) +++ Lib/test/test_shutil.py (working copy) @@ -130,6 +130,8 @@ if self.errorState == 0: if func is os.remove: self.assertEqual(arg, self.childpath) + elif func is os.unlinkat: + self.assertEqual(arg, (3, 'a')) else: self.assertIs(func, os.listdir, "func must be either os.remove or os.listdir") @@ -324,6 +326,41 @@ finally: shutil.rmtree(TESTFN, ignore_errors=True) + + def test_rmtree_4489(self): + # Issue #4489: test a symlink attack on rmtree + + import threading, time + def hack(a, b): + time.sleep(0.1) + os.rename(b, b + 'x') + self.tempdirs.append(b + 'x') + os.symlink(a, b) + + a = self.mkdtemp() + b = self.mkdtemp() + + for i in range(25000): + open(os.path.join(a, str(i)), "w").close() + open(os.path.join(b, str(i)), "w").close() + + l = sorted(os.listdir(a)) + + hackt = threading.Thread(target=hack, args=(a, b)) + hackt.start() + try: + shutil.rmtree(b) + except OSError as inst: + if inst.errno != 20: + raise + finally: + support.unlink(b) + self.tempdirs.remove(b) + if hasattr(os, 'openat') and hasattr(os, 'fdlistdir') and hasattr(os, + 'unlinkat') and hasattr(os, 'fstatat'): + self.assertEqual(l, sorted(os.listdir(a))) + + if hasattr(os, "mkfifo"): # Issue #3002: copyfile and copytree block indefinitely on named pipes def test_copyfile_named_pipe(self):