Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/filelock/_unix.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class UnixFileLock(BaseFileLock):

def _acquire(self) -> None:
ensure_directory_exists(self.lock_file)
open_flags = os.O_RDWR | os.O_TRUNC
open_flags = os.O_RDWR | os.O_TRUNC | os.O_NOFOLLOW
if not Path(self.lock_file).exists():
open_flags |= os.O_CREAT
fd = os.open(self.lock_file, open_flags, self._context.mode)
Expand Down
38 changes: 38 additions & 0 deletions src/filelock/_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,52 @@
from ._util import ensure_directory_exists, raise_on_not_writable_file

if sys.platform == "win32": # pragma: win32 cover
import ctypes
import msvcrt
from ctypes import wintypes

# Windows API constants for reparse point detection
FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400
INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF

# Load kernel32.dll
_kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
_kernel32.GetFileAttributesW.argtypes = [wintypes.LPCWSTR]
_kernel32.GetFileAttributesW.restype = wintypes.DWORD

def _is_reparse_point(path: str) -> bool:
"""
Check if a path is a reparse point (symlink, junction, etc.) on Windows.

:param path: Path to check
:return: True if path is a reparse point, False otherwise
:raises OSError: If GetFileAttributesW fails for reasons other than file-not-found
"""
attrs = _kernel32.GetFileAttributesW(path)
if attrs == INVALID_FILE_ATTRIBUTES:
# File doesn't exist yet - that's fine, we'll create it
err = ctypes.get_last_error()
if err == 2: # noqa: PLR2004 # ERROR_FILE_NOT_FOUND
return False
if err == 3: # noqa: PLR2004 # ERROR_PATH_NOT_FOUND
return False
# Some other error - let caller handle it
return False
return bool(attrs & FILE_ATTRIBUTE_REPARSE_POINT)

class WindowsFileLock(BaseFileLock):
"""Uses the :func:`msvcrt.locking` function to hard lock the lock file on Windows systems."""

def _acquire(self) -> None:
raise_on_not_writable_file(self.lock_file)
ensure_directory_exists(self.lock_file)

# Security check: Refuse to open reparse points (symlinks, junctions)
# This prevents TOCTOU symlink attacks (CVE-TBD)
if _is_reparse_point(self.lock_file):
msg = f"Lock file is a reparse point (symlink/junction): {self.lock_file}"
raise OSError(msg)

flags = (
os.O_RDWR # open for read and write
| os.O_CREAT # create file if not exists
Expand Down