From 5d2794a16bc1639e6053300c08a78d60526aadf2 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 11 Feb 2024 12:38:07 +0200 Subject: [PATCH] gh-67837, gh-112998: Fix dirs creation in concurrent extraction (GH-115082) Avoid race conditions in the creation of directories during concurrent extraction in tarfile and zipfile. Co-authored-by: Samantha Hughes Co-authored-by: Peder Bergebakken Sundt --- Lib/tarfile.py | 2 +- Lib/test/archiver_tests.py | 22 +++++++++++++++++++ Lib/zipfile/__init__.py | 8 +++++-- ...4-02-06-15-16-28.gh-issue-67837._JKa73.rst | 2 ++ 4 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-02-06-15-16-28.gh-issue-67837._JKa73.rst diff --git a/Lib/tarfile.py b/Lib/tarfile.py index 9775040cbe3..f4dd0fdab4a 100755 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -2411,7 +2411,7 @@ def _extract_member(self, tarinfo, targetpath, set_attrs=True, if upperdirs and not os.path.exists(upperdirs): # Create directories that are not part of the archive with # default permissions. - os.makedirs(upperdirs) + os.makedirs(upperdirs, exist_ok=True) if tarinfo.islnk() or tarinfo.issym(): self._dbg(1, "%s -> %s" % (tarinfo.name, tarinfo.linkname)) diff --git a/Lib/test/archiver_tests.py b/Lib/test/archiver_tests.py index 1a4bbb9e570..24745941b08 100644 --- a/Lib/test/archiver_tests.py +++ b/Lib/test/archiver_tests.py @@ -3,6 +3,7 @@ import os import sys +from test.support import swap_attr from test.support import os_helper class OverwriteTests: @@ -153,3 +154,24 @@ def test_overwrite_broken_dir_symlink_as_implicit_dir(self): self.extractall(ar) self.assertTrue(os.path.islink(target)) self.assertFalse(os.path.exists(target2)) + + def test_concurrent_extract_dir(self): + target = os.path.join(self.testdir, 'test') + def concurrent_mkdir(*args, **kwargs): + orig_mkdir(*args, **kwargs) + orig_mkdir(*args, **kwargs) + with swap_attr(os, 'mkdir', concurrent_mkdir) as orig_mkdir: + with self.open(self.ar_with_dir) as ar: + self.extractall(ar) + self.assertTrue(os.path.isdir(target)) + + def test_concurrent_extract_implicit_dir(self): + target = os.path.join(self.testdir, 'test') + def concurrent_mkdir(*args, **kwargs): + orig_mkdir(*args, **kwargs) + orig_mkdir(*args, **kwargs) + with swap_attr(os, 'mkdir', concurrent_mkdir) as orig_mkdir: + with self.open(self.ar_with_implicit_dir) as ar: + self.extractall(ar) + self.assertTrue(os.path.isdir(target)) + self.assertTrue(os.path.isfile(os.path.join(target, 'file'))) diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index 8005b4b34cc..cc08f602fe4 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -1802,11 +1802,15 @@ def _extract_member(self, member, targetpath, pwd): # Create all upper directories if necessary. upperdirs = os.path.dirname(targetpath) if upperdirs and not os.path.exists(upperdirs): - os.makedirs(upperdirs) + os.makedirs(upperdirs, exist_ok=True) if member.is_dir(): if not os.path.isdir(targetpath): - os.mkdir(targetpath) + try: + os.mkdir(targetpath) + except FileExistsError: + if not os.path.isdir(targetpath): + raise return targetpath with self.open(member, pwd=pwd) as source, \ diff --git a/Misc/NEWS.d/next/Library/2024-02-06-15-16-28.gh-issue-67837._JKa73.rst b/Misc/NEWS.d/next/Library/2024-02-06-15-16-28.gh-issue-67837._JKa73.rst new file mode 100644 index 00000000000..340b65f1883 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-02-06-15-16-28.gh-issue-67837._JKa73.rst @@ -0,0 +1,2 @@ +Avoid race conditions in the creation of directories during concurrent +extraction in :mod:`tarfile` and :mod:`zipfile`.