Skip to content

Commit 82f9041

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 1901d2a commit 82f9041

File tree

3 files changed

+106
-64
lines changed

3 files changed

+106
-64
lines changed

Diff for: 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")
@@ -550,15 +550,26 @@ def NamedTemporaryFile(mode='w+b', buffering=-1, encoding=None,
550550
if "b" not in mode:
551551
encoding = _io.text_encoding(encoding)
552552

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

564575
if _os.name != 'posix' or _sys.platform == 'cygwin':
@@ -597,9 +608,20 @@ def TemporaryFile(mode='w+b', buffering=-1, encoding=None,
597608

598609
flags = _bin_openflags
599610
if _O_TMPFILE_WORKS:
600-
try:
611+
fd = None
612+
def opener(*args):
613+
nonlocal fd
601614
flags2 = (flags | _os.O_TMPFILE) & ~_os.O_CREAT
602615
fd = _os.open(dir, flags2, 0o600)
616+
return fd
617+
try:
618+
file = _io.open(dir, mode, buffering=buffering,
619+
newline=newline, encoding=encoding,
620+
errors=errors, opener=opener)
621+
raw = getattr(file, 'buffer', file)
622+
raw = getattr(raw, 'raw', raw)
623+
raw.name = fd
624+
return file
603625
except IsADirectoryError:
604626
# Linux kernel older than 3.11 ignores the O_TMPFILE flag:
605627
# O_TMPFILE is read as O_DIRECTORY. Trying to open a directory
@@ -616,24 +638,25 @@ def TemporaryFile(mode='w+b', buffering=-1, encoding=None,
616638
# fails with NotADirectoryError, because O_TMPFILE is read as
617639
# O_DIRECTORY.
618640
pass
619-
else:
620-
try:
621-
return _io.open(fd, mode, buffering=buffering,
622-
newline=newline, encoding=encoding,
623-
errors=errors)
624-
except:
625-
_os.close(fd)
626-
raise
627641
# Fallback to _mkstemp_inner().
628642

629-
(fd, name) = _mkstemp_inner(dir, prefix, suffix, flags, output_type)
630-
try:
631-
_os.unlink(name)
632-
return _io.open(fd, mode, buffering=buffering,
633-
newline=newline, encoding=encoding, errors=errors)
634-
except:
635-
_os.close(fd)
636-
raise
643+
fd = None
644+
def opener(*args):
645+
nonlocal fd
646+
fd, name = _mkstemp_inner(dir, prefix, suffix, flags, output_type)
647+
try:
648+
_os.unlink(name)
649+
except BaseException as e:
650+
_os.close(fd)
651+
raise
652+
return fd
653+
file = _io.open(dir, mode, buffering=buffering,
654+
newline=newline, encoding=encoding, errors=errors,
655+
opener=opener)
656+
raw = getattr(file, 'buffer', file)
657+
raw = getattr(raw, 'raw', raw)
658+
raw.name = fd
659+
return file
637660

638661
class SpooledTemporaryFile:
639662
"""Temporary file wrapper, specialized to switch from BytesIO

Diff for: Lib/test/test_tempfile.py

+54-36
Original file line numberDiff line numberDiff line change
@@ -290,19 +290,14 @@ def our_candidate_list():
290290
def raise_OSError(*args, **kwargs):
291291
raise OSError()
292292

293-
with support.swap_attr(io, "open", raise_OSError):
294-
# test again with failing io.open()
293+
with support.swap_attr(os, "open", raise_OSError):
294+
# test again with failing os.open()
295295
with self.assertRaises(FileNotFoundError):
296296
tempfile._get_default_tempdir()
297297
self.assertEqual(os.listdir(our_temp_directory), [])
298298

299-
def bad_writer(*args, **kwargs):
300-
fp = orig_open(*args, **kwargs)
301-
fp.write = raise_OSError
302-
return fp
303-
304-
with support.swap_attr(io, "open", bad_writer) as orig_open:
305-
# test again with failing write()
299+
with support.swap_attr(os, "write", raise_OSError):
300+
# test again with failing os.write()
306301
with self.assertRaises(FileNotFoundError):
307302
tempfile._get_default_tempdir()
308303
self.assertEqual(os.listdir(our_temp_directory), [])
@@ -977,6 +972,7 @@ def test_del_on_close(self):
977972
try:
978973
with tempfile.NamedTemporaryFile(dir=dir) as f:
979974
f.write(b'blat')
975+
self.assertEqual(os.listdir(dir), [])
980976
self.assertFalse(os.path.exists(f.name),
981977
"NamedTemporaryFile %s exists after close" % f.name)
982978
finally:
@@ -1016,19 +1012,6 @@ def use_closed():
10161012
pass
10171013
self.assertRaises(ValueError, use_closed)
10181014

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

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

10431044
class TestSpooledTemporaryFile(BaseTestCase):
@@ -1068,8 +1069,10 @@ def test_del_on_close(self):
10681069
self.assertTrue(f._rolled)
10691070
filename = f.name
10701071
f.close()
1071-
self.assertFalse(isinstance(filename, str) and os.path.exists(filename),
1072-
"SpooledTemporaryFile %s exists after close" % filename)
1072+
self.assertEqual(os.listdir(dir), [])
1073+
if not isinstance(filename, int):
1074+
self.assertFalse(os.path.exists(filename),
1075+
"SpooledTemporaryFile %s exists after close" % filename)
10731076
finally:
10741077
os.rmdir(dir)
10751078

@@ -1356,19 +1359,34 @@ def roundtrip(input, *args, **kwargs):
13561359
roundtrip("\u039B", "w+", encoding="utf-16")
13571360
roundtrip("foo\r\n", "w+", newline="")
13581361

1359-
def test_no_leak_fd(self):
1360-
# Issue #21058: don't leak file descriptor when io.open() fails
1361-
closed = []
1362-
os_close = os.close
1363-
def close(fd):
1364-
closed.append(fd)
1365-
os_close(fd)
1366-
1367-
with mock.patch('os.close', side_effect=close):
1368-
with mock.patch('io.open', side_effect=ValueError):
1369-
self.assertRaises(ValueError, tempfile.TemporaryFile)
1370-
self.assertEqual(len(closed), 1)
1362+
def test_bad_mode(self):
1363+
dir = tempfile.mkdtemp()
1364+
self.addCleanup(os_helper.rmtree, dir)
1365+
with self.assertRaises(ValueError):
1366+
tempfile.TemporaryFile(mode='wr', dir=dir)
1367+
with self.assertRaises(TypeError):
1368+
tempfile.TemporaryFile(mode=2, dir=dir)
1369+
self.assertEqual(os.listdir(dir), [])
1370+
1371+
def test_bad_encoding(self):
1372+
dir = tempfile.mkdtemp()
1373+
self.addCleanup(os_helper.rmtree, dir)
1374+
with self.assertRaises(LookupError):
1375+
tempfile.TemporaryFile('w', encoding='bad-encoding', dir=dir)
1376+
self.assertEqual(os.listdir(dir), [])
13711377

1378+
def test_unexpected_error(self):
1379+
dir = tempfile.mkdtemp()
1380+
self.addCleanup(os_helper.rmtree, dir)
1381+
with mock.patch('tempfile._O_TMPFILE_WORKS', False), \
1382+
mock.patch('os.unlink') as mock_unlink, \
1383+
mock.patch('os.open') as mock_open, \
1384+
mock.patch('os.close') as mock_close:
1385+
mock_unlink.side_effect = KeyboardInterrupt()
1386+
with self.assertRaises(KeyboardInterrupt):
1387+
tempfile.TemporaryFile(dir=dir)
1388+
mock_close.assert_called()
1389+
self.assertEqual(os.listdir(dir), [])
13721390

13731391

13741392
# 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)