CVE-2026-112913: filelock: Windows side-file planting breaks ReadWriteLock

GHSA-9vhv-jxww-xfm5LOW
Published October 23, 2026
CISO Take

filelock's SQLite-backed ReadWriteLock on Windows trusts side files (-wal, -journal) that sit next to its lock database. A second local account can plant one in a shared directory such as C:\Windows\Temp and make every lock acquire fail with an uncaught sqlite3.OperationalError. The rated impact is low (CVSS 3.3). It needs local access on the same Windows host and a shared writable directory, and it causes availability loss only. There is no KEV listing, no public exploit or Nuclei template, and no EPSS data. The package has about 6,390 dependents, but only Windows deployments that use ReadWriteLock in a shared directory are exposed, so real exposure is narrow. Upgrade filelock to 4.2.1 (affected: 3.20.4 through 4.2.0), and move lock databases to per-user directories such as %LOCALAPPDATA% or %TEMP% on multi-user Windows hosts.

Sources:NVDGitHub AdvisoryATLAS

What is the risk?

Low overall (CVSS 3.3, AV:L/AC:L/PR:L, availability only). The attacker needs a logon on the same host and a directory where they can create files but not delete other accounts' files. Exploitation is trivial once those conditions hold: create one file and set its ACL. A variant (CVSS 3.6) makes the lock user's process delete one file the attacker names, but it needs a lock database that already contains a table and a journal the lock user can write. Confidentiality is not affected and mutual exclusion still holds. No active exploitation is known (not in CISA KEV, no public exploit, EPSS N/A). The AI relevance is indirect: filelock is a general-purpose Python utility that many ML tools pull in as a dependency.

How does the attack unfold?

Initial Access
A low-privileged local account on a shared Windows host gains the ability to create files in a directory such as C:\Windows\Temp that holds the lock database.
AML.T0012
Side-File Planting
The attacker creates a <database>-wal (or -journal) file beside the lock database, grants everyone modify rights and denies DELETE, so the lock user cannot remove it.
Exploitation
The next ReadWriteLock acquire makes SQLite act on the foreign side file, raising an uncaught sqlite3.OperationalError (or replaying a hot journal).
Impact
Lock acquisitions fail on every call until the attacker or an administrator removes the file, stalling dependent AI/ML workloads.
AML.T0029

What systems are affected?

PackageEcosystemVulnerable RangePatched
Anthropic Pythonpip>= 3.20.4, <= 4.2.04.2.1
4.0K5.4K dependentsPushed 8d ago93% patched~9d to patchFull package profile →

Do you use Anthropic Python? You're affected.

How severe is it?

CVSS 3.1
3.3 / 10
EPSS
N/A
Exploitation Status
No known exploitation
Sophistication
Trivial

What is the attack surface?

AVACPRUISCIA
AVLocal
ACLow
PRLow
UINone
SUnchanged
CNone
INone
ALow

What should I do?

1 step
  1. 1) Upgrade filelock to 4.2.1 or later (fix in PR #854). 2) Until patched, keep ReadWriteLock databases in directories only the owning account can write, such as %LOCALAPPDATA%, the per-user %TEMP%, or a directory with inheritance removed and only the owner, SYSTEM and Administrators granted. Avoid C:\Windows\Temp and C:\Users\Public. 3) Detection: treat a persistent 'sqlite3.OperationalError: disk I/O error' on every acquire as a sign of a foreign <database>-wal beside the lock file. Look for unexpected -wal or -journal files, owned by other accounts, next to lock databases. Only the file's creator or an administrator can remove them. 4) Audit lock directories on shared Windows hosts for ACLs that let Users create files. 5) Use software composition analysis (SCA) to find filelock versions 3.20.4 to 4.2.0 in Windows build and runtime images.

How is it classified?

Which compliance frameworks are affected?

This CVE is relevant to:

ISO 42001
A.10.3-Suppliers
NIST AI RMF
MANAGE 2.3-Procedures to respond to and recover from unknown risks
OWASP LLM Top 10
LLM03-Supply Chain

Frequently Asked Questions

What is CVE-2026-112913?

filelock's SQLite-backed ReadWriteLock on Windows trusts side files (-wal, -journal) that sit next to its lock database. A second local account can plant one in a shared directory such as C:\Windows\Temp and make every lock acquire fail with an uncaught sqlite3.OperationalError. The rated impact is low (CVSS 3.3). It needs local access on the same Windows host and a shared writable directory, and it causes availability loss only. There is no KEV listing, no public exploit or Nuclei template, and no EPSS data. The package has about 6,390 dependents, but only Windows deployments that use ReadWriteLock in a shared directory are exposed, so real exposure is narrow. Upgrade filelock to 4.2.1 (affected: 3.20.4 through 4.2.0), and move lock databases to per-user directories such as %LOCALAPPDATA% or %TEMP% on multi-user Windows hosts.

Is CVE-2026-112913 actively exploited?

No confirmed active exploitation of CVE-2026-112913 has been reported, but organizations should still patch proactively.

How to fix CVE-2026-112913?

1) Upgrade filelock to 4.2.1 or later (fix in PR #854). 2) Until patched, keep ReadWriteLock databases in directories only the owning account can write, such as %LOCALAPPDATA%, the per-user %TEMP%, or a directory with inheritance removed and only the owner, SYSTEM and Administrators granted. Avoid C:\Windows\Temp and C:\Users\Public. 3) Detection: treat a persistent 'sqlite3.OperationalError: disk I/O error' on every acquire as a sign of a foreign <database>-wal beside the lock file. Look for unexpected -wal or -journal files, owned by other accounts, next to lock databases. Only the file's creator or an administrator can remove them. 4) Audit lock directories on shared Windows hosts for ACLs that let Users create files. 5) Use software composition analysis (SCA) to find filelock versions 3.20.4 to 4.2.0 in Windows build and runtime images.

What systems are affected by CVE-2026-112913?

This vulnerability affects the following AI/ML architecture patterns: training pipelines, model serving, agent frameworks, Python ML dependency chains on Windows.

What is the CVSS score for CVE-2026-112913?

CVE-2026-112913 has a CVSS v3.1 base score of 3.3 (LOW).

What is the AI security impact?

Affected AI Architectures

training pipelinesmodel servingagent frameworksPython ML dependency chains on Windows

MITRE ATLAS Techniques

AML.T0029Denial of AI Service

Compliance Controls Affected

ISO 42001: A.10.3
NIST AI RMF: MANAGE 2.3
OWASP LLM Top 10: LLM03

What are the technical details?

Original Advisory

On Windows, `ReadWriteLock` and `AsyncReadWriteLock` (the SQLite-backed reader/writer lock) connect SQLite to the lock database by its path, in every directory. SQLite derives three side files from that path: `<database>-journal`, the rollback journal (pages saved to undo an interrupted write); `<database>-wal`, the write-ahead log (changes kept outside the main file); and `<database>-shm`, the shared-memory index of the WAL. SQLite acts on whichever of these it finds beside the database before the lock runs any statement. Another local account that can create files in the database's directory, but not delete files of other accounts there, places such a side file beside the database and denies everyone `DELETE` on it, which a file's creator may do. `C:\Windows\Temp` (the temporary directory of services, granting `BUILTIN\Users` create but not delete) and `C:\Users\Public` are such directories. With a `-wal` in place, every `read_lock()` and `write_lock()` of the user running the lock (below: the lock user) raises `sqlite3.OperationalError: disk I/O error` at once, whatever `timeout` says, until that account removes its file; the lock user cannot. With a `-journal` beside a lock database that holds a table, SQLite replays the journal and deletes the file the journal names, a file of the lock user, and the acquire reports success. Affected versions are filelock 3.20.4 through 4.2.0 on Windows. The maintainers found this while reviewing the fix for the POSIX form of the defect, GHSA-v4x8-3wr5-3wq8, whose text says "Windows does not take this path" (review round 4, finding F2 of set D). It is not among the findings of the report Anthropic's OSS Scanner sent on 15 October 2026. ## Severity **CVSS 3.1 `CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L` scores 3.3 (Low).** `AV:L` (local): the other account needs a logon on the same host. `AC:L` (low attack complexity): create one file beside the database and set its ACL (access control list); no race, and the other account can place the file days earlier. `PR:L` (low privileges): an ordinary account that can create files in the directory is enough. `UI:N` (no user interaction). `S:U` (scope unchanged). `C:N` (no confidentiality impact): the other account learns nothing. `I:N` (no integrity impact in the scored variant): see the scoring note. `A:L` (partial availability impact): every acquire fails with an exception that `Timeout` handlers do not catch, until the other account removes its file; no crash, and the lock's own data is not changed. Scoring note. The deletion variant reads `CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:L` (3.6): `I:L` because the lock user's process deletes one file the journal names, `AC:H` because it needs a lock database that holds a table, which filelock does not create, and a journal the lock user can open for writing. The lower-reading rule of this advisory set scores the variant that reaches every `ReadWriteLock` on Windows in such a directory, 3.3, and describes the deletion variant under Mechanism. Classification from the review: **security** (another account), availability for the scored variant. ## Scope and preconditions - Platform and classes: Windows only. `ReadWriteLock` and `AsyncReadWriteLock`, both of which connect through `_connect`. On Linux and macOS the lock takes other routes, covered by GHSA-v4x8-3wr5-3wq8. - Directory: one where another account can create files but cannot delete the lock user's. `C:\Windows\Temp` carries `BUILTIN\Users:(CI)(S,WD,AD,X)` (create file and create subdirectory, no delete child); `C:\Users\Public` and any directory with such an entry qualify. A directory only the lock user can write, such as `%LOCALAPPDATA%` or the per-user temporary directory, is not affected. - The other account creates `<database>-wal`, grants everyone modify and denies everyone `DELETE` on it. A file's creator may set both. The lock user can open and read the file but not delete it. - Deletion variant: the lock database holds at least one table (filelock creates an empty database, and the review's reproducer has the lock account create the table), and the other account's `<database>-journal` grants the lock account modify rights. The journal's tail names a file the lock user can delete. - What the lock user sees: `write_lock(timeout=1)` raised `sqlite3.OperationalError: disk I/O error` with the undeletable `-wal` in place; with the writable journal the acquire succeeded and the named file was gone; with an undeletable journal every acquire raised `sqlite3.OperationalError: unable to open database file`. - The documentation at the scanned commit states "the lock directory must not be writable by untrusted users" for `WindowsFileLock`, in its symlink paragraph; the `ReadWriteLock` documentation names no directory requirement on Windows. ## Affected code Scanned commit `b831496b0273`: the [Windows branch of `_connect`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_read_write.py#L951-L965) checks the database through a handle and then calls `sqlite3.connect(database, ...)` with the path. [`_open_lock_database`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_read_write.py#L1079-L1091) and [`_raise_if_wal`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_read_write.py#L1094-L1101) inspect that handle, not the side files. The first statement is [`PRAGMA busy_timeout=...; PRAGMA journal_mode=MEMORY;` at L816](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_read_write.py#L816). [`_do_acquire_inner` at L763](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_read_write.py#L763) turns only `"database is locked"` into `Timeout`; any other `sqlite3.OperationalError` leaves the acquire. ## Introduced [cfd4446](https://github.com/tox-dev/filelock/commit/cfd4446) "feat(lock): add SQLite-based ReadWriteLock" ([#399](https://github.com/tox-dev/filelock/pull/399), 2026-02-12), first tag 3.20.4 (verified with `git tag --contains`), passed the lock path to `sqlite3.connect` on every platform. The Linux descriptor route (4.0.9) and the macOS hard-link route (4.1.1) did not change Windows, where the connect has been by name in every release since. The reproducer ran against the head of PR #834 at `1f9e415`, whose Windows branch of `_connect` is the same code as at `b831496b0273`. ## Mechanism `_connect` on Windows opens the database with `_open_non_reparse_fd`, checks through that handle that the file is a regular file, not a reparse point, and that its header does not declare WAL mode, and keeps the handle open while `sqlite3.connect(database)` opens the same name. The check covers the database, not the names beside it. On the first statement, `PRAGMA journal_mode=MEMORY`, SQLite takes its first lock on the database and looks for `<database>-wal`. A non-empty `-wal` beside a database that is not in WAL mode is a leftover SQLite deletes. The other account's file denies `DELETE`, so the pragma raises `SQLITE_IOERR_DELETE`, which reaches the caller as `sqlite3.OperationalError: disk I/O error`. `_do_acquire_inner` converts only "database is locked" into `Timeout`, so `acquire_read()`, `acquire_write()` and the async forms raise at once, on every call, until the file is gone. The lock user has no right to delete it. The journal variant needs a database with content. Beside a non-empty database SQLite treats a `<database>-journal` with a valid header as hot (left by an interrupted write) and replays it before the first statement runs. The review's journal is 46 bytes: a file path, its 4-byte length, the 4-byte sum of its bytes and SQLite's journal magic. That tail names a super-journal, and recovery ends by deleting the file it names. The lock user's `write_lock()` therefore deletes one file the other account chose, provided the lock user can delete it, and returns without error. SQLite opens a hot journal for writing, so the other account's journal must grant the lock account modify rights; a read-only journal makes every acquire raise `sqlite3.OperationalError: unable to open database file` instead. GHSA-v4x8-3wr5-3wq8 describes the same side-file behavior on POSIX, where the sticky bit plays the part the directory ACL plays here. ## Reproduction The review's reproducer, quoted as received except for the second account's password on line 2, replaced by `<password>`. It ran on a Windows 11 VM (NTFS, CPython 3.14, SQLite 3.53.1) against the head of PR #834 at `1f9e415`, as the administrator account `bernat`, which stands in for the other account that creates the side files; the lock account is `fltester`, a member of `Users`, whose database lives in `C:\Windows\Temp`. **`win_two_accounts.py`** ```python # Run from /tmp/claude-1000/-home-bernat-git-filelock-filelock-main/3ccf38c2-14f9-4acc-a05f-18230504d7cd/scratchpad/review4/D/834 (the script is staged there): # winvm up && winvm sync && winvm run "python3.14 -I win_two_accounts.py . fltester '<password>'" # which runs, as the administrator account on the VM: # python3.14 -I win_two_accounts.py <worktree> <second-user> <password> # The second (non-administrator) account is the lock user. Its database lives in C:\Windows\Temp, where every account # may create files but not delete another account's. This administrator account stands in for the other account that # plants SQLite side files there: it creates them, grants everyone read, and denies everyone delete, all of which the # file's creator may do. import ctypes import os import pathlib import sqlite3 import struct import subprocess import sys from ctypes import wintypes sys.path.insert(0, sys.argv[1] + "/src") from filelock import ReadWriteLock user, password = sys.argv[2], sys.argv[3] base = pathlib.Path(os.environ["SYSTEMROOT"]) / "Temp" icacls = os.path.join(os.environ["SYSTEMROOT"], "System32", "icacls.exe") tag = f"rw834-{os.getpid()}" advapi32 = ctypes.WinDLL("advapi32", use_last_error=True) advapi32.LogonUserW.argtypes = [wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, ctypes.POINTER(wintypes.HANDLE)] advapi32.GetUserNameW.argtypes = [wintypes.LPWSTR, ctypes.POINTER(wintypes.DWORD)] token = wintypes.HANDLE() if not advapi32.LogonUserW(user, ".", password, 3, 0, ctypes.byref(token)): raise ctypes.WinError(ctypes.get_last_error()) def current_user() -> str: size = wintypes.DWORD(256) buffer = ctypes.create_unicode_buffer(size.value) advapi32.GetUserNameW(buffer, ctypes.byref(size)) return buffer.value def as_lock_user(action): if not advapi32.ImpersonateLoggedOnUser(token): raise ctypes.WinError(ctypes.get_last_error()) try: return action() finally: advapi32.RevertToSelf() def plant(path: pathlib.Path, data: bytes, *, deletable: bool) -> None: path.write_bytes(data) subprocess.run([icacls, str(path), "/grant", "*S-1-1-0:(M)"], check=True, capture_output=True) if not deletable: subprocess.run([icacls, str(path), "/deny", "*S-1-1-0:(D)"], check=True, capture_output=True) def acquire(db: pathlib.Path) -> str: try: with ReadWriteLock(db, is_singleton=False).write_lock(timeout=1): pass except Exception as error: return f"acquire failed: {type(error).__name__}: {error}" return "acquired" def hot_journal(super_journal: pathlib.Path) -> bytes: name = os.fsencode(str(super_journal)) return name + struct.pack(">II", len(name), sum(name)) + bytes.fromhex("d9d505f920a163d7") print("planting account:", current_user(), "| lock account:", as_lock_user(current_user)) db = base / f"{tag}-empty.db" as_lock_user(lambda: db.touch()) plant(pathlib.Path(f"{db}-wal"), b"\0" * 32, deletable=False) print("empty database, undeletable -wal:", as_lock_user(lambda: acquire(db)), "| -wal still exists:", pathlib.Path(f"{db}-wal").exists()) for deletable in (True, False): db = base / f"{tag}-tables-{deletable}.db" notes = base / f"{tag}-notes-{deletable}.txt" def prepare() -> None: with sqlite3.connect(db) as connection: connection.execute("CREATE TABLE application (value)") connection.close() notes.write_text("keep") as_lock_user(prepare) plant(pathlib.Path(f"{db}-journal"), hot_journal(notes), deletable=deletable) print(f"database with a table, {'deletable' if deletable else 'undeletable'} -journal naming the lock account's file:", as_lock_user(lambda: acquire(db)), "| named file still exists:", notes.exists(), "| -journal still exists:", pathlib.Path(f"{db}-journal").exists()) ``` Recorded output on the VM: ```text planting account: bernat | lock account: fltester empty database, undeletable -wal: acquire failed: OperationalError: disk I/O error | -wal still exists: True database with a table, deletable -journal naming the lock account's file: acquired | named file still exists: False | -journal still exists: False database with a table, undeletable -journal naming the lock account's file: acquire failed: OperationalError: unable to open database file | named file still exists: True | -journal still exists: True ``` ## Failure sequence ```mermaid flowchart TD P["other account: create lock.db-wal (or lock.db-journal) beside lock.db in C:/Windows/Temp, grant everyone modify, deny everyone DELETE"] --> C["lock user: ReadWriteLock(lock.db).write_lock(timeout=1)"] C --> O["_open_lock_database checks the handle: regular file, no reparse point, header not WAL"] O --> S["sqlite3.connect(lock.db) by path, then PRAGMA journal_mode=MEMORY"] S -- "empty database, undeletable -wal" --> E1["SQLite cannot delete the leftover -wal: OperationalError disk I/O error on every acquire, timeout not consulted"] S -- "database with a table, writable -journal" --> E2["SQLite treats the journal as hot, replays it and deletes the file its tail names, acquire succeeds"] S -- "database with a table, read-only -journal" --> E3["OperationalError unable to open database file on every acquire"] classDef ok fill:#d8f3dc,stroke:#2d6a4f,color:#1b4332; classDef bad fill:#ffd6d6,stroke:#c1121f,color:#5c0000; classDef info fill:#cfe4ff,stroke:#1f6feb,color:#0b1f3a; class P,C,O,S info; class E1,E2,E3 bad; ``` ## Evidence The reproducer printed the three lines above on the VM: the undeletable `-wal` beside an empty database made `write_lock(timeout=1)` fail with `OperationalError: disk I/O error` and left the file in place; the writable journal beside a database with one table let the acquire succeed while the file the journal named, created by the lock account with the content `keep`, was gone along with the journal; the read-only journal made the acquire fail with `OperationalError: unable to open database file` and left both files. The review notes that the Windows branch of `_connect` is unchanged since #399, so the behavior is the same at `b831496b0273` and in 4.2.0. The scanner and the maintainers measured the same side-file effects on Linux for GHSA-v4x8-3wr5-3wq8. ## Impact In a directory such as `C:\Windows\Temp`, another local account turns every `read_lock()` and `write_lock()` on a `ReadWriteLock` database into an immediate `sqlite3.OperationalError`, for as long as its file stays. Callers that handle `Timeout` or `OSError` do not catch it, and the lock user cannot remove the file. With a lock database that holds a table and a journal the lock user can write, the lock user's process deletes one file of the other account's choosing, among the files the lock user may delete, and the acquire reports success. The lock's own database is not changed, nothing is read, and mutual exclusion between processes that acquire the lock holds. ## Workarounds - Keep the `ReadWriteLock` database in a directory other accounts cannot create files in: `%LOCALAPPDATA%`, the per-user temporary directory (`%TEMP%` of a user session), or a directory with inheritance removed and only the owner, `SYSTEM` and `Administrators` granted. `C:\Windows\Temp` and `C:\Users\Public` do not qualify. - Treat `sqlite3.OperationalError: disk I/O error` on every acquire as a foreign `<database>-wal` beside the database, which only its creator or an administrator can remove. ## Patched version 4.2.1. Fix: https://github.com/tox-dev/filelock/pull/854 ## Credits Found by the maintainers while reviewing the fix for the POSIX form of this defect, GHSA-v4x8-3wr5-3wq8, reported by Anthropic's OSS Scanner on 15 October 2026 (review round 4, set D, finding F2). - 2026-10-15 23:08 UTC: report received (36 findings; the Windows side-file effect is not among them) - 2026-10-16 01:15 UTC: PR #834 opened, for the POSIX routes - 2026-10-16 08:31 UTC: finding identified in the maintainers' review of PR #834 - 2026-10-16 22:43 UTC: draft advisory created

Exploitation Scenario

On a shared Windows server that runs ML services as a service account with lock files in C:\Windows\Temp, a low-privileged local user creates a lock.db-wal file next to the service's lock database. They grant everyone modify rights and deny DELETE, which a file's creator may do. Every read_lock() or write_lock() call by the service then fails at once with a disk I/O error that its Timeout handling does not catch. Model-cache refreshes or job coordination stop until an administrator removes the file. In a narrower variant, the attacker plants a writable hot -journal beside a lock database that holds a table, so that the service's process deletes one file the attacker named.

Weaknesses (CWE)

CWE-367 — Time-of-check Time-of-use (TOCTOU) Race Condition: The product checks the state of a resource before using that resource, but the resource's state can change between the check and the use in a way that invalidates the results of the check.

  • [Implementation] The most basic advice for TOCTOU vulnerabilities is to not perform a check before the use. This does not resolve the underlying issue of the execution of a function on a resource whose state and identity cannot be assured, but it does help to limit the false sense of security given by the check.
  • [Implementation] When the file being altered is owned by the current user and group, set the effective gid and uid to that of the current user and group when executing this statement.

Source: MITRE CWE corpus.

CVSS Vector

CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L

Timeline

Published
October 23, 2026
Last Modified
October 23, 2026
First Seen
October 24, 2026

Related Vulnerabilities