mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
[3.12] gh-91133: tempfile.TemporaryDirectory: fix symlink bug in cleanup (GH-99930) (GH-112838)
(cherry picked from commit 81c16cd94e
)
Co-authored-by: Søren Løvborg <sorenl@unity3d.com>
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
This commit is contained in:
parent
8f1c9128dd
commit
6ceb8aeda5
3 changed files with 125 additions and 15 deletions
|
@ -270,6 +270,22 @@ def _mkstemp_inner(dir, pre, suf, flags, output_type):
|
||||||
raise FileExistsError(_errno.EEXIST,
|
raise FileExistsError(_errno.EEXIST,
|
||||||
"No usable temporary file name found")
|
"No usable temporary file name found")
|
||||||
|
|
||||||
|
def _dont_follow_symlinks(func, path, *args):
|
||||||
|
# Pass follow_symlinks=False, unless not supported on this platform.
|
||||||
|
if func in _os.supports_follow_symlinks:
|
||||||
|
func(path, *args, follow_symlinks=False)
|
||||||
|
elif _os.name == 'nt' or not _os.path.islink(path):
|
||||||
|
func(path, *args)
|
||||||
|
|
||||||
|
def _resetperms(path):
|
||||||
|
try:
|
||||||
|
chflags = _os.chflags
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
_dont_follow_symlinks(chflags, path, 0)
|
||||||
|
_dont_follow_symlinks(_os.chmod, path, 0o700)
|
||||||
|
|
||||||
|
|
||||||
# User visible interfaces.
|
# User visible interfaces.
|
||||||
|
|
||||||
|
@ -876,17 +892,10 @@ class TemporaryDirectory:
|
||||||
def _rmtree(cls, name, ignore_errors=False):
|
def _rmtree(cls, name, ignore_errors=False):
|
||||||
def onexc(func, path, exc):
|
def onexc(func, path, exc):
|
||||||
if isinstance(exc, PermissionError):
|
if isinstance(exc, PermissionError):
|
||||||
def resetperms(path):
|
|
||||||
try:
|
|
||||||
_os.chflags(path, 0)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
_os.chmod(path, 0o700)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if path != name:
|
if path != name:
|
||||||
resetperms(_os.path.dirname(path))
|
_resetperms(_os.path.dirname(path))
|
||||||
resetperms(path)
|
_resetperms(path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_os.unlink(path)
|
_os.unlink(path)
|
||||||
|
|
|
@ -1673,6 +1673,103 @@ class TestTemporaryDirectory(BaseTestCase):
|
||||||
"were deleted")
|
"were deleted")
|
||||||
d2.cleanup()
|
d2.cleanup()
|
||||||
|
|
||||||
|
@os_helper.skip_unless_symlink
|
||||||
|
def test_cleanup_with_symlink_modes(self):
|
||||||
|
# cleanup() should not follow symlinks when fixing mode bits (#91133)
|
||||||
|
with self.do_create(recurse=0) as d2:
|
||||||
|
file1 = os.path.join(d2, 'file1')
|
||||||
|
open(file1, 'wb').close()
|
||||||
|
dir1 = os.path.join(d2, 'dir1')
|
||||||
|
os.mkdir(dir1)
|
||||||
|
for mode in range(8):
|
||||||
|
mode <<= 6
|
||||||
|
with self.subTest(mode=format(mode, '03o')):
|
||||||
|
def test(target, target_is_directory):
|
||||||
|
d1 = self.do_create(recurse=0)
|
||||||
|
symlink = os.path.join(d1.name, 'symlink')
|
||||||
|
os.symlink(target, symlink,
|
||||||
|
target_is_directory=target_is_directory)
|
||||||
|
try:
|
||||||
|
os.chmod(symlink, mode, follow_symlinks=False)
|
||||||
|
except NotImplementedError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
os.chmod(symlink, mode)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
os.chmod(d1.name, mode)
|
||||||
|
d1.cleanup()
|
||||||
|
self.assertFalse(os.path.exists(d1.name))
|
||||||
|
|
||||||
|
with self.subTest('nonexisting file'):
|
||||||
|
test('nonexisting', target_is_directory=False)
|
||||||
|
with self.subTest('nonexisting dir'):
|
||||||
|
test('nonexisting', target_is_directory=True)
|
||||||
|
|
||||||
|
with self.subTest('existing file'):
|
||||||
|
os.chmod(file1, mode)
|
||||||
|
old_mode = os.stat(file1).st_mode
|
||||||
|
test(file1, target_is_directory=False)
|
||||||
|
new_mode = os.stat(file1).st_mode
|
||||||
|
self.assertEqual(new_mode, old_mode,
|
||||||
|
'%03o != %03o' % (new_mode, old_mode))
|
||||||
|
|
||||||
|
with self.subTest('existing dir'):
|
||||||
|
os.chmod(dir1, mode)
|
||||||
|
old_mode = os.stat(dir1).st_mode
|
||||||
|
test(dir1, target_is_directory=True)
|
||||||
|
new_mode = os.stat(dir1).st_mode
|
||||||
|
self.assertEqual(new_mode, old_mode,
|
||||||
|
'%03o != %03o' % (new_mode, old_mode))
|
||||||
|
|
||||||
|
@unittest.skipUnless(hasattr(os, 'chflags'), 'requires os.chflags')
|
||||||
|
@os_helper.skip_unless_symlink
|
||||||
|
def test_cleanup_with_symlink_flags(self):
|
||||||
|
# cleanup() should not follow symlinks when fixing flags (#91133)
|
||||||
|
flags = stat.UF_IMMUTABLE | stat.UF_NOUNLINK
|
||||||
|
self.check_flags(flags)
|
||||||
|
|
||||||
|
with self.do_create(recurse=0) as d2:
|
||||||
|
file1 = os.path.join(d2, 'file1')
|
||||||
|
open(file1, 'wb').close()
|
||||||
|
dir1 = os.path.join(d2, 'dir1')
|
||||||
|
os.mkdir(dir1)
|
||||||
|
def test(target, target_is_directory):
|
||||||
|
d1 = self.do_create(recurse=0)
|
||||||
|
symlink = os.path.join(d1.name, 'symlink')
|
||||||
|
os.symlink(target, symlink,
|
||||||
|
target_is_directory=target_is_directory)
|
||||||
|
try:
|
||||||
|
os.chflags(symlink, flags, follow_symlinks=False)
|
||||||
|
except NotImplementedError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
os.chflags(symlink, flags)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
os.chflags(d1.name, flags)
|
||||||
|
d1.cleanup()
|
||||||
|
self.assertFalse(os.path.exists(d1.name))
|
||||||
|
|
||||||
|
with self.subTest('nonexisting file'):
|
||||||
|
test('nonexisting', target_is_directory=False)
|
||||||
|
with self.subTest('nonexisting dir'):
|
||||||
|
test('nonexisting', target_is_directory=True)
|
||||||
|
|
||||||
|
with self.subTest('existing file'):
|
||||||
|
os.chflags(file1, flags)
|
||||||
|
old_flags = os.stat(file1).st_flags
|
||||||
|
test(file1, target_is_directory=False)
|
||||||
|
new_flags = os.stat(file1).st_flags
|
||||||
|
self.assertEqual(new_flags, old_flags)
|
||||||
|
|
||||||
|
with self.subTest('existing dir'):
|
||||||
|
os.chflags(dir1, flags)
|
||||||
|
old_flags = os.stat(dir1).st_flags
|
||||||
|
test(dir1, target_is_directory=True)
|
||||||
|
new_flags = os.stat(dir1).st_flags
|
||||||
|
self.assertEqual(new_flags, old_flags)
|
||||||
|
|
||||||
@support.cpython_only
|
@support.cpython_only
|
||||||
def test_del_on_collection(self):
|
def test_del_on_collection(self):
|
||||||
# A TemporaryDirectory is deleted when garbage collected
|
# A TemporaryDirectory is deleted when garbage collected
|
||||||
|
@ -1845,10 +1942,7 @@ class TestTemporaryDirectory(BaseTestCase):
|
||||||
d.cleanup()
|
d.cleanup()
|
||||||
self.assertFalse(os.path.exists(d.name))
|
self.assertFalse(os.path.exists(d.name))
|
||||||
|
|
||||||
@unittest.skipUnless(hasattr(os, 'chflags'), 'requires os.chflags')
|
def check_flags(self, flags):
|
||||||
def test_flags(self):
|
|
||||||
flags = stat.UF_IMMUTABLE | stat.UF_NOUNLINK
|
|
||||||
|
|
||||||
# skip the test if these flags are not supported (ex: FreeBSD 13)
|
# skip the test if these flags are not supported (ex: FreeBSD 13)
|
||||||
filename = os_helper.TESTFN
|
filename = os_helper.TESTFN
|
||||||
try:
|
try:
|
||||||
|
@ -1857,13 +1951,18 @@ class TestTemporaryDirectory(BaseTestCase):
|
||||||
os.chflags(filename, flags)
|
os.chflags(filename, flags)
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
# "OSError: [Errno 45] Operation not supported"
|
# "OSError: [Errno 45] Operation not supported"
|
||||||
self.skipTest(f"chflags() doesn't support "
|
self.skipTest(f"chflags() doesn't support flags "
|
||||||
f"UF_IMMUTABLE|UF_NOUNLINK: {exc}")
|
f"{flags:#b}: {exc}")
|
||||||
else:
|
else:
|
||||||
os.chflags(filename, 0)
|
os.chflags(filename, 0)
|
||||||
finally:
|
finally:
|
||||||
os_helper.unlink(filename)
|
os_helper.unlink(filename)
|
||||||
|
|
||||||
|
@unittest.skipUnless(hasattr(os, 'chflags'), 'requires os.chflags')
|
||||||
|
def test_flags(self):
|
||||||
|
flags = stat.UF_IMMUTABLE | stat.UF_NOUNLINK
|
||||||
|
self.check_flags(flags)
|
||||||
|
|
||||||
d = self.do_create(recurse=3, dirs=2, files=2)
|
d = self.do_create(recurse=3, dirs=2, files=2)
|
||||||
with d:
|
with d:
|
||||||
# Change files and directories flags recursively.
|
# Change files and directories flags recursively.
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Fix a bug in :class:`tempfile.TemporaryDirectory` cleanup, which now no longer
|
||||||
|
dereferences symlinks when working around file system permission errors.
|
Loading…
Add table
Add a link
Reference in a new issue