Skip to content

Commit 68112c3

Browse files
gh-83499: Fix closing file descriptors in tempfile (GH-93874)
(cherry picked from commit d4792ce) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent 4ec1f8d commit 68112c3

File tree

3 files changed

+106
-64
lines changed

3 files changed

+106
-64
lines changed

Lib/tempfile.py

+51-28
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,7 @@ def _get_default_tempdir():
203203
fd = _os.open(filename, _bin_openflags, 0o600)
204204
try:
205205
try:
206-
with _io.open(fd, 'wb', closefd=False) as fp:
207-
fp.write(b'blat')
206+
_os.write(fd, b'blat')
208207
finally:
209208
_os.close(fd)
210209
finally:
@@ -244,6 +243,7 @@ def _get_candidate_names():
244243
def _mkstemp_inner(dir, pre, suf, flags, output_type):
245244
"""Code common to mkstemp, TemporaryFile, and NamedTemporaryFile."""
246245

246+
dir = _os.path.abspath(dir)
247247
names = _get_candidate_names()
248248
if output_type is bytes:
249249
names = map(_os.fsencode, names)
@@ -264,7 +264,7 @@ def _mkstemp_inner(dir, pre, suf, flags, output_type):
264264
continue
265265
else:
266266
raise
267-
return (fd, _os.path.abspath(file))
267+
return fd, file
268268

269269
raise FileExistsError(_errno.EEXIST,
270270
"No usable temporary file name found")
@@ -554,15 +554,26 @@ def NamedTemporaryFile(mode='w+b', buffering=-1, encoding=None,
554554
if "b" not in mode:
555555
encoding = _io.text_encoding(encoding)
556556

557-
(fd, name) = _mkstemp_inner(dir, prefix, suffix, flags, output_type)
557+
name = None
558+
def opener(*args):
559+
nonlocal name
560+
fd, name = _mkstemp_inner(dir, prefix, suffix, flags, output_type)
561+
return fd
558562
try:
559-
file = _io.open(fd, mode, buffering=buffering,
560-
newline=newline, encoding=encoding, errors=errors)
561-
562-
return _TemporaryFileWrapper(file, name, delete)
563-
except BaseException:
564-
_os.unlink(name)
565-
_os.close(fd)
563+
file = _io.open(dir, mode, buffering=buffering,
564+
newline=newline, encoding=encoding, errors=errors,
565+
opener=opener)
566+
try:
567+
raw = getattr(file, 'buffer', file)
568+
raw = getattr(raw, 'raw', raw)
569+
raw.name = name
570+
return _TemporaryFileWrapper(file, name, delete)
571+
except:
572+
file.close()
573+
raise
574+
except:
575+
if name is not None and not (_os.name == 'nt' and delete):
576+
_os.unlink(name)
566577
raise
567578

568579
if _os.name != 'posix' or _sys.platform == 'cygwin':
@@ -601,9 +612,20 @@ def TemporaryFile(mode='w+b', buffering=-1, encoding=None,
601612

602613
flags = _bin_openflags
603614
if _O_TMPFILE_WORKS:
604-
try:
615+
fd = None
616+
def opener(*args):
617+
nonlocal fd
605618
flags2 = (flags | _os.O_TMPFILE) & ~_os.O_CREAT
606619
fd = _os.open(dir, flags2, 0o600)
620+
return fd
621+
try:
622+
file = _io.open(dir, mode, buffering=buffering,
623+
newline=newline, encoding=encoding,
624+
errors=errors, opener=opener)
625+
raw = getattr(file, 'buffer', file)
626+
raw = getattr(raw, 'raw', raw)
627+
raw.name = fd
628+
return file
607629
except IsADirectoryError:
608630
# Linux kernel older than 3.11 ignores the O_TMPFILE flag:
609631
# O_TMPFILE is read as O_DIRECTORY. Trying to open a directory
@@ -620,24 +642,25 @@ def TemporaryFile(mode='w+b', buffering=-1, encoding=None,
620642
# fails with NotADirectoryError, because O_TMPFILE is read as
621643
# O_DIRECTORY.
622644
pass
623-
else:
624-
try:
625-
return _io.open(fd, mode, buffering=buffering,
626-
newline=newline, encoding=encoding,
627-
errors=errors)
628-
except:
629-
_os.close(fd)
630-
raise
631645
# Fallback to _mkstemp_inner().
632646

633-
(fd, name) = _mkstemp_inner(dir, prefix, suffix, flags, output_type)
634-
try:
635-
_os.unlink(name)
636-
return _io.open(fd, mode, buffering=buffering,
637-
newline=newline, encoding=encoding, errors=errors)
638-
except:
639-
_os.close(fd)
640-
raise
647+
fd = None
648+
def opener(*args):
649+
nonlocal fd
650+
fd, name = _mkstemp_inner(dir, prefix, suffix, flags, output_type)
651+
try:
652+
_os.unlink(name)
653+
except BaseException as e:
654+
_os.close(fd)
655+
raise
656+
return fd
657+
file = _io.open(dir, mode, buffering=buffering,
658+
newline=newline, encoding=encoding, errors=errors,
659+
opener=opener)
660+
raw = getattr(file, 'buffer', file)
661+
raw = getattr(raw, 'raw', raw)
662+
raw.name = fd
663+
return file
641664

642665
class SpooledTemporaryFile(_io.IOBase):
643666
"""Temporary file wrapper, specialized to switch from BytesIO

Lib/test/test_tempfile.py

+54-36
Original file line numberDiff line numberDiff line change
@@ -285,19 +285,14 @@ def our_candidate_list():
285285
def raise_OSError(*args, **kwargs):
286286
raise OSError()
287287

288-
with support.swap_attr(io, "open", raise_OSError):
289-
# test again with failing io.open()
288+
with support.swap_attr(os, "open", raise_OSError):
289+
# test again with failing os.open()
290290
with self.assertRaises(FileNotFoundError):
291291
tempfile._get_default_tempdir()
292292
self.assertEqual(os.listdir(our_temp_directory), [])
293293

294-
def bad_writer(*args, **kwargs):
295-
fp = orig_open(*args, **kwargs)
296-
fp.write = raise_OSError
297-
return fp
298-
299-
with support.swap_attr(io, "open", bad_writer) as orig_open:
300-
# test again with failing write()
294+
with support.swap_attr(os, "write", raise_OSError):
295+
# test again with failing os.write()
301296
with self.assertRaises(FileNotFoundError):
302297
tempfile._get_default_tempdir()
303298
self.assertEqual(os.listdir(our_temp_directory), [])
@@ -978,6 +973,7 @@ def test_del_on_close(self):
978973
try:
979974
with tempfile.NamedTemporaryFile(dir=dir) as f:
980975
f.write(b'blat')
976+
self.assertEqual(os.listdir(dir), [])
981977
self.assertFalse(os.path.exists(f.name),
982978
"NamedTemporaryFile %s exists after close" % f.name)
983979
finally:
@@ -1017,19 +1013,6 @@ def use_closed():
10171013
pass
10181014
self.assertRaises(ValueError, use_closed)
10191015

1020-
def test_no_leak_fd(self):
1021-
# Issue #21058: don't leak file descriptor when io.open() fails
1022-
closed = []
1023-
os_close = os.close
1024-
def close(fd):
1025-
closed.append(fd)
1026-
os_close(fd)
1027-
1028-
with mock.patch('os.close', side_effect=close):
1029-
with mock.patch('io.open', side_effect=ValueError):
1030-
self.assertRaises(ValueError, tempfile.NamedTemporaryFile)
1031-
self.assertEqual(len(closed), 1)
1032-
10331016
def test_bad_mode(self):
10341017
dir = tempfile.mkdtemp()
10351018
self.addCleanup(os_helper.rmtree, dir)
@@ -1039,6 +1022,24 @@ def test_bad_mode(self):
10391022
tempfile.NamedTemporaryFile(mode=2, dir=dir)
10401023
self.assertEqual(os.listdir(dir), [])
10411024

1025+
def test_bad_encoding(self):
1026+
dir = tempfile.mkdtemp()
1027+
self.addCleanup(os_helper.rmtree, dir)
1028+
with self.assertRaises(LookupError):
1029+
tempfile.NamedTemporaryFile('w', encoding='bad-encoding', dir=dir)
1030+
self.assertEqual(os.listdir(dir), [])
1031+
1032+
def test_unexpected_error(self):
1033+
dir = tempfile.mkdtemp()
1034+
self.addCleanup(os_helper.rmtree, dir)
1035+
with mock.patch('tempfile._TemporaryFileWrapper') as mock_ntf, \
1036+
mock.patch('io.open', mock.mock_open()) as mock_open:
1037+
mock_ntf.side_effect = KeyboardInterrupt()
1038+
with self.assertRaises(KeyboardInterrupt):
1039+
tempfile.NamedTemporaryFile(dir=dir)
1040+
mock_open().close.assert_called()
1041+
self.assertEqual(os.listdir(dir), [])
1042+
10421043
# How to test the mode and bufsize parameters?
10431044

10441045
class TestSpooledTemporaryFile(BaseTestCase):
@@ -1093,8 +1094,10 @@ def test_del_on_close(self):
10931094
self.assertTrue(f._rolled)
10941095
filename = f.name
10951096
f.close()
1096-
self.assertFalse(isinstance(filename, str) and os.path.exists(filename),
1097-
"SpooledTemporaryFile %s exists after close" % filename)
1097+
self.assertEqual(os.listdir(dir), [])
1098+
if not isinstance(filename, int):
1099+
self.assertFalse(os.path.exists(filename),
1100+
"SpooledTemporaryFile %s exists after close" % filename)
10981101
finally:
10991102
os.rmdir(dir)
11001103

@@ -1411,19 +1414,34 @@ def roundtrip(input, *args, **kwargs):
14111414
roundtrip("\u039B", "w+", encoding="utf-16")
14121415
roundtrip("foo\r\n", "w+", newline="")
14131416

1414-
def test_no_leak_fd(self):
1415-
# Issue #21058: don't leak file descriptor when io.open() fails
1416-
closed = []
1417-
os_close = os.close
1418-
def close(fd):
1419-
closed.append(fd)
1420-
os_close(fd)
1421-
1422-
with mock.patch('os.close', side_effect=close):
1423-
with mock.patch('io.open', side_effect=ValueError):
1424-
self.assertRaises(ValueError, tempfile.TemporaryFile)
1425-
self.assertEqual(len(closed), 1)
1417+
def test_bad_mode(self):
1418+
dir = tempfile.mkdtemp()
1419+
self.addCleanup(os_helper.rmtree, dir)
1420+
with self.assertRaises(ValueError):
1421+
tempfile.TemporaryFile(mode='wr', dir=dir)
1422+
with self.assertRaises(TypeError):
1423+
tempfile.TemporaryFile(mode=2, dir=dir)
1424+
self.assertEqual(os.listdir(dir), [])
1425+
1426+
def test_bad_encoding(self):
1427+
dir = tempfile.mkdtemp()
1428+
self.addCleanup(os_helper.rmtree, dir)
1429+
with self.assertRaises(LookupError):
1430+
tempfile.TemporaryFile('w', encoding='bad-encoding', dir=dir)
1431+
self.assertEqual(os.listdir(dir), [])
14261432

1433+
def test_unexpected_error(self):
1434+
dir = tempfile.mkdtemp()
1435+
self.addCleanup(os_helper.rmtree, dir)
1436+
with mock.patch('tempfile._O_TMPFILE_WORKS', False), \
1437+
mock.patch('os.unlink') as mock_unlink, \
1438+
mock.patch('os.open') as mock_open, \
1439+
mock.patch('os.close') as mock_close:
1440+
mock_unlink.side_effect = KeyboardInterrupt()
1441+
with self.assertRaises(KeyboardInterrupt):
1442+
tempfile.TemporaryFile(dir=dir)
1443+
mock_close.assert_called()
1444+
self.assertEqual(os.listdir(dir), [])
14271445

14281446

14291447
# Helper for test_del_on_shutdown
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix double closing of file description in :mod:`tempfile`.

0 commit comments

Comments
 (0)