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

131 statements  

1from __future__ import annotations 

2 

3import os 

4import sys 

5from contextlib import suppress 

6from pathlib import Path 

7from typing import Final, cast 

8 

9from ._api import BaseFileLock 

10from ._util import ensure_directory_exists, raise_on_not_writable_file 

11 

12if sys.platform == "win32": # pragma: win32 cover 

13 import ctypes 

14 import msvcrt 

15 from ctypes import wintypes 

16 

17 _GENERIC_READ: Final[int] = 0x80000000 

18 _GENERIC_WRITE: Final[int] = 0x40000000 

19 _SYNCHRONIZE: Final[int] = 0x00100000 

20 _DESIRED_ACCESS: Final[int] = _GENERIC_READ | _GENERIC_WRITE | _SYNCHRONIZE 

21 _FILE_SHARE_READ_WRITE: Final[int] = ( 

22 0x00000001 | 0x00000002 

23 ) # read | write; matches os.open (_SH_DENYNO), no delete 

24 _FILE_OPEN_IF: Final[int] = 3 # open the file if it exists, create it otherwise; the NtCreateFile OPEN_ALWAYS 

25 _FILE_ATTRIBUTE_READONLY: Final[int] = 0x00000001 

26 _FILE_ATTRIBUTE_NORMAL: Final[int] = 0x00000080 

27 _FILE_ATTRIBUTE_REPARSE_POINT: Final[int] = 0x00000400 

28 # CreateOptions: keep the handle synchronous (the CRT and msvcrt.locking rely on a maintained file position), 

29 # refuse a directory, and open a reparse point rather than following it so the check below acts on the link itself. 

30 _FILE_SYNCHRONOUS_IO_NONALERT: Final[int] = 0x00000020 

31 _FILE_NON_DIRECTORY_FILE: Final[int] = 0x00000040 

32 _FILE_OPEN_REPARSE_POINT: Final[int] = 0x00200000 

33 _CREATE_OPTIONS: Final[int] = _FILE_SYNCHRONOUS_IO_NONALERT | _FILE_NON_DIRECTORY_FILE | _FILE_OPEN_REPARSE_POINT 

34 _OBJ_CASE_INSENSITIVE: Final[int] = 0x00000040 # Win32 name lookups are case-insensitive 

35 _OWNER_WRITE: Final[int] = 0o200 

36 

37 # LockFileEx locks a byte range at an offset carried in OVERLAPPED, independent of the descriptor's file position. 

38 # msvcrt.locking starts at the current position instead, so a metadata write between lock and unlock could shift 

39 # the byte a later unlock targets; the explicit offset removes that hazard for both the path lock and #608's 

40 # descriptor lock. 

41 _LOCKFILE_FAIL_IMMEDIATELY: Final[int] = 0x00000001 

42 _LOCKFILE_EXCLUSIVE_LOCK: Final[int] = 0x00000002 

43 _ERROR_LOCK_VIOLATION: Final[int] = 33 # another handle holds the byte range 

44 

45 # NtCreateFile returns the raw NTSTATUS as its value, where CreateFileW collapses several of these into one 

46 # ERROR_ACCESS_DENIED. Telling them apart is the point (#604): a name pending deletion or a share conflict is 

47 # transient and worth a retry, a real access denial is not. 

48 _STATUS_SUCCESS: Final[int] = 0x00000000 

49 _STATUS_ACCESS_DENIED: Final[int] = 0xC0000022 

50 _STATUS_SHARING_VIOLATION: Final[int] = 0xC0000043 

51 _STATUS_DELETE_PENDING: Final[int] = 0xC0000056 

52 

53 _ntdll: Final[ctypes.WinDLL] = ctypes.WinDLL("ntdll") 

54 _kernel32: Final[ctypes.WinDLL] = ctypes.WinDLL("kernel32", use_last_error=True) 

55 

56 class _UNICODE_STRING(ctypes.Structure): # ruff:ignore[invalid-class-name] # mirrors the Win32 struct name 

57 _fields_ = ( 

58 ("Length", wintypes.USHORT), # byte length, not character count 

59 ("MaximumLength", wintypes.USHORT), 

60 ("Buffer", wintypes.LPWSTR), 

61 ) 

62 

63 class _OBJECT_ATTRIBUTES(ctypes.Structure): # ruff:ignore[invalid-class-name] # mirrors the Win32 struct name 

64 _fields_ = ( 

65 ("Length", wintypes.ULONG), 

66 ("RootDirectory", wintypes.HANDLE), 

67 ("ObjectName", ctypes.POINTER(_UNICODE_STRING)), 

68 ("Attributes", wintypes.ULONG), 

69 ("SecurityDescriptor", ctypes.c_void_p), 

70 ("SecurityQualityOfService", ctypes.c_void_p), 

71 ) 

72 

73 class _IO_STATUS_BLOCK(ctypes.Structure): # ruff:ignore[invalid-class-name] # mirrors the Win32 struct name 

74 _fields_ = ( 

75 ("Status", ctypes.c_void_p), # a union of NTSTATUS and PVOID, so it is pointer-sized 

76 ("Information", ctypes.c_void_p), 

77 ) 

78 

79 class _OVERLAPPED(ctypes.Structure): # mirrors the Win32 struct name 

80 _fields_ = ( 

81 ("Internal", ctypes.c_void_p), # ULONG_PTR: pointer-sized, not DWORD, or the x64 layout corrupts Offset 

82 ("InternalHigh", ctypes.c_void_p), 

83 ("Offset", wintypes.DWORD), # the DUMMYUNIONNAME struct, flattened: low 32 bits of the byte offset 

84 ("OffsetHigh", wintypes.DWORD), 

85 ("hEvent", wintypes.HANDLE), 

86 ) 

87 

88 class _BY_HANDLE_FILE_INFORMATION(ctypes.Structure): # ruff:ignore[invalid-class-name] # mirrors the Win32 struct name 

89 _fields_ = ( 

90 ("dwFileAttributes", wintypes.DWORD), 

91 ("ftCreationTime", wintypes.FILETIME), 

92 ("ftLastAccessTime", wintypes.FILETIME), 

93 ("ftLastWriteTime", wintypes.FILETIME), 

94 ("dwVolumeSerialNumber", wintypes.DWORD), 

95 ("nFileSizeHigh", wintypes.DWORD), 

96 ("nFileSizeLow", wintypes.DWORD), 

97 ("nNumberOfLinks", wintypes.DWORD), 

98 ("nFileIndexHigh", wintypes.DWORD), 

99 ("nFileIndexLow", wintypes.DWORD), 

100 ) 

101 

102 _ntdll.NtCreateFile.restype = wintypes.LONG # NTSTATUS 

103 _ntdll.NtCreateFile.argtypes = [ 

104 ctypes.POINTER(wintypes.HANDLE), 

105 wintypes.DWORD, 

106 ctypes.POINTER(_OBJECT_ATTRIBUTES), 

107 ctypes.POINTER(_IO_STATUS_BLOCK), 

108 ctypes.POINTER(ctypes.c_longlong), # PLARGE_INTEGER AllocationSize 

109 wintypes.ULONG, 

110 wintypes.ULONG, 

111 wintypes.ULONG, 

112 wintypes.ULONG, 

113 ctypes.c_void_p, 

114 wintypes.ULONG, 

115 ] 

116 _ntdll.RtlDosPathNameToNtPathName_U_WithStatus.restype = wintypes.LONG # NTSTATUS 

117 _ntdll.RtlDosPathNameToNtPathName_U_WithStatus.argtypes = [ 

118 wintypes.LPCWSTR, 

119 ctypes.POINTER(_UNICODE_STRING), 

120 ctypes.c_void_p, 

121 ctypes.c_void_p, 

122 ] 

123 _ntdll.RtlFreeUnicodeString.restype = None 

124 _ntdll.RtlFreeUnicodeString.argtypes = [ctypes.POINTER(_UNICODE_STRING)] 

125 _ntdll.RtlNtStatusToDosError.restype = wintypes.ULONG 

126 _ntdll.RtlNtStatusToDosError.argtypes = [wintypes.LONG] 

127 

128 _kernel32.CloseHandle.argtypes = [wintypes.HANDLE] 

129 _kernel32.CloseHandle.restype = wintypes.BOOL 

130 _kernel32.GetFileInformationByHandle.argtypes = [wintypes.HANDLE, ctypes.POINTER(_BY_HANDLE_FILE_INFORMATION)] 

131 _kernel32.GetFileInformationByHandle.restype = wintypes.BOOL 

132 _kernel32.LockFileEx.argtypes = [ 

133 wintypes.HANDLE, 

134 wintypes.DWORD, 

135 wintypes.DWORD, 

136 wintypes.DWORD, 

137 wintypes.DWORD, 

138 ctypes.POINTER(_OVERLAPPED), 

139 ] 

140 _kernel32.LockFileEx.restype = wintypes.BOOL 

141 _kernel32.UnlockFileEx.argtypes = [ 

142 wintypes.HANDLE, 

143 wintypes.DWORD, 

144 wintypes.DWORD, 

145 wintypes.DWORD, 

146 ctypes.POINTER(_OVERLAPPED), 

147 ] 

148 _kernel32.UnlockFileEx.restype = wintypes.BOOL 

149 

150 def _lock_fd_nonblocking(fd: int) -> bool: 

151 # One nonblocking exclusive LockFileEx attempt shared by WindowsFileLock and lock_descriptor, over the one-byte 

152 # range at offset 0. True on acquisition, False on contention, raise otherwise. The caller owns fd; the handle 

153 # from get_osfhandle belongs to the CRT descriptor and must not be closed here. 

154 overlapped = _OVERLAPPED() # zero-initialized, so Offset/OffsetHigh/hEvent are 0 

155 flags = _LOCKFILE_EXCLUSIVE_LOCK | _LOCKFILE_FAIL_IMMEDIATELY 

156 if _kernel32.LockFileEx(msvcrt.get_osfhandle(fd), flags, 0, 1, 0, ctypes.byref(overlapped)): 

157 return True 

158 err = ctypes.get_last_error() 

159 if err == _ERROR_LOCK_VIOLATION: 

160 return False 

161 # A non-contention LockFileEx failure is not reproducible in-process. 

162 raise ctypes.WinError(err) # pragma: no cover 

163 

164 def _unlock_fd(fd: int) -> None: 

165 overlapped = _OVERLAPPED() # the same offset 0 and one-byte length the lock used 

166 # Unlocking the exact range we hold does not fail. 

167 if not _kernel32.UnlockFileEx(msvcrt.get_osfhandle(fd), 0, 1, 0, ctypes.byref(overlapped)): # pragma: no cover 

168 raise ctypes.WinError(ctypes.get_last_error()) 

169 

170 class WindowsFileLock(BaseFileLock): 

171 """ 

172 Uses ``LockFileEx`` to hard lock a byte range of the lock file on Windows systems. 

173 

174 Lock file cleanup: Windows attempts to delete the lock file after release, but deletion is 

175 not guaranteed in multi-threaded scenarios where another thread holds an open handle. The lock 

176 file may persist on disk, which does not affect lock correctness. 

177 """ 

178 

179 def _acquire(self) -> None: 

180 raise_on_not_writable_file(self.lock_file) 

181 ensure_directory_exists(self.lock_file) 

182 

183 # The reparse test is bound to the opened handle, so a symlink or junction swapped in cannot defeat it 

184 # through a check-then-open TOCTOU race. 

185 fd = _open_non_reparse_fd(self.lock_file, self._open_mode()) 

186 if fd is None: 

187 return # open contention (share conflict or a name pending deletion); let the retry loop try again 

188 try: 

189 locked = _lock_fd_nonblocking(fd) 

190 if locked: 

191 self._mark_descriptor_owned(fd) 

192 except BaseException: # pragma: no cover # cleanup only if the lock attempt itself raises 

193 os.close(fd) 

194 raise 

195 if not locked: 

196 os.close(fd) # another holder owns the byte-range lock; let the retry loop try again 

197 

198 def _release(self) -> None: 

199 fd = cast("int", self._context.lock_file_fd) 

200 # Retain the descriptor until the OS unlock succeeds: if UnlockFileEx raises, the byte-range lock is still 

201 # held, so is_locked must keep reporting held rather than losing the fd. Only after the unlock commits do 

202 # close and unlink run as post-unlock cleanup; their failure cannot make the lock held again. 

203 _unlock_fd(fd) 

204 self._mark_descriptor_released() 

205 self._close_released_fd(fd, default_suppresses=False) 

206 if not self._preserve_lock_file: # preserve_lock_file keeps a stable file identity for the caller (#605) 

207 with suppress(OSError): 

208 Path(self.lock_file).unlink() 

209 

210 def _open_non_reparse_fd(path: str, mode: int) -> int | None: 

211 """ 

212 Open *path* for locking while refusing reparse points, bound to the handle actually locked. 

213 

214 The file is opened through ``NtCreateFile`` with ``FILE_OPEN_REPARSE_POINT`` so a symlink or junction planted 

215 at the path is not followed, and the reparse decision is read from *that* handle via 

216 ``GetFileInformationByHandle`` rather than from a prior pathname query. Reading the held handle closes the 

217 check-then-open race: an attacker cannot swap the path between validation and use because both act on the same 

218 handle. Share mode omits delete so a peer cannot unlink or rename the file out from under a live holder, 

219 matching ``os.open``'s ``_SH_DENYNO``. 

220 

221 ``NtCreateFile`` is used instead of ``CreateFileW`` because its return value carries the raw ``NTSTATUS``. 

222 Windows collapses a transient delete-pending name and a permanent access denial into the same Win32 

223 ``ERROR_ACCESS_DENIED``; the status keeps them apart, so a real denial fails fast instead of spinning until the 

224 caller's timeout (#604). 

225 

226 The reparse option only guards the final path component; Windows still follows reparse points in intermediate 

227 directories. This assumes the lock file sits in a lock directory untrusted users cannot modify. A path with 

228 attacker-controlled parent directories would need component-by-component handle validation. 

229 

230 :param path: the lock file path. 

231 :param mode: the permission mode; as ``os.open`` does on Windows, a cleared owner-write bit creates the file 

232 read-only. The attribute only takes effect when the file is created, not when an existing one is opened. 

233 

234 :returns: a file descriptor owning the opened handle, or ``None`` on a sharing violation or a delete-pending 

235 name the caller should treat as contention and retry. 

236 

237 :raises OSError: if the path resolves to a reparse point, or the open fails for any other reason, raised with 

238 the Win32 error the status maps to. 

239 

240 """ 

241 # Emit the audit event os.open would, so consumers watching "open" still see the path-level open and can veto. 

242 sys.audit("open", path, None, os.O_RDWR | os.O_CREAT) 

243 handle, status = _nt_open(path, read_only=not mode & _OWNER_WRITE) 

244 if status != _STATUS_SUCCESS: 

245 if status in {_STATUS_SHARING_VIOLATION, _STATUS_DELETE_PENDING}: 

246 return None 

247 winerror = _ntdll.RtlNtStatusToDosError(status) 

248 raise OSError(None, ctypes.FormatError(winerror).strip(), path, winerror) 

249 

250 info = _BY_HANDLE_FILE_INFORMATION() 

251 # Querying an open handle we just created does not fail. 

252 if not _kernel32.GetFileInformationByHandle(handle, ctypes.byref(info)): # pragma: no cover 

253 err = ctypes.get_last_error() 

254 _kernel32.CloseHandle(handle) 

255 raise ctypes.WinError(err) 

256 if info.dwFileAttributes & _FILE_ATTRIBUTE_REPARSE_POINT: 

257 _kernel32.CloseHandle(handle) 

258 msg = f"Lock file is a reparse point (symlink/junction): {path}" 

259 raise OSError(msg) 

260 

261 try: 

262 # O_NOINHERIT mirrors os.open on Windows: the lock fd must not leak into child processes. 

263 return msvcrt.open_osfhandle(handle, os.O_RDWR | os.O_NOINHERIT) 

264 except BaseException: # pragma: no cover # open_osfhandle audits too; a hook raising must not leak the handle 

265 _kernel32.CloseHandle(handle) 

266 raise 

267 

268 def _nt_open(path: str, *, read_only: bool) -> tuple[int, int]: 

269 """ 

270 Open *path* through ``NtCreateFile`` and return ``(handle, status)``. 

271 

272 ``RtlDosPathNameToNtPathName_U_WithStatus`` translates the Win32 path to the NT namespace, handling relative, 

273 drive, UNC and extended-length path forms as Win32 itself would, and allocates a buffer that 

274 ``RtlFreeUnicodeString`` releases. The handle is ``0`` unless the status is ``STATUS_SUCCESS``. 

275 """ 

276 nt_name = _UNICODE_STRING() 

277 status = _ntdll.RtlDosPathNameToNtPathName_U_WithStatus(path, ctypes.byref(nt_name), None, None) & 0xFFFFFFFF 

278 if status != _STATUS_SUCCESS: 

279 return 0, status 

280 try: 

281 attributes = _OBJECT_ATTRIBUTES() 

282 attributes.Length = ctypes.sizeof(_OBJECT_ATTRIBUTES) 

283 attributes.ObjectName = ctypes.pointer(nt_name) 

284 attributes.Attributes = _OBJ_CASE_INSENSITIVE 

285 handle = wintypes.HANDLE() 

286 io_status = _IO_STATUS_BLOCK() 

287 status = ( 

288 _ntdll.NtCreateFile( 

289 ctypes.byref(handle), 

290 _DESIRED_ACCESS, 

291 ctypes.byref(attributes), 

292 ctypes.byref(io_status), 

293 None, 

294 _FILE_ATTRIBUTE_READONLY if read_only else _FILE_ATTRIBUTE_NORMAL, 

295 _FILE_SHARE_READ_WRITE, 

296 _FILE_OPEN_IF, 

297 _CREATE_OPTIONS, 

298 None, 

299 0, 

300 ) 

301 & 0xFFFFFFFF 

302 ) 

303 finally: 

304 _ntdll.RtlFreeUnicodeString(ctypes.byref(nt_name)) 

305 if status != _STATUS_SUCCESS: 

306 return 0, status 

307 return handle.value or 0, status 

308 

309else: # pragma: win32 no cover 

310 

311 class WindowsFileLock(BaseFileLock): 

312 """Uses ``LockFileEx`` to hard lock a byte range of the lock file on Windows systems.""" 

313 

314 def _acquire(self) -> None: 

315 raise NotImplementedError 

316 

317 def _release(self) -> None: 

318 raise NotImplementedError 

319 

320 

321__all__ = [ 

322 "WindowsFileLock", 

323]