Skip to content

Commit 95cbaeb

Browse files
committed
gh-142916: fix mkdir TOCTOU race condition
1 parent cf6758f commit 95cbaeb

File tree

3 files changed

+65
-1
lines changed

3 files changed

+65
-1
lines changed

Lib/pathlib/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1215,9 +1215,19 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False):
12151215
except OSError:
12161216
# Cannot rely on checking for EEXIST, since the operating system
12171217
# could give priority to other errors like EACCES or EROFS
1218-
if not exist_ok or not self.is_dir():
1218+
if not exist_ok:
12191219
raise
12201220

1221+
if not self.is_dir():
1222+
if not self.exists():
1223+
try:
1224+
os.mkdir(self, mode)
1225+
except FileExistsError:
1226+
if not self.is_dir():
1227+
raise
1228+
else:
1229+
raise
1230+
12211231
def chmod(self, mode, *, follow_symlinks=True):
12221232
"""
12231233
Change the permissions of the path, like os.chmod().

Lib/test/test_pathlib/test_pathlib.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2495,6 +2495,59 @@ def my_mkdir(path, mode=0o777):
24952495
self.assertNotIn(str(p12), concurrently_created)
24962496
self.assertTrue(p.exists())
24972497

2498+
def test_mkdir_exist_ok_concurrent_delete(self):
2499+
# Test for TOCTOU race condition with real threading.
2500+
# One thread repeatedly creates a directory with exist_ok=True,
2501+
# another thread repeatedly deletes it. This should never raise
2502+
# FileExistsError once the directory has been deleted.
2503+
import threading
2504+
import time
2505+
2506+
p = self.cls(self.base, 'dirTOCTOU')
2507+
self.assertFalse(p.exists())
2508+
2509+
p.mkdir()
2510+
self.assertTrue(p.exists())
2511+
2512+
errors = []
2513+
stop_flag = [False]
2514+
2515+
def mkdir_thread():
2516+
while not stop_flag[0]:
2517+
try:
2518+
p.mkdir(exist_ok=True)
2519+
except FileExistsError as e:
2520+
errors.append(e)
2521+
stop_flag[0] = True
2522+
break
2523+
2524+
def rmdir_thread():
2525+
while not stop_flag[0]:
2526+
try:
2527+
if p.exists():
2528+
p.rmdir()
2529+
except OSError:
2530+
pass
2531+
2532+
t1 = threading.Thread(target=mkdir_thread)
2533+
t2 = threading.Thread(target=rmdir_thread)
2534+
2535+
try:
2536+
t1.start()
2537+
t2.start()
2538+
2539+
time.sleep(0.5)
2540+
2541+
stop_flag[0] = True
2542+
t1.join(timeout=2.0)
2543+
t2.join(timeout=2.0)
2544+
2545+
self.assertEqual(errors, [])
2546+
finally:
2547+
stop_flag[0] = True
2548+
if p.exists():
2549+
p.rmdir()
2550+
24982551
@needs_symlinks
24992552
def test_symlink_to(self):
25002553
P = self.cls(self.base)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fix mkdir TOCTOU race condition

0 commit comments

Comments
 (0)