Skip to content

Commit 9be9b58

Browse files
bpo-46785: Fix race condition between os.stat() and unlink on Windows (GH-31858)
* [3.10] bpo-46785: Fix race condition between os.stat() and unlink on Windows (GH-31858). (cherry picked from commit 39e6b8a) Co-authored-by: Itai Steinherz <[email protected]>
1 parent dd0e8a6 commit 9be9b58

File tree

4 files changed

+56
-1
lines changed

4 files changed

+56
-1
lines changed

Lib/test/test_os.py

+43
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import sys
2424
import sysconfig
2525
import tempfile
26+
import textwrap
2627
import threading
2728
import time
2829
import types
@@ -2859,6 +2860,48 @@ def test_getfinalpathname_handles(self):
28592860

28602861
self.assertEqual(0, handle_delta)
28612862

2863+
def test_stat_unlink_race(self):
2864+
# bpo-46785: the implementation of os.stat() falls back to reading
2865+
# the parent directory if CreateFileW() fails with a permission
2866+
# error. If reading the parent directory fails because the file or
2867+
# directory are subsequently unlinked, or because the volume or
2868+
# share are no longer available, then the original permission error
2869+
# should not be restored.
2870+
filename = os_helper.TESTFN
2871+
self.addCleanup(os_helper.unlink, filename)
2872+
deadline = time.time() + 5
2873+
command = textwrap.dedent("""\
2874+
import os
2875+
import sys
2876+
import time
2877+
2878+
filename = sys.argv[1]
2879+
deadline = float(sys.argv[2])
2880+
2881+
while time.time() < deadline:
2882+
try:
2883+
with open(filename, "w") as f:
2884+
pass
2885+
except OSError:
2886+
pass
2887+
try:
2888+
os.remove(filename)
2889+
except OSError:
2890+
pass
2891+
""")
2892+
2893+
with subprocess.Popen([sys.executable, '-c', command, filename, str(deadline)]) as proc:
2894+
while time.time() < deadline:
2895+
try:
2896+
os.stat(filename)
2897+
except FileNotFoundError as e:
2898+
assert e.winerror == 2 # ERROR_FILE_NOT_FOUND
2899+
try:
2900+
proc.wait(1)
2901+
except subprocess.TimeoutExpired:
2902+
proc.terminate()
2903+
2904+
28622905
@os_helper.skip_unless_symlink
28632906
class NonLocalSymlinkTests(unittest.TestCase):
28642907

Misc/ACKS

+1
Original file line numberDiff line numberDiff line change
@@ -1694,6 +1694,7 @@ Anthony Starks
16941694
David Steele
16951695
Oliver Steele
16961696
Greg Stein
1697+
Itai Steinherz
16971698
Marek Stepniowski
16981699
Baruch Sterin
16991700
Chris Stern
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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``.

Modules/posixmodule.c

+11-1
Original file line numberDiff line numberDiff line change
@@ -1905,7 +1905,17 @@ win32_xstat_impl(const wchar_t *path, struct _Py_stat_struct *result,
19051905
/* Try reading the parent directory. */
19061906
if (!attributes_from_dir(path, &fileInfo, &tagInfo.ReparseTag)) {
19071907
/* Cannot read the parent directory. */
1908-
SetLastError(error);
1908+
switch (GetLastError()) {
1909+
case ERROR_FILE_NOT_FOUND: /* File cannot be found */
1910+
case ERROR_PATH_NOT_FOUND: /* File parent directory cannot be found */
1911+
case ERROR_NOT_READY: /* Drive exists but unavailable */
1912+
case ERROR_BAD_NET_NAME: /* Remote drive unavailable */
1913+
break;
1914+
/* Restore the error from CreateFileW(). */
1915+
default:
1916+
SetLastError(error);
1917+
}
1918+
19091919
return -1;
19101920
}
19111921
if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {

0 commit comments

Comments
 (0)