GitHub Advisoryadvisory · PyPI

GHSA-wfhq-q4j2-c37m filelock: SoftReadWriteLock follows a lock tree swapped in after its first acquire and writes lock records into directories of the user running the lock

LOW Published 2026-10-28 Modified 2026-10-28 Official page ↗
Aliases
CVE-2026-112941
Ecosystem
PyPI
Packages
filelock
Severity
Low
CVSS vector
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N
Weaknesses
CWE-367
Reviewed
2026-10-28

Affected packages

• PyPI filelock: >= 4.1.0, < 4.2.1 → fixed in 4.2.1

Description

`SoftReadWriteLock` (and `AsyncSoftReadWriteLock`, which wraps one) checks once per lock object, by name, that `.rw`, `.rw/gen` and `.rw/holders` are real directories and not symbolic links (files that redirect to another path). It then keeps only a boolean. Every later record operation resolves the full path again, and `O_NOFOLLOW` (a flag that makes an open fail when the final name is a symbolic link) protects the last component only. Another local user (the other user below) who can write the directory holding the lock, or who created `.rw` first in a sticky directory such as `/tmp` (the sticky bit is a directory flag that lets only a file's owner rename or remove it), can put a tree under that name whose `gen` or `holders` is a symbolic link into a directory of the user running the lock (the lock user below). A long-running process of the lock user that has already passed the check then creates, renames over and deletes files there with the lock user's privileges, including in `0700` directories the other user cannot open. The names and bytes are filelock's own lock records, not chosen by the other user. The scanner's reproducer replaces `.git/HEAD` in the lock user's private repository so that `git status` fails. There is no race. Affected versions: filelock >= 4.1.0 and .rw` aside and puts its tree in place. In a sticky parent such as `/tmp` the extra precondition is that it created `.rw` (with real `gen` and `holders` directories, mode `0777`) before the lock user's first acquire. `prepare()` accepts an existing tree whoever owns it, and the other user later replaces its own `gen` with a symbolic link. `fs.protected_symlinks=1` (a Linux setting that refuses to follow another user's symbolic link in a world-writable sticky directory) does not intervene, because the link sits inside the other user's own non-sticky `.rw`, not in the sticky parent (the scanner ran with that setting). • Lock user: any process that keeps one `SoftReadWriteLock` or `AsyncSoftReadWriteLock` object (a singleton per path by default) and acquires it more than once, or is waiting for it when the swap happens. A fresh process that meets the planted tree gets `RuntimeError`. Between two acquisitions there is no timing to win. • Targets: any directory the lock user can write, on any filesystem. The other user needs only to know its path (`/home/victim/project/.git` in the reproducer, under a `0700` home directory the other user cannot open). • Platform: observed on Linux. The code path is the same on every POSIX platform and on Windows, where a junction (a Windows directory that points at another directory) at a directory component plays the symbolic link's part. The scanner ran Linux only. • 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 is the case when each ancestor belongs to the lock user or root and is either not writable by others or sticky. A user who can rename entries in a non-sticky directory above the lock controls every name below it. No file-based lock can defend against that. Affected code At the scanned commit `b831496b0273`, `OsFiles.prepare` checks the three directories with `lstat` and caches `_prepared`. `lstat` reports what a name is without following a symbolic link. Every later operation takes a full path string, for example `create` (`O_NOFOLLOW` on the last component only), `replace` (`Path(source).replace(target)`), `unlink` and `listdir`. The callers that write through the swapped tree are `Participant.publish` / `_create_holder`, `GenerationLog._incarnation` (creates `epoch`), `commit` (links a generation record and renames over `HEAD`), `_keep_claim_fresh`, `_sweep` and `_list` (unlink). There is one `OsFiles` per lock object (`_sync.py` line 204), handed to every `Participant` at line 501. Introduced The once-per-object shortcut (`_prepared`) came with `367b111` ("fix(soft-rw): publish the head through gen/HEAD (#799)", 2026-10-08), first released in 4.1.0 (verified with `git tag --contains 367b111`, which answers 4.1.0). That commit opens the route between two acquisitions with no timing to win. The check-then-use gap itself is old…

Related

Alias · CVE (1)

  • CVE-2026-112941 filelock is a platform-independent file lock for Python. vulnerability

Weakness · CWE (1)

  • CWE-367 Time-of-check Time-of-use (TOCTOU) Race Condition weakness

References