[Python-checkins] bpo-46785: Fix race condition between os.stat() and unlink on Windows (GH-31858)

zooba webhook-mailer at python.org
Mon May 2 19:19:18 EDT 2022


https://github.com/python/cpython/commit/39e6b8ae6a5b49bb23746fdcc354d148ff2d98e3
commit: 39e6b8ae6a5b49bb23746fdcc354d148ff2d98e3
branch: main
author: Itai Steinherz <itaisteinherz at gmail.com>
committer: zooba <steve.dower at microsoft.com>
date: 2022-05-03T00:19:13+01:00
summary:

bpo-46785: Fix race condition between os.stat() and unlink on Windows (GH-31858)

files:
A Misc/NEWS.d/next/Windows/2022-03-13-20-35-41.bpo-46785.Pnknyl.rst
M Lib/test/test_os.py
M Misc/ACKS
M Modules/posixmodule.c

diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py
index 5f73af81d4560..36ad587760d70 100644
--- a/Lib/test/test_os.py
+++ b/Lib/test/test_os.py
@@ -24,6 +24,7 @@
 import sys
 import sysconfig
 import tempfile
+import textwrap
 import time
 import types
 import unittest
@@ -2883,6 +2884,49 @@ def test_getfinalpathname_handles(self):
 
         self.assertEqual(0, handle_delta)
 
+    @support.requires_subprocess()
+    def test_stat_unlink_race(self):
+        # bpo-46785: the implementation of os.stat() falls back to reading
+        # the parent directory if CreateFileW() fails with a permission
+        # error. If reading the parent directory fails because the file or
+        # directory are subsequently unlinked, or because the volume or
+        # share are no longer available, then the original permission error
+        # should not be restored.
+        filename =  os_helper.TESTFN
+        self.addCleanup(os_helper.unlink, filename)
+        deadline = time.time() + 5
+        command = textwrap.dedent("""\
+            import os
+            import sys
+            import time
+
+            filename = sys.argv[1]
+            deadline = float(sys.argv[2])
+
+            while time.time() < deadline:
+                try:
+                    with open(filename, "w") as f:
+                        pass
+                except OSError:
+                    pass
+                try:
+                    os.remove(filename)
+                except OSError:
+                    pass
+            """)
+
+        with subprocess.Popen([sys.executable, '-c', command, filename, str(deadline)]) as proc:
+            while time.time() < deadline:
+                try:
+                    os.stat(filename)
+                except FileNotFoundError as e:
+                    assert e.winerror == 2  # ERROR_FILE_NOT_FOUND
+            try:
+                proc.wait(1)
+            except subprocess.TimeoutExpired:
+                proc.terminate()
+
+
 @os_helper.skip_unless_symlink
 class NonLocalSymlinkTests(unittest.TestCase):
 
diff --git a/Misc/ACKS b/Misc/ACKS
index 5e66a2e757adf..30b698f90e83b 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -1705,6 +1705,7 @@ Anthony Starks
 David Steele
 Oliver Steele
 Greg Stein
+Itai Steinherz
 Marek Stepniowski
 Baruch Sterin
 Chris Stern
diff --git a/Misc/NEWS.d/next/Windows/2022-03-13-20-35-41.bpo-46785.Pnknyl.rst b/Misc/NEWS.d/next/Windows/2022-03-13-20-35-41.bpo-46785.Pnknyl.rst
new file mode 100644
index 0000000000000..0a87abd77c8ff
--- /dev/null
+++ b/Misc/NEWS.d/next/Windows/2022-03-13-20-35-41.bpo-46785.Pnknyl.rst
@@ -0,0 +1 @@
+Fix race condition between :func:`os.stat` and unlinking a file on Windows, by using errors codes returned by ``FindFirstFileW()`` when appropriate in ``win32_xstat_impl``.
diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c
index a2ea5079969e9..a7f57804687cd 100644
--- a/Modules/posixmodule.c
+++ b/Modules/posixmodule.c
@@ -1890,7 +1890,17 @@ win32_xstat_impl(const wchar_t *path, struct _Py_stat_struct *result,
             /* Try reading the parent directory. */
             if (!attributes_from_dir(path, &fileInfo, &tagInfo.ReparseTag)) {
                 /* Cannot read the parent directory. */
-                SetLastError(error);
+                switch (GetLastError()) {
+                case ERROR_FILE_NOT_FOUND: /* File cannot be found */
+                case ERROR_PATH_NOT_FOUND: /* File parent directory cannot be found */
+                case ERROR_NOT_READY: /* Drive exists but unavailable */
+                case ERROR_BAD_NET_NAME: /* Remote drive unavailable */
+                    break;
+                /* Restore the error from CreateFileW(). */
+                default:
+                    SetLastError(error);
+                }
+
                 return -1;
             }
             if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {



More information about the Python-checkins mailing list