| from __future__ import annotations |
|
|
| import os |
| import sys |
| from contextlib import suppress |
| from pathlib import Path |
| from typing import Final, cast |
|
|
| from ._api import BaseFileLock |
| from ._util import ensure_directory_exists, raise_on_not_writable_file |
|
|
| if sys.platform == "win32": |
| import ctypes |
| import msvcrt |
| from ctypes import wintypes |
|
|
| _GENERIC_READ: Final[int] = 0x80000000 |
| _GENERIC_WRITE: Final[int] = 0x40000000 |
| _SYNCHRONIZE: Final[int] = 0x00100000 |
| _DESIRED_ACCESS: Final[int] = _GENERIC_READ | _GENERIC_WRITE | _SYNCHRONIZE |
| _FILE_SHARE_READ_WRITE: Final[int] = ( |
| 0x00000001 | 0x00000002 |
| ) |
| _FILE_OPEN_IF: Final[int] = 3 |
| _FILE_ATTRIBUTE_READONLY: Final[int] = 0x00000001 |
| _FILE_ATTRIBUTE_NORMAL: Final[int] = 0x00000080 |
| _FILE_ATTRIBUTE_REPARSE_POINT: Final[int] = 0x00000400 |
| |
| |
| _FILE_SYNCHRONOUS_IO_NONALERT: Final[int] = 0x00000020 |
| _FILE_NON_DIRECTORY_FILE: Final[int] = 0x00000040 |
| _FILE_OPEN_REPARSE_POINT: Final[int] = 0x00200000 |
| _CREATE_OPTIONS: Final[int] = _FILE_SYNCHRONOUS_IO_NONALERT | _FILE_NON_DIRECTORY_FILE | _FILE_OPEN_REPARSE_POINT |
| _OBJ_CASE_INSENSITIVE: Final[int] = 0x00000040 |
| _OWNER_WRITE: Final[int] = 0o200 |
|
|
| |
| |
| |
| |
| _LOCKFILE_FAIL_IMMEDIATELY: Final[int] = 0x00000001 |
| _LOCKFILE_EXCLUSIVE_LOCK: Final[int] = 0x00000002 |
| _ERROR_LOCK_VIOLATION: Final[int] = 33 |
|
|
| |
| |
| |
| _STATUS_SUCCESS: Final[int] = 0x00000000 |
| _STATUS_ACCESS_DENIED: Final[int] = 0xC0000022 |
| _STATUS_SHARING_VIOLATION: Final[int] = 0xC0000043 |
| _STATUS_DELETE_PENDING: Final[int] = 0xC0000056 |
|
|
| _ntdll: Final[ctypes.WinDLL] = ctypes.WinDLL("ntdll") |
| _kernel32: Final[ctypes.WinDLL] = ctypes.WinDLL("kernel32", use_last_error=True) |
|
|
| class _UNICODE_STRING(ctypes.Structure): |
| _fields_ = ( |
| ("Length", wintypes.USHORT), |
| ("MaximumLength", wintypes.USHORT), |
| ("Buffer", wintypes.LPWSTR), |
| ) |
|
|
| class _OBJECT_ATTRIBUTES(ctypes.Structure): |
| _fields_ = ( |
| ("Length", wintypes.ULONG), |
| ("RootDirectory", wintypes.HANDLE), |
| ("ObjectName", ctypes.POINTER(_UNICODE_STRING)), |
| ("Attributes", wintypes.ULONG), |
| ("SecurityDescriptor", ctypes.c_void_p), |
| ("SecurityQualityOfService", ctypes.c_void_p), |
| ) |
|
|
| class _IO_STATUS_BLOCK(ctypes.Structure): |
| _fields_ = ( |
| ("Status", ctypes.c_void_p), |
| ("Information", ctypes.c_void_p), |
| ) |
|
|
| class _OVERLAPPED(ctypes.Structure): |
| _fields_ = ( |
| ("Internal", ctypes.c_void_p), |
| ("InternalHigh", ctypes.c_void_p), |
| ("Offset", wintypes.DWORD), |
| ("OffsetHigh", wintypes.DWORD), |
| ("hEvent", wintypes.HANDLE), |
| ) |
|
|
| class _BY_HANDLE_FILE_INFORMATION(ctypes.Structure): |
| _fields_ = ( |
| ("dwFileAttributes", wintypes.DWORD), |
| ("ftCreationTime", wintypes.FILETIME), |
| ("ftLastAccessTime", wintypes.FILETIME), |
| ("ftLastWriteTime", wintypes.FILETIME), |
| ("dwVolumeSerialNumber", wintypes.DWORD), |
| ("nFileSizeHigh", wintypes.DWORD), |
| ("nFileSizeLow", wintypes.DWORD), |
| ("nNumberOfLinks", wintypes.DWORD), |
| ("nFileIndexHigh", wintypes.DWORD), |
| ("nFileIndexLow", wintypes.DWORD), |
| ) |
|
|
| _ntdll.NtCreateFile.restype = wintypes.LONG |
| _ntdll.NtCreateFile.argtypes = [ |
| ctypes.POINTER(wintypes.HANDLE), |
| wintypes.DWORD, |
| ctypes.POINTER(_OBJECT_ATTRIBUTES), |
| ctypes.POINTER(_IO_STATUS_BLOCK), |
| ctypes.POINTER(ctypes.c_longlong), |
| wintypes.ULONG, |
| wintypes.ULONG, |
| wintypes.ULONG, |
| wintypes.ULONG, |
| ctypes.c_void_p, |
| wintypes.ULONG, |
| ] |
| _ntdll.RtlDosPathNameToNtPathName_U_WithStatus.restype = wintypes.LONG |
| _ntdll.RtlDosPathNameToNtPathName_U_WithStatus.argtypes = [ |
| wintypes.LPCWSTR, |
| ctypes.POINTER(_UNICODE_STRING), |
| ctypes.c_void_p, |
| ctypes.c_void_p, |
| ] |
| _ntdll.RtlFreeUnicodeString.restype = None |
| _ntdll.RtlFreeUnicodeString.argtypes = [ctypes.POINTER(_UNICODE_STRING)] |
| _ntdll.RtlNtStatusToDosError.restype = wintypes.ULONG |
| _ntdll.RtlNtStatusToDosError.argtypes = [wintypes.LONG] |
|
|
| _kernel32.CloseHandle.argtypes = [wintypes.HANDLE] |
| _kernel32.CloseHandle.restype = wintypes.BOOL |
| _kernel32.GetFileInformationByHandle.argtypes = [wintypes.HANDLE, ctypes.POINTER(_BY_HANDLE_FILE_INFORMATION)] |
| _kernel32.GetFileInformationByHandle.restype = wintypes.BOOL |
| _kernel32.LockFileEx.argtypes = [ |
| wintypes.HANDLE, |
| wintypes.DWORD, |
| wintypes.DWORD, |
| wintypes.DWORD, |
| wintypes.DWORD, |
| ctypes.POINTER(_OVERLAPPED), |
| ] |
| _kernel32.LockFileEx.restype = wintypes.BOOL |
| _kernel32.UnlockFileEx.argtypes = [ |
| wintypes.HANDLE, |
| wintypes.DWORD, |
| wintypes.DWORD, |
| wintypes.DWORD, |
| ctypes.POINTER(_OVERLAPPED), |
| ] |
| _kernel32.UnlockFileEx.restype = wintypes.BOOL |
|
|
| def _lock_fd_nonblocking(fd: int) -> bool: |
| |
| |
| |
| overlapped = _OVERLAPPED() |
| flags = _LOCKFILE_EXCLUSIVE_LOCK | _LOCKFILE_FAIL_IMMEDIATELY |
| if _kernel32.LockFileEx(msvcrt.get_osfhandle(fd), flags, 0, 1, 0, ctypes.byref(overlapped)): |
| return True |
| err = ctypes.get_last_error() |
| if err == _ERROR_LOCK_VIOLATION: |
| return False |
| |
| raise ctypes.WinError(err) |
|
|
| def _unlock_fd(fd: int) -> None: |
| overlapped = _OVERLAPPED() |
| |
| if not _kernel32.UnlockFileEx(msvcrt.get_osfhandle(fd), 0, 1, 0, ctypes.byref(overlapped)): |
| raise ctypes.WinError(ctypes.get_last_error()) |
|
|
| class WindowsFileLock(BaseFileLock): |
| """ |
| Uses ``LockFileEx`` to hard lock a byte range of the lock file on Windows systems. |
| |
| Lock file cleanup: Windows attempts to delete the lock file after release, but deletion is |
| not guaranteed in multi-threaded scenarios where another thread holds an open handle. The lock |
| file may persist on disk, which does not affect lock correctness. |
| """ |
|
|
| def _acquire(self) -> None: |
| raise_on_not_writable_file(self.lock_file) |
| ensure_directory_exists(self.lock_file) |
|
|
| |
| |
| fd = _open_non_reparse_fd(self.lock_file, self._open_mode()) |
| if fd is None: |
| return |
| try: |
| locked = _lock_fd_nonblocking(fd) |
| if locked: |
| self._mark_descriptor_owned(fd) |
| except BaseException: |
| os.close(fd) |
| raise |
| if not locked: |
| os.close(fd) |
|
|
| def _release(self) -> None: |
| fd = cast("int", self._context.lock_file_fd) |
| |
| |
| |
| _unlock_fd(fd) |
| self._mark_descriptor_released() |
| self._close_released_fd(fd, default_suppresses=False) |
| if not self._preserve_lock_file: |
| with suppress(OSError): |
| Path(self.lock_file).unlink() |
|
|
| def _open_non_reparse_fd(path: str, mode: int) -> int | None: |
| """ |
| Open *path* for locking while refusing reparse points, bound to the handle actually locked. |
| |
| The file is opened through ``NtCreateFile`` with ``FILE_OPEN_REPARSE_POINT`` so a symlink or junction planted |
| at the path is not followed, and the reparse decision is read from *that* handle via |
| ``GetFileInformationByHandle`` rather than from a prior pathname query. Reading the held handle closes the |
| check-then-open race: an attacker cannot swap the path between validation and use because both act on the same |
| handle. Share mode omits delete so a peer cannot unlink or rename the file out from under a live holder, |
| matching ``os.open``'s ``_SH_DENYNO``. |
| |
| ``NtCreateFile`` is used instead of ``CreateFileW`` because its return value carries the raw ``NTSTATUS``. |
| Windows collapses a transient delete-pending name and a permanent access denial into the same Win32 |
| ``ERROR_ACCESS_DENIED``; the status keeps them apart, so a real denial fails fast instead of spinning until the |
| caller's timeout (#604). |
| |
| The reparse option only guards the final path component; Windows still follows reparse points in intermediate |
| directories. This assumes the lock file sits in a lock directory untrusted users cannot modify. A path with |
| attacker-controlled parent directories would need component-by-component handle validation. |
| |
| :param path: the lock file path. |
| :param mode: the permission mode; as ``os.open`` does on Windows, a cleared owner-write bit creates the file |
| read-only. The attribute only takes effect when the file is created, not when an existing one is opened. |
| |
| :returns: a file descriptor owning the opened handle, or ``None`` on a sharing violation or a delete-pending |
| name the caller should treat as contention and retry. |
| |
| :raises OSError: if the path resolves to a reparse point, or the open fails for any other reason, raised with |
| the Win32 error the status maps to. |
| |
| """ |
| |
| sys.audit("open", path, None, os.O_RDWR | os.O_CREAT) |
| handle, status = _nt_open(path, read_only=not mode & _OWNER_WRITE) |
| if status != _STATUS_SUCCESS: |
| if status in {_STATUS_SHARING_VIOLATION, _STATUS_DELETE_PENDING}: |
| return None |
| winerror = _ntdll.RtlNtStatusToDosError(status) |
| raise OSError(None, ctypes.FormatError(winerror).strip(), path, winerror) |
|
|
| info = _BY_HANDLE_FILE_INFORMATION() |
| |
| if not _kernel32.GetFileInformationByHandle(handle, ctypes.byref(info)): |
| err = ctypes.get_last_error() |
| _kernel32.CloseHandle(handle) |
| raise ctypes.WinError(err) |
| if info.dwFileAttributes & _FILE_ATTRIBUTE_REPARSE_POINT: |
| _kernel32.CloseHandle(handle) |
| msg = f"Lock file is a reparse point (symlink/junction): {path}" |
| raise OSError(msg) |
|
|
| try: |
| |
| return msvcrt.open_osfhandle(handle, os.O_RDWR | os.O_NOINHERIT) |
| except BaseException: |
| _kernel32.CloseHandle(handle) |
| raise |
|
|
| def _nt_open(path: str, *, read_only: bool) -> tuple[int, int]: |
| """ |
| Open *path* through ``NtCreateFile`` and return ``(handle, status)``. |
| |
| ``RtlDosPathNameToNtPathName_U_WithStatus`` translates the Win32 path to the NT namespace, handling relative, |
| drive, UNC and extended-length path forms as Win32 itself would, and allocates a buffer that |
| ``RtlFreeUnicodeString`` releases. The handle is ``0`` unless the status is ``STATUS_SUCCESS``. |
| """ |
| nt_name = _UNICODE_STRING() |
| status = _ntdll.RtlDosPathNameToNtPathName_U_WithStatus(path, ctypes.byref(nt_name), None, None) & 0xFFFFFFFF |
| if status != _STATUS_SUCCESS: |
| return 0, status |
| try: |
| attributes = _OBJECT_ATTRIBUTES() |
| attributes.Length = ctypes.sizeof(_OBJECT_ATTRIBUTES) |
| attributes.ObjectName = ctypes.pointer(nt_name) |
| attributes.Attributes = _OBJ_CASE_INSENSITIVE |
| handle = wintypes.HANDLE() |
| io_status = _IO_STATUS_BLOCK() |
| status = ( |
| _ntdll.NtCreateFile( |
| ctypes.byref(handle), |
| _DESIRED_ACCESS, |
| ctypes.byref(attributes), |
| ctypes.byref(io_status), |
| None, |
| _FILE_ATTRIBUTE_READONLY if read_only else _FILE_ATTRIBUTE_NORMAL, |
| _FILE_SHARE_READ_WRITE, |
| _FILE_OPEN_IF, |
| _CREATE_OPTIONS, |
| None, |
| 0, |
| ) |
| & 0xFFFFFFFF |
| ) |
| finally: |
| _ntdll.RtlFreeUnicodeString(ctypes.byref(nt_name)) |
| if status != _STATUS_SUCCESS: |
| return 0, status |
| return handle.value or 0, status |
|
|
| else: |
|
|
| class WindowsFileLock(BaseFileLock): |
| """Uses ``LockFileEx`` to hard lock a byte range of the lock file on Windows systems.""" |
|
|
| def _acquire(self) -> None: |
| raise NotImplementedError |
|
|
| def _release(self) -> None: |
| raise NotImplementedError |
|
|
|
|
| __all__ = [ |
| "WindowsFileLock", |
| ] |
|
|