Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/filelock/_windows.py: 10%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import os
4import sys
5import time
6from contextlib import suppress
7from pathlib import Path
8from typing import Final, cast
10from ._api import BaseFileLock
11from ._util import ensure_directory_exists, raise_on_not_writable_file
13if sys.platform == "win32": # pragma: win32 cover
14 import ctypes
15 import msvcrt
16 from ctypes import wintypes
18 _GENERIC_READ: Final[int] = 0x80000000
19 _GENERIC_WRITE: Final[int] = 0x40000000
20 _SYNCHRONIZE: Final[int] = 0x00100000
21 _DESIRED_ACCESS: Final[int] = _GENERIC_READ | _GENERIC_WRITE | _SYNCHRONIZE
22 _FILE_SHARE_READ_WRITE: Final[int] = (
23 0x00000001 | 0x00000002
24 ) # read | write; matches os.open (_SH_DENYNO), no delete
25 _FILE_OPEN_IF: Final[int] = 3 # open the file if it exists, create it otherwise; the NtCreateFile OPEN_ALWAYS
26 _FILE_ATTRIBUTE_READONLY: Final[int] = 0x00000001
27 _FILE_ATTRIBUTE_NORMAL: Final[int] = 0x00000080
28 _FILE_ATTRIBUTE_REPARSE_POINT: Final[int] = 0x00000400
29 # CreateOptions: keep the handle synchronous (the CRT and msvcrt.locking rely on a maintained file position),
30 # refuse a directory, and open a reparse point rather than following it so the check below acts on the link itself.
31 _FILE_SYNCHRONOUS_IO_NONALERT: Final[int] = 0x00000020
32 _FILE_NON_DIRECTORY_FILE: Final[int] = 0x00000040
33 _FILE_OPEN_REPARSE_POINT: Final[int] = 0x00200000
34 _CREATE_OPTIONS: Final[int] = _FILE_SYNCHRONOUS_IO_NONALERT | _FILE_NON_DIRECTORY_FILE | _FILE_OPEN_REPARSE_POINT
35 _OBJ_CASE_INSENSITIVE: Final[int] = 0x00000040 # Win32 name lookups are case-insensitive
36 _OWNER_WRITE: Final[int] = 0o200
38 # LockFileEx locks a byte range at an offset carried in OVERLAPPED, independent of the descriptor's file position.
39 # msvcrt.locking starts at the current position instead, so a metadata write between lock and unlock could shift
40 # the byte a later unlock targets; the explicit offset removes that hazard for both the path lock and #608's
41 # descriptor lock.
42 _LOCKFILE_FAIL_IMMEDIATELY: Final[int] = 0x00000001
43 _LOCKFILE_EXCLUSIVE_LOCK: Final[int] = 0x00000002
44 _ERROR_LOCK_VIOLATION: Final[int] = 33 # another handle holds the byte range
46 # NtCreateFile returns the raw NTSTATUS as its value, where CreateFileW collapses several of these into one
47 # ERROR_ACCESS_DENIED. Telling them apart is the point (#604): a name pending deletion or a share conflict is
48 # transient and worth a retry, a real access denial is not.
49 _STATUS_SUCCESS: Final[int] = 0x00000000
50 _STATUS_ACCESS_DENIED: Final[int] = 0xC0000022
51 _STATUS_SHARING_VIOLATION: Final[int] = 0xC0000043
52 _STATUS_DELETE_PENDING: Final[int] = 0xC0000056
53 #: How long an open waits out a STATUS_ACCESS_DENIED before treating it as a real denial: NTFS answers it for a
54 #: moment while a peer's unlink tears the name down, so a bounded wait keeps a real denial failing fast (#604).
55 _ACCESS_DENIED_GRACE: Final[float] = 0.5
56 _ACCESS_DENIED_RETRY: Final[float] = 0.002
58 _ntdll: Final[ctypes.WinDLL] = ctypes.WinDLL("ntdll")
59 _kernel32: Final[ctypes.WinDLL] = ctypes.WinDLL("kernel32", use_last_error=True)
61 class _UNICODE_STRING(ctypes.Structure): # ruff:ignore[invalid-class-name] # mirrors the Win32 struct name
62 _fields_ = (
63 ("Length", wintypes.USHORT), # byte length, not character count
64 ("MaximumLength", wintypes.USHORT),
65 ("Buffer", wintypes.LPWSTR),
66 )
68 class _OBJECT_ATTRIBUTES(ctypes.Structure): # ruff:ignore[invalid-class-name] # mirrors the Win32 struct name
69 _fields_ = (
70 ("Length", wintypes.ULONG),
71 ("RootDirectory", wintypes.HANDLE),
72 ("ObjectName", ctypes.POINTER(_UNICODE_STRING)),
73 ("Attributes", wintypes.ULONG),
74 ("SecurityDescriptor", ctypes.c_void_p),
75 ("SecurityQualityOfService", ctypes.c_void_p),
76 )
78 class _IO_STATUS_BLOCK(ctypes.Structure): # ruff:ignore[invalid-class-name] # mirrors the Win32 struct name
79 _fields_ = (
80 ("Status", ctypes.c_void_p), # a union of NTSTATUS and PVOID, so it is pointer-sized
81 ("Information", ctypes.c_void_p),
82 )
84 class _OVERLAPPED(ctypes.Structure): # mirrors the Win32 struct name
85 _fields_ = (
86 ("Internal", ctypes.c_void_p), # ULONG_PTR: pointer-sized, not DWORD, or the x64 layout corrupts Offset
87 ("InternalHigh", ctypes.c_void_p),
88 ("Offset", wintypes.DWORD), # the DUMMYUNIONNAME struct, flattened: low 32 bits of the byte offset
89 ("OffsetHigh", wintypes.DWORD),
90 ("hEvent", wintypes.HANDLE),
91 )
93 class _BY_HANDLE_FILE_INFORMATION(ctypes.Structure): # ruff:ignore[invalid-class-name] # mirrors the Win32 struct name
94 _fields_ = (
95 ("dwFileAttributes", wintypes.DWORD),
96 ("ftCreationTime", wintypes.FILETIME),
97 ("ftLastAccessTime", wintypes.FILETIME),
98 ("ftLastWriteTime", wintypes.FILETIME),
99 ("dwVolumeSerialNumber", wintypes.DWORD),
100 ("nFileSizeHigh", wintypes.DWORD),
101 ("nFileSizeLow", wintypes.DWORD),
102 ("nNumberOfLinks", wintypes.DWORD),
103 ("nFileIndexHigh", wintypes.DWORD),
104 ("nFileIndexLow", wintypes.DWORD),
105 )
107 _ntdll.NtCreateFile.restype = wintypes.LONG # NTSTATUS
108 _ntdll.NtCreateFile.argtypes = [
109 ctypes.POINTER(wintypes.HANDLE),
110 wintypes.DWORD,
111 ctypes.POINTER(_OBJECT_ATTRIBUTES),
112 ctypes.POINTER(_IO_STATUS_BLOCK),
113 ctypes.POINTER(ctypes.c_longlong), # PLARGE_INTEGER AllocationSize
114 wintypes.ULONG,
115 wintypes.ULONG,
116 wintypes.ULONG,
117 wintypes.ULONG,
118 ctypes.c_void_p,
119 wintypes.ULONG,
120 ]
121 _ntdll.RtlDosPathNameToNtPathName_U_WithStatus.restype = wintypes.LONG # NTSTATUS
122 _ntdll.RtlDosPathNameToNtPathName_U_WithStatus.argtypes = [
123 wintypes.LPCWSTR,
124 ctypes.POINTER(_UNICODE_STRING),
125 ctypes.c_void_p,
126 ctypes.c_void_p,
127 ]
128 _ntdll.RtlFreeUnicodeString.restype = None
129 _ntdll.RtlFreeUnicodeString.argtypes = [ctypes.POINTER(_UNICODE_STRING)]
130 _ntdll.RtlNtStatusToDosError.restype = wintypes.ULONG
131 _ntdll.RtlNtStatusToDosError.argtypes = [wintypes.LONG]
133 _kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
134 _kernel32.CloseHandle.restype = wintypes.BOOL
135 _kernel32.GetFileInformationByHandle.argtypes = [wintypes.HANDLE, ctypes.POINTER(_BY_HANDLE_FILE_INFORMATION)]
136 _kernel32.GetFileInformationByHandle.restype = wintypes.BOOL
137 _kernel32.LockFileEx.argtypes = [
138 wintypes.HANDLE,
139 wintypes.DWORD,
140 wintypes.DWORD,
141 wintypes.DWORD,
142 wintypes.DWORD,
143 ctypes.POINTER(_OVERLAPPED),
144 ]
145 _kernel32.LockFileEx.restype = wintypes.BOOL
146 _kernel32.UnlockFileEx.argtypes = [
147 wintypes.HANDLE,
148 wintypes.DWORD,
149 wintypes.DWORD,
150 wintypes.DWORD,
151 ctypes.POINTER(_OVERLAPPED),
152 ]
153 _kernel32.UnlockFileEx.restype = wintypes.BOOL
155 def _lock_fd_nonblocking(fd: int) -> bool:
156 # One nonblocking exclusive LockFileEx attempt shared by WindowsFileLock and lock_descriptor, over the one-byte
157 # range at offset 0. True on acquisition, False on contention, raise otherwise. The caller owns fd; the handle
158 # from get_osfhandle belongs to the CRT descriptor and must not be closed here.
159 overlapped = _OVERLAPPED() # zero-initialized, so Offset/OffsetHigh/hEvent are 0
160 flags = _LOCKFILE_EXCLUSIVE_LOCK | _LOCKFILE_FAIL_IMMEDIATELY
161 if _kernel32.LockFileEx(msvcrt.get_osfhandle(fd), flags, 0, 1, 0, ctypes.byref(overlapped)):
162 return True
163 err = ctypes.get_last_error()
164 if err == _ERROR_LOCK_VIOLATION:
165 return False
166 # A non-contention LockFileEx failure is not reproducible in-process.
167 raise ctypes.WinError(err) # pragma: no cover
169 def _unlock_fd(fd: int) -> None:
170 overlapped = _OVERLAPPED() # the same offset 0 and one-byte length the lock used
171 # Unlocking the exact range we hold does not fail.
172 if not _kernel32.UnlockFileEx(msvcrt.get_osfhandle(fd), 0, 1, 0, ctypes.byref(overlapped)): # pragma: no cover
173 raise ctypes.WinError(ctypes.get_last_error())
175 class WindowsFileLock(BaseFileLock):
176 """
177 Uses ``LockFileEx`` to hard lock a byte range of the lock file on Windows systems.
179 Lock file cleanup: Windows attempts to delete the lock file after release, but deletion is
180 not guaranteed in multi-threaded scenarios where another thread holds an open handle. The lock
181 file may persist on disk, which does not affect lock correctness.
182 """
184 def _acquire(self) -> None:
185 raise_on_not_writable_file(self.lock_file)
186 ensure_directory_exists(self.lock_file)
188 # The reparse test is bound to the opened handle, so a symlink or junction swapped in cannot defeat it
189 # through a check-then-open TOCTOU race.
190 fd = _open_non_reparse_fd(self.lock_file, self._open_mode())
191 if fd is None:
192 return # open contention (share conflict or a name pending deletion); let the retry loop try again
193 try:
194 locked = _lock_fd_nonblocking(fd)
195 if locked:
196 self._mark_descriptor_owned(fd)
197 except BaseException: # pragma: no cover # cleanup only if the lock attempt itself raises
198 os.close(fd)
199 raise
200 if not locked:
201 os.close(fd) # another holder owns the byte-range lock; let the retry loop try again
203 def _release(self) -> None:
204 fd = cast("int", self._context.lock_file_fd)
205 # Retain the descriptor until the OS unlock succeeds: if UnlockFileEx raises, the byte-range lock is still
206 # held, so is_locked must keep reporting held rather than losing the fd. Only after the unlock commits do
207 # close and unlink run as post-unlock cleanup; their failure cannot make the lock held again.
208 _unlock_fd(fd)
209 self._mark_descriptor_released()
210 self._close_released_fd(fd, default_suppresses=False)
211 if not self._preserve_lock_file: # preserve_lock_file keeps a stable file identity for the caller (#605)
212 with suppress(OSError):
213 Path(self.lock_file).unlink()
215 def _open_non_reparse_fd(path: str, mode: int) -> int | None:
216 """
217 Open *path* for locking while refusing reparse points, bound to the handle actually locked.
219 The file is opened through ``NtCreateFile`` with ``FILE_OPEN_REPARSE_POINT`` so a symlink or junction planted
220 at the path is not followed, and the reparse decision is read from *that* handle via
221 ``GetFileInformationByHandle`` rather than from a prior pathname query. Reading the held handle closes the
222 check-then-open race: an attacker cannot swap the path between validation and use because both act on the same
223 handle. Share mode omits delete so a peer cannot unlink or rename the file out from under a live holder,
224 matching ``os.open``'s ``_SH_DENYNO``.
226 ``NtCreateFile`` is used instead of ``CreateFileW`` because its return value carries the raw ``NTSTATUS``.
227 Windows collapses a transient delete-pending name and a permanent access denial into the same Win32
228 ``ERROR_ACCESS_DENIED``; the status keeps them apart, so a real denial fails fast instead of spinning until the
229 caller's timeout (#604).
231 The reparse option only guards the final path component; Windows still follows reparse points in intermediate
232 directories. This assumes the lock file sits in a lock directory untrusted users cannot modify. A path with
233 attacker-controlled parent directories would need component-by-component handle validation.
235 :param path: the lock file path.
236 :param mode: the permission mode; as ``os.open`` does on Windows, a cleared owner-write bit creates the file
237 read-only. The attribute only takes effect when the file is created, not when an existing one is opened.
239 :returns: a file descriptor owning the opened handle, or ``None`` on a sharing violation or a delete-pending
240 name the caller should treat as contention and retry.
242 :raises OSError: if the path resolves to a reparse point, or the open fails for any other reason, raised with
243 the Win32 error the status maps to. An access denial is raised only once it outlasts a short grace, since
244 a peer unlinking the file as it releases can make the open answer ``STATUS_ACCESS_DENIED`` for a moment.
246 """
247 # Emit the audit event os.open would, so consumers watching "open" still see the path-level open and can veto.
248 sys.audit("open", path, None, os.O_RDWR | os.O_CREAT)
249 read_only = not mode & _OWNER_WRITE
250 handle, status = _nt_open(path, read_only=read_only)
251 if status == _STATUS_ACCESS_DENIED:
252 deadline = time.monotonic() + _ACCESS_DENIED_GRACE
253 while status == _STATUS_ACCESS_DENIED and time.monotonic() < deadline:
254 time.sleep(_ACCESS_DENIED_RETRY)
255 handle, status = _nt_open(path, read_only=read_only)
256 if status != _STATUS_SUCCESS:
257 if status in {_STATUS_SHARING_VIOLATION, _STATUS_DELETE_PENDING}:
258 return None
259 winerror = _ntdll.RtlNtStatusToDosError(status)
260 raise OSError(None, ctypes.FormatError(winerror).strip(), path, winerror)
262 info = _BY_HANDLE_FILE_INFORMATION()
263 # Querying an open handle we just created does not fail.
264 if not _kernel32.GetFileInformationByHandle(handle, ctypes.byref(info)): # pragma: no cover
265 err = ctypes.get_last_error()
266 _kernel32.CloseHandle(handle)
267 raise ctypes.WinError(err)
268 if info.dwFileAttributes & _FILE_ATTRIBUTE_REPARSE_POINT:
269 _kernel32.CloseHandle(handle)
270 msg = f"Lock file is a reparse point (symlink/junction): {path}"
271 raise OSError(msg)
273 try:
274 # O_NOINHERIT mirrors os.open on Windows: the lock fd must not leak into child processes.
275 return msvcrt.open_osfhandle(handle, os.O_RDWR | os.O_NOINHERIT)
276 except BaseException: # pragma: no cover # open_osfhandle audits too; a hook raising must not leak the handle
277 _kernel32.CloseHandle(handle)
278 raise
280 def _nt_open(path: str, *, read_only: bool) -> tuple[int, int]:
281 """
282 Open *path* through ``NtCreateFile`` and return ``(handle, status)``.
284 ``RtlDosPathNameToNtPathName_U_WithStatus`` translates the Win32 path to the NT namespace, handling relative,
285 drive, UNC and extended-length path forms as Win32 itself would, and allocates a buffer that
286 ``RtlFreeUnicodeString`` releases. The handle is ``0`` unless the status is ``STATUS_SUCCESS``.
287 """
288 nt_name = _UNICODE_STRING()
289 status = _ntdll.RtlDosPathNameToNtPathName_U_WithStatus(path, ctypes.byref(nt_name), None, None) & 0xFFFFFFFF
290 if status != _STATUS_SUCCESS:
291 return 0, status
292 try:
293 attributes = _OBJECT_ATTRIBUTES()
294 attributes.Length = ctypes.sizeof(_OBJECT_ATTRIBUTES)
295 attributes.ObjectName = ctypes.pointer(nt_name)
296 attributes.Attributes = _OBJ_CASE_INSENSITIVE
297 handle = wintypes.HANDLE()
298 io_status = _IO_STATUS_BLOCK()
299 status = (
300 _ntdll.NtCreateFile(
301 ctypes.byref(handle),
302 _DESIRED_ACCESS,
303 ctypes.byref(attributes),
304 ctypes.byref(io_status),
305 None,
306 _FILE_ATTRIBUTE_READONLY if read_only else _FILE_ATTRIBUTE_NORMAL,
307 _FILE_SHARE_READ_WRITE,
308 _FILE_OPEN_IF,
309 _CREATE_OPTIONS,
310 None,
311 0,
312 )
313 & 0xFFFFFFFF
314 )
315 finally:
316 _ntdll.RtlFreeUnicodeString(ctypes.byref(nt_name))
317 if status != _STATUS_SUCCESS:
318 return 0, status
319 return handle.value or 0, status
321else: # pragma: win32 no cover
323 class WindowsFileLock(BaseFileLock):
324 """Uses ``LockFileEx`` to hard lock a byte range of the lock file on Windows systems."""
326 def _acquire(self) -> None:
327 raise NotImplementedError
329 def _release(self) -> None:
330 raise NotImplementedError
333__all__ = [
334 "WindowsFileLock",
335]