https://github.com/python/cpython/commit/da03b36f455c29fd984d65d63735442e936b4f24
commit: da03b36f455c29fd984d65d63735442e936b4f24
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-02-24T11:32:12Z
summary:

[3.14] gh-66305: Fix a hang on Windows in the tempfile module (GH-144672) 
(GH-145168)

It occurred when trying to create a temporary file or subdirectory in
a non-writable directory.
(cherry picked from commit ca66d3c40cd9ac1fb94dd7cd79ccb8fecf019527)

Co-authored-by: Serhiy Storchaka <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-02-10-16-56-05.gh-issue-66305.PZ6GN8.rst
M Lib/tempfile.py
M Lib/test/test_tempfile.py

diff --git a/Lib/tempfile.py b/Lib/tempfile.py
index 5e3ccab5f48502..a34e062f8399a0 100644
--- a/Lib/tempfile.py
+++ b/Lib/tempfile.py
@@ -57,10 +57,11 @@
 if hasattr(_os, 'O_BINARY'):
     _bin_openflags |= _os.O_BINARY
 
-if hasattr(_os, 'TMP_MAX'):
-    TMP_MAX = _os.TMP_MAX
-else:
-    TMP_MAX = 10000
+# This is more than enough.
+# Each name contains over 40 random bits.  Even with a million temporary
+# files, the chance of a conflict is less than 1 in a million, and with
+# 20 attempts, it is less than 1e-120.
+TMP_MAX = 20
 
 # This variable _was_ unused for legacy reasons, see issue 10354.
 # 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):
     for dir in dirlist:
         if dir != _os.curdir:
             dir = _os.path.abspath(dir)
-        # Try only a few names per directory.
-        for seq in range(100):
+        for seq in range(TMP_MAX):
             name = next(namer)
             filename = _os.path.join(dir, name)
             try:
@@ -213,10 +213,8 @@ def _get_default_tempdir(dirlist=None):
             except FileExistsError:
                 pass
             except PermissionError:
-                # This exception is thrown when a directory with the chosen 
name
-                # already exists on windows.
-                if (_os.name == 'nt' and _os.path.isdir(dir) and
-                    _os.access(dir, _os.W_OK)):
+                # See the comment in mkdtemp().
+                if _os.name == 'nt' and _os.path.isdir(dir):
                     continue
                 break   # no point trying more names in this directory
             except OSError:
@@ -258,10 +256,8 @@ def _mkstemp_inner(dir, pre, suf, flags, output_type):
         except FileExistsError:
             continue    # try again
         except PermissionError:
-            # This exception is thrown when a directory with the chosen name
-            # already exists on windows.
-            if (_os.name == 'nt' and _os.path.isdir(dir) and
-                _os.access(dir, _os.W_OK)):
+            # See the comment in mkdtemp().
+            if _os.name == 'nt' and _os.path.isdir(dir) and seq < TMP_MAX - 1:
                 continue
             else:
                 raise
@@ -386,10 +382,14 @@ def mkdtemp(suffix=None, prefix=None, dir=None):
         except FileExistsError:
             continue    # try again
         except PermissionError:
-            # This exception is thrown when a directory with the chosen name
-            # already exists on windows.
-            if (_os.name == 'nt' and _os.path.isdir(dir) and
-                _os.access(dir, _os.W_OK)):
+            # On Posix, this exception is raised when the user has no
+            # write access to the parent directory.
+            # On Windows, it is also raised when a directory with
+            # the chosen name already exists, or if the parent directory
+            # is not a directory.
+            # We cannot distinguish between "directory-exists-error" and
+            # "access-denied-error".
+            if _os.name == 'nt' and _os.path.isdir(dir) and seq < TMP_MAX - 1:
                 continue
             else:
                 raise
diff --git a/Lib/test/test_tempfile.py b/Lib/test/test_tempfile.py
index 52b13b98cbcce5..2c524635b572e5 100644
--- a/Lib/test/test_tempfile.py
+++ b/Lib/test/test_tempfile.py
@@ -330,17 +330,36 @@ def _mock_candidate_names(*names):
 class TestBadTempdir:
     def test_read_only_directory(self):
         with _inside_empty_temp_dir():
-            oldmode = mode = os.stat(tempfile.tempdir).st_mode
-            mode &= ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
-            os.chmod(tempfile.tempdir, mode)
+            probe = os.path.join(tempfile.tempdir, 'probe')
+            if os.name == 'nt':
+                cmd = ['icacls', tempfile.tempdir, '/deny', 'Everyone:(W)']
+                stdout = None if support.verbose > 1 else subprocess.DEVNULL
+                subprocess.run(cmd, check=True, stdout=stdout)
+            else:
+                oldmode = mode = os.stat(tempfile.tempdir).st_mode
+                mode &= ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
+                mode = stat.S_IREAD
+                os.chmod(tempfile.tempdir, mode)
             try:
-                if os.access(tempfile.tempdir, os.W_OK):
+                # Check that the directory is read-only.
+                try:
+                    os.mkdir(probe)
+                except PermissionError:
+                    pass
+                else:
+                    os.rmdir(probe)
                     self.skipTest("can't set the directory read-only")
+                # gh-66305: Now it takes a split second, but previously
+                # it took about 10 days on Windows.
                 with self.assertRaises(PermissionError):
                     self.make_temp()
-                self.assertEqual(os.listdir(tempfile.tempdir), [])
             finally:
-                os.chmod(tempfile.tempdir, oldmode)
+                if os.name == 'nt':
+                    cmd = ['icacls', tempfile.tempdir, '/grant:r', 
'Everyone:(M)']
+                    subprocess.run(cmd, check=True, stdout=stdout)
+                else:
+                    os.chmod(tempfile.tempdir, oldmode)
+            self.assertEqual(os.listdir(tempfile.tempdir), [])
 
     def test_nonexisting_directory(self):
         with _inside_empty_temp_dir():
diff --git 
a/Misc/NEWS.d/next/Library/2026-02-10-16-56-05.gh-issue-66305.PZ6GN8.rst 
b/Misc/NEWS.d/next/Library/2026-02-10-16-56-05.gh-issue-66305.PZ6GN8.rst
new file mode 100644
index 00000000000000..276711e6ba3900
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-02-10-16-56-05.gh-issue-66305.PZ6GN8.rst
@@ -0,0 +1,3 @@
+Fixed a hang on Windows in the :mod:`tempfile` module when
+trying to create a temporary file or subdirectory in a non-writable
+directory.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to