GitHub Advisoryadvisory · PyPI
GHSA-cwqr-hh33-gh44 filelock ReadWriteLock: a crafted lock database makes every acquire stall or hang regardless of timeout
MEDIUM
Official page ↗
- Aliases
- CVE-2026-112915
- Ecosystem
- PyPI
- Packages
- filelock
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
- Weaknesses
- CWE-400
- Reviewed
- 2026-10-28
Affected packages
• PyPI filelock: >= 3.20.4, < 4.2.1 → fixed in 4.2.1
Description
`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, and for a read lock `SELECT name FROM sqlite_master LIMIT 1` at line 832. The code in `_do_acquire_inner` closes the previous connection and opens a new one on every acquire. The only content checks are `_open_lock_database` (regular file) and `_raise_if_wal` (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 r…
Related
Alias · CVE (1)
- CVE-2026-112915 filelock is a platform-independent file lock for Python. vulnerability
Weakness · CWE (1)
- CWE-400 Uncontrolled Resource Consumption weakness
References
- https://github.com/tox-dev/filelock/security/advisories/GHSA-cwqr-hh33-gh44 web
- https://nvd.nist.gov/vuln/detail/CVE-2026-112915 advisory
- https://github.com/tox-dev/filelock/pull/834 web
- https://github.com/tox-dev/filelock/commit/5e3f2ec0c5fb17cb2635b60b2b68bc63ab81ca2d web
- https://github.com/tox-dev/filelock package
- https://github.com/tox-dev/filelock/releases/tag/4.2.1 web