Skip to content

Commit ca66d3c

Browse files
gh-66305: Fix a hang on Windows in the tempfile module (GH-144672)
It occurred when trying to create a temporary file or subdirectory in a non-writable directory.
1 parent b32c830 commit ca66d3c

File tree

3 files changed

+46
-24
lines changed

3 files changed

+46
-24
lines changed

Lib/tempfile.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,11 @@
5757
if hasattr(_os, 'O_BINARY'):
5858
_bin_openflags |= _os.O_BINARY
5959

60-
if hasattr(_os, 'TMP_MAX'):
61-
TMP_MAX = _os.TMP_MAX
62-
else:
63-
TMP_MAX = 10000
60+
# This is more than enough.
61+
# Each name contains over 40 random bits. Even with a million temporary
62+
# files, the chance of a conflict is less than 1 in a million, and with
63+
# 20 attempts, it is less than 1e-120.
64+
TMP_MAX = 20
6465

6566
# This variable _was_ unused for legacy reasons, see issue 10354.
6667
# But as of 3.5 we actually use it at runtime so changing it would
@@ -196,8 +197,7 @@ def _get_default_tempdir(dirlist=None):
196197
for dir in dirlist:
197198
if dir != _os.curdir:
198199
dir = _os.path.abspath(dir)
199-
# Try only a few names per directory.
200-
for seq in range(100):
200+
for seq in range(TMP_MAX):
201201
name = next(namer)
202202
filename = _os.path.join(dir, name)
203203
try:
@@ -213,10 +213,8 @@ def _get_default_tempdir(dirlist=None):
213213
except FileExistsError:
214214
pass
215215
except PermissionError:
216-
# This exception is thrown when a directory with the chosen name
217-
# already exists on windows.
218-
if (_os.name == 'nt' and _os.path.isdir(dir) and
219-
_os.access(dir, _os.W_OK)):
216+
# See the comment in mkdtemp().
217+
if _os.name == 'nt' and _os.path.isdir(dir):
220218
continue
221219
break # no point trying more names in this directory
222220
except OSError:
@@ -258,10 +256,8 @@ def _mkstemp_inner(dir, pre, suf, flags, output_type):
258256
except FileExistsError:
259257
continue # try again
260258
except PermissionError:
261-
# This exception is thrown when a directory with the chosen name
262-
# already exists on windows.
263-
if (_os.name == 'nt' and _os.path.isdir(dir) and
264-
_os.access(dir, _os.W_OK)):
259+
# See the comment in mkdtemp().
260+
if _os.name == 'nt' and _os.path.isdir(dir) and seq < TMP_MAX - 1:
265261
continue
266262
else:
267263
raise
@@ -386,10 +382,14 @@ def mkdtemp(suffix=None, prefix=None, dir=None):
386382
except FileExistsError:
387383
continue # try again
388384
except PermissionError:
389-
# This exception is thrown when a directory with the chosen name
390-
# already exists on windows.
391-
if (_os.name == 'nt' and _os.path.isdir(dir) and
392-
_os.access(dir, _os.W_OK)):
385+
# On Posix, this exception is raised when the user has no
386+
# write access to the parent directory.
387+
# On Windows, it is also raised when a directory with
388+
# the chosen name already exists, or if the parent directory
389+
# is not a directory.
390+
# We cannot distinguish between "directory-exists-error" and
391+
# "access-denied-error".
392+
if _os.name == 'nt' and _os.path.isdir(dir) and seq < TMP_MAX - 1:
393393
continue
394394
else:
395395
raise

Lib/test/test_tempfile.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -330,17 +330,36 @@ def _mock_candidate_names(*names):
330330
class TestBadTempdir:
331331
def test_read_only_directory(self):
332332
with _inside_empty_temp_dir():
333-
oldmode = mode = os.stat(tempfile.tempdir).st_mode
334-
mode &= ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
335-
os.chmod(tempfile.tempdir, mode)
333+
probe = os.path.join(tempfile.tempdir, 'probe')
334+
if os.name == 'nt':
335+
cmd = ['icacls', tempfile.tempdir, '/deny', 'Everyone:(W)']
336+
stdout = None if support.verbose > 1 else subprocess.DEVNULL
337+
subprocess.run(cmd, check=True, stdout=stdout)
338+
else:
339+
oldmode = mode = os.stat(tempfile.tempdir).st_mode
340+
mode &= ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
341+
mode = stat.S_IREAD
342+
os.chmod(tempfile.tempdir, mode)
336343
try:
337-
if os.access(tempfile.tempdir, os.W_OK):
344+
# Check that the directory is read-only.
345+
try:
346+
os.mkdir(probe)
347+
except PermissionError:
348+
pass
349+
else:
350+
os.rmdir(probe)
338351
self.skipTest("can't set the directory read-only")
352+
# gh-66305: Now it takes a split second, but previously
353+
# it took about 10 days on Windows.
339354
with self.assertRaises(PermissionError):
340355
self.make_temp()
341-
self.assertEqual(os.listdir(tempfile.tempdir), [])
342356
finally:
343-
os.chmod(tempfile.tempdir, oldmode)
357+
if os.name == 'nt':
358+
cmd = ['icacls', tempfile.tempdir, '/grant:r', 'Everyone:(M)']
359+
subprocess.run(cmd, check=True, stdout=stdout)
360+
else:
361+
os.chmod(tempfile.tempdir, oldmode)
362+
self.assertEqual(os.listdir(tempfile.tempdir), [])
344363

345364
def test_nonexisting_directory(self):
346365
with _inside_empty_temp_dir():
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fixed a hang on Windows in the :mod:`tempfile` module when
2+
trying to create a temporary file or subdirectory in a non-writable
3+
directory.

0 commit comments

Comments
 (0)