From 95cbaeba08e168cbc020ceb9732ad8e91418fcb7 Mon Sep 17 00:00:00 2001 From: fatelei Date: Thu, 25 Dec 2025 13:51:29 +0800 Subject: [PATCH] gh-142916: fix mkdir TOCTOU race condition --- Lib/pathlib/__init__.py | 12 ++++- Lib/test/test_pathlib/test_pathlib.py | 53 +++++++++++++++++++ ...-12-25-13-51-06.gh-issue-142916.gm4EzD.rst | 1 + 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2025-12-25-13-51-06.gh-issue-142916.gm4EzD.rst diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index 44f967eb12dd4f..0cff848b48800f 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -1215,9 +1215,19 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False): except OSError: # Cannot rely on checking for EEXIST, since the operating system # could give priority to other errors like EACCES or EROFS - if not exist_ok or not self.is_dir(): + if not exist_ok: raise + if not self.is_dir(): + if not self.exists(): + try: + os.mkdir(self, mode) + except FileExistsError: + if not self.is_dir(): + raise + else: + raise + def chmod(self, mode, *, follow_symlinks=True): """ Change the permissions of the path, like os.chmod(). diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index ef9ea0d11d06a6..41d7e19a4ff27e 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -2495,6 +2495,59 @@ def my_mkdir(path, mode=0o777): self.assertNotIn(str(p12), concurrently_created) self.assertTrue(p.exists()) + def test_mkdir_exist_ok_concurrent_delete(self): + # Test for TOCTOU race condition with real threading. + # One thread repeatedly creates a directory with exist_ok=True, + # another thread repeatedly deletes it. This should never raise + # FileExistsError once the directory has been deleted. + import threading + import time + + p = self.cls(self.base, 'dirTOCTOU') + self.assertFalse(p.exists()) + + p.mkdir() + self.assertTrue(p.exists()) + + errors = [] + stop_flag = [False] + + def mkdir_thread(): + while not stop_flag[0]: + try: + p.mkdir(exist_ok=True) + except FileExistsError as e: + errors.append(e) + stop_flag[0] = True + break + + def rmdir_thread(): + while not stop_flag[0]: + try: + if p.exists(): + p.rmdir() + except OSError: + pass + + t1 = threading.Thread(target=mkdir_thread) + t2 = threading.Thread(target=rmdir_thread) + + try: + t1.start() + t2.start() + + time.sleep(0.5) + + stop_flag[0] = True + t1.join(timeout=2.0) + t2.join(timeout=2.0) + + self.assertEqual(errors, []) + finally: + stop_flag[0] = True + if p.exists(): + p.rmdir() + @needs_symlinks def test_symlink_to(self): P = self.cls(self.base) diff --git a/Misc/NEWS.d/next/Library/2025-12-25-13-51-06.gh-issue-142916.gm4EzD.rst b/Misc/NEWS.d/next/Library/2025-12-25-13-51-06.gh-issue-142916.gm4EzD.rst new file mode 100644 index 00000000000000..a936f3bf230def --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-25-13-51-06.gh-issue-142916.gm4EzD.rst @@ -0,0 +1 @@ +fix mkdir TOCTOU race condition