Skip to content

Commit e9b51c0

Browse files
bpo-26660, bpo-35144: Fix permission errors in TemporaryDirectory cleanup. (GH-10320)
TemporaryDirectory.cleanup() failed when non-writeable or non-searchable files or directories were created inside a temporary directory.
1 parent 38ab7d4 commit e9b51c0

File tree

4 files changed

+86
-14
lines changed

4 files changed

+86
-14
lines changed

Lib/shutil.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -584,11 +584,16 @@ def _rmtree_safe_fd(topfd, path, onerror):
584584
fullname = os.path.join(path, entry.name)
585585
try:
586586
is_dir = entry.is_dir(follow_symlinks=False)
587-
if is_dir:
588-
orig_st = entry.stat(follow_symlinks=False)
589-
is_dir = stat.S_ISDIR(orig_st.st_mode)
590587
except OSError:
591588
is_dir = False
589+
else:
590+
if is_dir:
591+
try:
592+
orig_st = entry.stat(follow_symlinks=False)
593+
is_dir = stat.S_ISDIR(orig_st.st_mode)
594+
except OSError:
595+
onerror(os.lstat, fullname, sys.exc_info())
596+
continue
592597
if is_dir:
593598
try:
594599
dirfd = os.open(entry.name, os.O_RDONLY, dir_fd=topfd)

Lib/tempfile.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -777,9 +777,39 @@ def __init__(self, suffix=None, prefix=None, dir=None):
777777
self, self._cleanup, self.name,
778778
warn_message="Implicitly cleaning up {!r}".format(self))
779779

780+
@classmethod
781+
def _rmtree(cls, name):
782+
def onerror(func, path, exc_info):
783+
if issubclass(exc_info[0], PermissionError):
784+
def resetperms(path):
785+
try:
786+
_os.chflags(path, 0)
787+
except AttributeError:
788+
pass
789+
_os.chmod(path, 0o700)
790+
791+
try:
792+
if path != name:
793+
resetperms(_os.path.dirname(path))
794+
resetperms(path)
795+
796+
try:
797+
_os.unlink(path)
798+
# PermissionError is raised on FreeBSD for directories
799+
except (IsADirectoryError, PermissionError):
800+
cls._rmtree(path)
801+
except FileNotFoundError:
802+
pass
803+
elif issubclass(exc_info[0], FileNotFoundError):
804+
pass
805+
else:
806+
raise
807+
808+
_shutil.rmtree(name, onerror=onerror)
809+
780810
@classmethod
781811
def _cleanup(cls, name, warn_message):
782-
_shutil.rmtree(name)
812+
cls._rmtree(name)
783813
_warnings.warn(warn_message, ResourceWarning)
784814

785815
def __repr__(self):
@@ -793,4 +823,4 @@ def __exit__(self, exc, value, tb):
793823

794824
def cleanup(self):
795825
if self._finalizer.detach():
796-
_shutil.rmtree(self.name)
826+
self._rmtree(self.name)

Lib/test/test_tempfile.py

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1297,19 +1297,25 @@ def __exit__(self, *exc_info):
12971297
class TestTemporaryDirectory(BaseTestCase):
12981298
"""Test TemporaryDirectory()."""
12991299

1300-
def do_create(self, dir=None, pre="", suf="", recurse=1):
1300+
def do_create(self, dir=None, pre="", suf="", recurse=1, dirs=1, files=1):
13011301
if dir is None:
13021302
dir = tempfile.gettempdir()
13031303
tmp = tempfile.TemporaryDirectory(dir=dir, prefix=pre, suffix=suf)
13041304
self.nameCheck(tmp.name, dir, pre, suf)
1305-
# Create a subdirectory and some files
1306-
if recurse:
1307-
d1 = self.do_create(tmp.name, pre, suf, recurse-1)
1308-
d1.name = None
1309-
with open(os.path.join(tmp.name, "test.txt"), "wb") as f:
1310-
f.write(b"Hello world!")
1305+
self.do_create2(tmp.name, recurse, dirs, files)
13111306
return tmp
13121307

1308+
def do_create2(self, path, recurse=1, dirs=1, files=1):
1309+
# Create subdirectories and some files
1310+
if recurse:
1311+
for i in range(dirs):
1312+
name = os.path.join(path, "dir%d" % i)
1313+
os.mkdir(name)
1314+
self.do_create2(name, recurse-1, dirs, files)
1315+
for i in range(files):
1316+
with open(os.path.join(path, "test%d.txt" % i), "wb") as f:
1317+
f.write(b"Hello world!")
1318+
13131319
def test_mkdtemp_failure(self):
13141320
# Check no additional exception if mkdtemp fails
13151321
# Previously would raise AttributeError instead
@@ -1349,7 +1355,7 @@ def test_cleanup_with_symlink_to_a_directory(self):
13491355
"TemporaryDirectory %s exists after cleanup" % d1.name)
13501356
self.assertTrue(os.path.exists(d2.name),
13511357
"Directory pointed to by a symlink was deleted")
1352-
self.assertEqual(os.listdir(d2.name), ['test.txt'],
1358+
self.assertEqual(os.listdir(d2.name), ['test0.txt'],
13531359
"Contents of the directory pointed to by a symlink "
13541360
"were deleted")
13551361
d2.cleanup()
@@ -1384,7 +1390,7 @@ def test_del_on_shutdown(self):
13841390
13851391
tmp2 = os.path.join(tmp.name, 'test_dir')
13861392
os.mkdir(tmp2)
1387-
with open(os.path.join(tmp2, "test.txt"), "w") as f:
1393+
with open(os.path.join(tmp2, "test0.txt"), "w") as f:
13881394
f.write("Hello world!")
13891395
13901396
{mod}.tmp = tmp
@@ -1452,6 +1458,33 @@ def test_context_manager(self):
14521458
self.assertEqual(name, d.name)
14531459
self.assertFalse(os.path.exists(name))
14541460

1461+
def test_modes(self):
1462+
for mode in range(8):
1463+
mode <<= 6
1464+
with self.subTest(mode=format(mode, '03o')):
1465+
d = self.do_create(recurse=3, dirs=2, files=2)
1466+
with d:
1467+
# Change files and directories mode recursively.
1468+
for root, dirs, files in os.walk(d.name, topdown=False):
1469+
for name in files:
1470+
os.chmod(os.path.join(root, name), mode)
1471+
os.chmod(root, mode)
1472+
d.cleanup()
1473+
self.assertFalse(os.path.exists(d.name))
1474+
1475+
@unittest.skipUnless(hasattr(os, 'chflags'), 'requires os.lchflags')
1476+
def test_flags(self):
1477+
flags = stat.UF_IMMUTABLE | stat.UF_NOUNLINK
1478+
d = self.do_create(recurse=3, dirs=2, files=2)
1479+
with d:
1480+
# Change files and directories flags recursively.
1481+
for root, dirs, files in os.walk(d.name, topdown=False):
1482+
for name in files:
1483+
os.chflags(os.path.join(root, name), flags)
1484+
os.chflags(root, flags)
1485+
d.cleanup()
1486+
self.assertFalse(os.path.exists(d.name))
1487+
14551488

14561489
if __name__ == "__main__":
14571490
unittest.main()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fixed permission errors in :class:`~tempfile.TemporaryDirectory` clean up.
2+
Previously ``TemporaryDirectory.cleanup()`` failed when non-writeable or
3+
non-searchable files or directories were created inside a temporary
4+
directory.

0 commit comments

Comments
 (0)