CVE-2026-112911: filelock: unverified lock-dir ownership breaks mutual exclusion

GHSA-9c4x-gm8v-wgc4MEDIUM
Published October 28, 2026
CISO Take

filelock's StrictSoftFileLock and SoftReadWriteLock (3.30.0 through 4.2.0) adopt an existing lock directory without checking who owns it, so a local user who pre-creates that directory in a shared sticky location such as /tmp can silently put two processes of the lock user, root included, into the same critical section. The blast radius is wide (5,436 downstream dependents, and filelock is a transitive dependency of many ML tooling stacks, including model and dataset caching), but exploitation needs a local account on the same host and a new lock path, so the risk is concentrated on shared multi-tenant ML hosts, notebook servers, and CI runners. There is no KEV listing, no public exploit and no Nuclei template; EPSS is low in absolute terms (0.00148) but sits in the top 96th percentile. Upgrade to filelock 4.2.1, and until then keep these lock types out of /tmp and other shared writable directories.

Sources:NVDGitHub AdvisoryEPSS

What is the risk?

Medium (CVSS 6.3). Local-only, low privileges, high attack complexity: the attacker must create the protocol directory before the victim's first acquire on a given path. Once planted, no race is needed. Integrity and availability impact are high. The SoftReadWriteLock variant can also redirect file creates and renames made with the lock user's privileges (root for a root service) into attacker-chosen directories, and can hang or exhaust memory in the victim. There is no confidentiality impact. Exposure is limited to the 3.30.0-4.2.0 range and to the strict and soft read-write lock classes, not the default FileLock.

How does the attack unfold?

Initial Access
A local low-privileged user on a shared host pre-creates the lock's protocol directory in a sticky shared directory such as /tmp before the victim's first acquire.
AML.T0010.001
Exploitation
The lock adopts the attacker-owned directory without an ownership check, and the attacker deletes the holder's claim files or swaps gen/holders for a symlink.
Impact
Two victim processes enter the same critical section without any error, corrupting shared model or data artifacts, or the victim's file operations are redirected or stalled.
AML.T0029

What systems are affected?

PackageEcosystemVulnerable RangePatched
Anthropic Pythonpip>= 3.30.0, <= 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
6.3 / 10
EPSS
0.1%
chance of exploitation in 30 days
Higher than 4% of all CVEs
Exploitation Status
No known exploitation
Sophistication
Moderate

What is the attack surface?

AVACPRUISCIA
AVLocal
ACHigh
PRLow
UINone
SUnchanged
CNone
IHigh
AHigh

What should I do?

1 step
  1. 1) Upgrade filelock to 4.2.1 or later (fix in PR #839). 2) Inventory use of StrictSoftFileLock, AsyncStrictSoftFileLock, SoftReadWriteLock and AsyncSoftReadWriteLock with pip/SBOM tooling and code search; plain FileLock usage is not in scope for this advisory. 3) Workaround: place lock paths in directories that other users cannot write to, not /tmp or /var/tmp. 4) Where a shared directory is unavoidable, pre-create <path>.filelock/claims or <path>.rw (with gen and holders) as the service user at install or boot, and verify ownership with stat before first use. 5) Re-check after reboots, tmp cleaners or cache purges, which reopen the window. 6) Detection: alert on lock directories (*.filelock, *.rw) owned by a different UID than the service account, and investigate unexplained PermissionError on a new lock path. 7) On multi-tenant ML hosts, set per-user cache and temp directories (for example TMPDIR) so locks never live in a shared sticky directory.

How is it classified?

Which compliance frameworks are affected?

This CVE is relevant to:

ISO 42001
A.6.2.6-AI system operation and monitoring
NIST AI RMF
MANAGE 2.3-Responses to previously unknown or unmeasured risks are managed
OWASP LLM Top 10
LLM03-Supply Chain

Frequently Asked Questions

What is CVE-2026-112911?

filelock's StrictSoftFileLock and SoftReadWriteLock (3.30.0 through 4.2.0) adopt an existing lock directory without checking who owns it, so a local user who pre-creates that directory in a shared sticky location such as /tmp can silently put two processes of the lock user, root included, into the same critical section. The blast radius is wide (5,436 downstream dependents, and filelock is a transitive dependency of many ML tooling stacks, including model and dataset caching), but exploitation needs a local account on the same host and a new lock path, so the risk is concentrated on shared multi-tenant ML hosts, notebook servers, and CI runners. There is no KEV listing, no public exploit and no Nuclei template; EPSS is low in absolute terms (0.00148) but sits in the top 96th percentile. Upgrade to filelock 4.2.1, and until then keep these lock types out of /tmp and other shared writable directories.

Is CVE-2026-112911 actively exploited?

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

How to fix CVE-2026-112911?

1) Upgrade filelock to 4.2.1 or later (fix in PR #839). 2) Inventory use of StrictSoftFileLock, AsyncStrictSoftFileLock, SoftReadWriteLock and AsyncSoftReadWriteLock with pip/SBOM tooling and code search; plain FileLock usage is not in scope for this advisory. 3) Workaround: place lock paths in directories that other users cannot write to, not /tmp or /var/tmp. 4) Where a shared directory is unavoidable, pre-create <path>.filelock/claims or <path>.rw (with gen and holders) as the service user at install or boot, and verify ownership with stat before first use. 5) Re-check after reboots, tmp cleaners or cache purges, which reopen the window. 6) Detection: alert on lock directories (*.filelock, *.rw) owned by a different UID than the service account, and investigate unexplained PermissionError on a new lock path. 7) On multi-tenant ML hosts, set per-user cache and temp directories (for example TMPDIR) so locks never live in a shared sticky directory.

What systems are affected by CVE-2026-112911?

This vulnerability affects the following AI/ML architecture patterns: training pipelines, model serving, shared notebook environments, CI/CD ML pipelines, model and dataset caching.

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

CVE-2026-112911 has a CVSS v3.1 base score of 6.3 (MEDIUM). The EPSS exploitation probability is 0.15%.

What is the AI security impact?

Affected AI Architectures

training pipelinesmodel servingshared notebook environmentsCI/CD ML pipelinesmodel and dataset caching

MITRE ATLAS Techniques

AML.T0010.001AI Software
AML.T0029Denial of AI Service

Compliance Controls Affected

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

What are the technical details?

Original Advisory

`StrictSoftFileLock`, `AsyncStrictSoftFileLock`, `SoftReadWriteLock` and `AsyncSoftReadWriteLock` keep their whole ownership state as files in a directory derived from the lock path (`<path>.filelock/claims`, `<path>.rw/{gen,holders}`). When that directory already exists they adopt it after checking only that it is a real directory, not who owns it. Another local user who can create names in the lock's directory can create the tree before the first acquire on that path by the user running the lock (below: the lock user). In `/tmp`, `/var/tmp` or another sticky shared directory any user can do that; the sticky bit is a directory flag that lets only a file's owner remove or rename it. The other user then owns the tree. They can unlink the lock user's claims while the lock user holds the lock, so a second process of the lock user, root included, enters the same critical section and neither process sees an error. Against `SoftReadWriteLock` the same position also admits two writers with `on_compromise` silent. It redirects the lock user's own creates and renames into a directory the other user picks (the scanner replaced a repository's `.git/HEAD` with a filelock record written with the lock user's privileges). It also keeps the lock user's acquire or release running past any `timeout`. Affected versions are filelock 3.30.0 through 4.2.0 (finding `ANT-2026-Q5VK5JEE`). ## Severity **CVSS 3.1 `CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:H/A:H` scores 6.3 (Medium).** `AV:L` (local): the other user needs an account on the same host. `PR:L` (low privileges): an ordinary account is enough. `UI:N` (no user interaction). `AC:H` (high attack complexity): the other user must create the protocol directory before the lock user's first acquire on that path. That depends on the path being new, or on a reboot, a tmp cleaner or the documented `<path>.rw` recovery having removed its tree; once planted, the rest needs no race. `I:H` (high integrity impact): two holders write whatever the lock protects, and for `SoftReadWriteLock` the lock user's process creates, replaces or deletes files named `HEAD`, `epoch` and `0000000000000000000N` in any directory the lock user can write. `A:H` (high availability impact): the owner of `<path>.rw` can make the lock user's `read_lock(timeout=1)` spin at full CPU without end, cost 22 s per acquire, or pin 1 MiB of memory per planted file until a heartbeat thread dies of `MemoryError`. `C:N` (no confidentiality impact). `S:U` (scope unchanged). Classification from the review: **security, conditional** on the other user winning initial ownership of the protocol directory. The scanner rated it high (CWE-283). With `AC:L` the same impacts would score 7.1. ## Scope and preconditions - The other user can create names in the directory that holds the lock. In a sticky `1777` directory such as `/tmp` any local user can. The sticky bit stops that user from renaming or removing a directory the lock user created, but does nothing for one the other user created first. - The other user gets there first: `<path>.filelock` (strict) or `<path>.rw` (soft read-write) must not exist when the lock user's process first acquires. `_strict.py` does not remove the directory at any point, so a path the lock user has already used stays safe for as long as the tree exists. The exposure is a new lock path, or one whose tree a reboot, a tmp cleaner, a cache purge or the recovery described in `docs/how-to.rst` removed. - For the strict lock the planted directory must be writable by the lock user (`0777`); a planted `0755` directory makes the lock user's acquire fail with `PermissionError` instead. Planting only the outer `<path>.filelock` is enough. The lock user creates `claims` inside it, and the other user, as owner of the parent, renames that `claims` aside and creates an empty one. - The Linux `fs.protected_regular`, `fs.protected_symlinks`, `fs.protected_hardlinks` and `fs.protected_fifos` sysctls (kernel settings, all enabled in the scanner's image) do not interfere. They cover regular files, symlinks, hard links and FIFOs, not directories. The `SoftReadWriteLock` symlink redirect below also passes `fs.protected_symlinks=1`, because the symlink (a file that points at another path) sits in the other user's own non-sticky directory. - The affected classes are `StrictSoftFileLock` and `AsyncStrictSoftFileLock` (which inherits `_acquire`), and `SoftReadWriteLock` and `AsyncSoftReadWriteLock` with default options. `FileLock` gives way to the same position (when the other user creates the lock file first with mode `0666` and unlinks it during a hold, a second lock user process acquires), so the strict lock is not weaker than the native one here. This advisory reports it because `StrictSoftFileLock` is the lock documented as fail-closed and already raises `SoftFileLockProtocolError` for a symlink or a regular file at the same names, while it uses a directory that belongs to someone else without any signal. The `SoftReadWriteLock` half has no such counterpart: its redirected creates, renames and unlinks land outside the lock's own files. - A non-sticky directory writable by other users is outside the boundary below. There anyone who can create the directory can also rename the lock user's own tree aside, and no check inside the lock can tell that from a first use. These protections against another local user hold only when 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. A user who can rename entries in a non-sticky directory above the lock controls every name below it, which no file-based lock can defend against. ## Affected code At the scanned commit b831496: [`_ensure_protocol_directory`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_strict.py#L996-L1006) returns for any existing real directory, whatever its owner or mode. [`StrictSoftFileLock._acquire`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_strict.py#L127-L128) calls it for both levels and does not look at directory ownership again. [`_attempt_doorway`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_strict.py#L179-L181) decides from the claim scan alone, and [`_unlink_owner_path_result`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_strict.py#L896-L900) counts a missing claim as removed on release. The sibling site is [`OsFiles.prepare`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_soft_rw/_storage.py#L121-L133). Its `mkdir(mode=0o700)` takes effect only when it creates the directory, and its `lstat` check reads neither `st_uid` nor the permission bits. It runs once per instance, and every later `OsFiles` method takes a full path, with `O_NOFOLLOW` guarding only the last component. ## Introduced The strict site came with the claims protocol in 650c17c79c54 ("feat(strict): replace shared markers with owner claims", #659, 2026-07-14), first released in 3.30.0. The `SoftReadWriteLock` site came with the `<path>.rw` tree itself in f9743288a288 ("fix(soft-rw): replace the state mutex with a generation log", #738, 2026-09-17), first released in 4.0.0. Both match the git tags; the advisory range starts at the earlier one. ## Mechanism The owner of a directory can unlink and rename every entry in it, whatever the entries' own owner and mode. A strict hold consists of two names, `intent-v1-<token>.claim` and `held-v1-<token>.claim`, in `<path>.filelock/claims`. The sentinel (the lock file at `<path>`) is permanent and says nothing about who holds. When the other user runs the equivalent of `mkdir -m 777 -p work.lock.filelock/claims` before the lock user first uses `work.lock`, every lock user acquire publishes its claims into a directory the other user can empty. Lock user process A acquires and holds. The other user unlinks both of A's claims; both `unlink` calls succeed, because write permission on the directory is enough. Lock user process B calls `acquire()` while A is inside its critical section. `_attempt_doorway` starts with `if _read_existing_claims(...): return False`, which turns a contender away while a holder exists, but the scan is now empty. B publishes its own intent and held claims, sees only those in the later rescans, and wins. A is not told. A strict holder does not look at its claims between acquire and release, and on release `_unlink_owner_path_result` takes the `FileNotFoundError` for each missing claim as "already removed", so `_release` finishes without error. The break involves neither a `lifetime` nor a lease, and neither `break_lock()` nor `force_break()`. The benign path shows what the check is missing. When the lock user's first acquire creates the directories itself they are `0755` and owned by the lock user (umask `022`). The other user's `unlink` then fails with `EACCES`, its `rename` or `rmdir` of `work.lock.filelock` fails with `EPERM` under the sticky bit, and B raises `Timeout`. `OsFiles.prepare` has the same gap for `SoftReadWriteLock`. The code suppresses `EEXIST` from `mkdir(mode=0o700)`, and the check that follows looks only at `S_ISDIR` and `S_ISLNK`. The class docstring's protection against "same-host different-UID users via `0o600` / `0o700` permissions" therefore holds only for whoever gets there first. With the other user owning `/tmp/app.lock.rw`, `gen` and `holders` before the lock user first uses `SoftReadWriteLock("/tmp/app.lock")`, the scanner observed the following, with the same two accounts. **Creates and renames redirected through a symlink.** The other user's tree holds a generation log (the record of who holds the lock, under `gen/`) naming a live writer, so the lock user polls. Once the lock user's record appears in `holders/` (its `prepare()` is over), the other user renames `gen` aside and puts a symlink to `/home/victim/project/.git`, a repository inside a `0700` directory, in its place. The lock user's `write_lock()` returns without error. By then `GenerationLog.commit()` has created `epoch`, `00000000000000000001` and `00000000000000000002` in `.git` and renamed a record over `.git/HEAD`, which now reads `<epoch>\nfilelock-rw/1\ngeneration=2\n`, owned by the lock user; `git status` answers `fatal: not a git repository`. With `holders` swapped instead, `Participant._sweep()` unlinks files with 32-hex-digit names in the target directory once they have stayed unchanged for `stale_threshold`. This needs no race: every later acquisition on an instance that has passed `prepare()` goes through the swapped `gen`. **Two writers, no `on_compromise`.** `OsFiles.read()` accepts any regular file, so the owner of `gen` writes snapshots itself. While lock user process V1 is inside `write_lock()`, the other user creates the next `gen/<N>` naming no holder, and V2's `write_lock(timeout=2)` succeeds at once. A further forged generation naming both tokens keeps both heartbeats answering `ok`, because `Participant.heartbeat()` tests only `self.token in latest.members`; `compromise` was still `None` in both after the default 30 s heartbeat. The other user reads no lock user record at any point: listing `gen/` and `holders/` gives the generation and the tokens. **Hangs, memory growth and bare `OSError`s.** The protocol code treats every record in `gen/` and `holders/` as written by a cooperating peer, so the tree's owner can do several things. They can make `Participant.leave()` retry a refused commit without end (`read_lock(timeout=1)` spins at full CPU instead of raising `Timeout`). They can keep `GenerationLog._probe()` walking by appending records ahead of it (3 s of writes kept `read_lock(timeout=1)` busy for about 60 s). They can make every `acquire_write(timeout=1)` and `release()` on a free lock take about 22 s with 200 000 generation names hard-linked into a sticky `gen/` (a hard link is a second name for the same file), which `_list()` and `_compact_through()` walk again on each poll because `OsFiles.unlink()` swallows the `EPERM`. They can also pin 1 MiB of the lock user's memory per sparse file planted in `holders/`, because `Ledger.observe()` keeps every swept record whole (2 000 hard links to one 1 MiB file: 2 028 MiB resident when `acquire_write()` returns). Under an address-space limit the sweep raises `MemoryError` inside the heartbeat thread, which nothing handles, so the record is not refreshed again. After `stale_threshold` a second lock user process gets the write lock while the first still has `compromise` `None`. And they can make every acquire of a free lock raise a bare `OSError` with a mode-`000` orphan in `holders/` or a symlink at `gen/HEAD`. None of these is reachable in a tree the lock user created itself: it is `0700` at all three levels and the other user's writes fail with `EACCES`. ## Reproduction The scanner's reproducer for `ANT-2026-Q5VK5JEE`, quoted as received. It ran against commit `b831496b0273`. **`repro`** ```python #!/usr/bin/env python3 import os import pwd import subprocess import sys import tempfile # the companions are passed with -c, so victim and attacker need no read access to /tmp/repro.d victim_worker = open("/tmp/repro.d/victim_worker.py").read() attacker_plant_directory = open("/tmp/repro.d/attacker_plant_directory.py").read() attacker_remove_claims = open("/tmp/repro.d/attacker_remove_claims.py").read() as_victim = ["runuser", "-u", "victim", "--", sys.executable, "-c", victim_worker] as_attacker = ["runuser", "-u", "attacker", "--", sys.executable, "-c"] def two_victims_contend_while_attacker_removes_claims(lock_path): claims = f"{lock_path}.filelock/claims" first = subprocess.Popen([*as_victim, lock_path], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True) first_lines = [first.stdout.readline().strip()] claims_owner = pwd.getpwuid(os.stat(claims).st_uid).pw_name removal = subprocess.run([*as_attacker, attacker_remove_claims, lock_path], check=True, capture_output=True, text=True) # the first process has not been sent its line yet: it is still inside its critical section second = subprocess.Popen([*as_victim, lock_path], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True) second_lines = [second.stdout.readline().strip()] second_lines += second.communicate("\n")[0].split() first_lines += first.communicate("\n")[0].split() print(f" {claims} is owned by {claims_owner}") print(f" attacker, while the first victim process holds the lock: {'; '.join(removal.stdout.splitlines())}") print(f" first victim process: {first_lines}") print(f" second victim process, started while the first is inside: {second_lines}") return first_lines, second_lines # set up: a root-owned, sticky, world-writable directory, like /tmp itself shared = tempfile.mkdtemp(prefix="shared-", dir="/tmp") os.chmod(shared, 0o1777) # control: the victim's first acquire creates control.lock.filelock/claims itself print("CONTROL victim creates the protocol directories") _, control_second = two_victims_contend_while_attacker_removes_claims(f"{shared}/control.lock") # attack: the attacker creates work.lock.filelock/claims before the victim first uses work.lock print("OBSERVED attacker creates the protocol directories first") subprocess.run([*as_attacker, attacker_plant_directory, f"{shared}/work.lock"], check=True) first, second = two_victims_contend_while_attacker_removes_claims(f"{shared}/work.lock") # check: the second process must be kept out while the first is inside, as it is in the control round inside_and_released_cleanly = ["acquired", "is_locked=True", "released"] control_excludes = control_second == ["Timeout"] both_inside = first == second == inside_and_released_cleanly print(f"control_excludes={control_excludes} both_inside={both_inside}") print("BUG" if control_excludes and both_inside else "OK") ``` **`repro.d/attacker_plant_directory.py`** ```python import os import sys os.umask(0) # 0777: the victim must be able to publish its claims inside os.makedirs(f"{sys.argv[1]}.filelock/claims") ``` **`repro.d/attacker_remove_claims.py`** ```python import os import sys claims = f"{sys.argv[1]}.filelock/claims" for name in sorted(os.listdir(claims)): try: os.unlink(f"{claims}/{name}") print(f"removed {name}") except PermissionError as error: print(f"cannot remove {name}: {error.strerror}") ``` **`repro.d/victim_worker.py`** ```python import sys from filelock import SoftFileLockProtocolError, StrictSoftFileLock, Timeout lock = StrictSoftFileLock(sys.argv[1], timeout=1) try: lock.acquire() except (Timeout, SoftFileLockProtocolError) as error: print(type(error).__name__, flush=True) sys.exit() print("acquired", flush=True) sys.stdin.readline() # the critical section: it lasts until the parent sends a line print(f"is_locked={lock.is_locked}", flush=True) lock.release() print("released", flush=True) ``` ## Attack sequence ```mermaid sequenceDiagram participant X as other account (uid attacker) participant D as work.lock.filelock/claims (sticky /tmp) participant A as holder A (uid victim) participant B as contender B (uid victim) rect rgb(255, 214, 214) X->>D: mkdir -m 777 -p, before the lock user's first use A->>D: acquire(): directory exists, is a directory, adopted A->>D: publish intent-v1-<token>.claim and held-v1-<token>.claim X->>D: unlink both claims (owner of the directory may) B->>D: acquire(): scan finds no claims, publishes its own, wins Note over A,B: both inside, is_locked True in both A->>D: release(): claims missing, counted as removed, no error end Note over A,D: control: directory created by the lock user is 0755, the other account's unlink gets EACCES, B raises Timeout ``` ## Evidence The scanner's reproducer (`victim` uid 1000, `attacker` uid 1001, lock `work.lock` in a root-owned `1777` directory under `/tmp`, `StrictSoftFileLock(path, timeout=1)`, Linux 6.1, CPython 3.14.8, overlayfs) runs the same contention twice. In the control round on `control.lock`, whose directories `victim` creates, it prints `claims is owned by victim`, the unlinks by `attacker` answer `Permission denied`, the first process prints `['acquired', 'is_locked=True', 'released']` and the second `['Timeout']`. In the observed round on `work.lock`, planted by `attacker`, it prints `claims is owned by attacker` and `removed held-v1-….claim; removed intent-v1-….claim`, and both processes print `['acquired', 'is_locked=True', 'released']`, the second having acquired before the first was let out. The verdict line reads `control_excludes=True both_inside=True` and then `BUG` after about 2 s. The scanner ran the variant that plants only the outer `<path>.filelock` (then renames the `claims` of `victim` aside) by hand with the same result, and each `SoftReadWriteLock` effect listed under Mechanism in its own run with the same two accounts. Our review ran the reproducer against the 4.2.0 release image and the scanned commit (`BUG` on both) and against the head of #839 (`OK`). The review's regression tests for both sites need a sticky `/tmp` of another UID and ran there. ## Impact Another local user who can create names in the lock's directory puts two processes of the lock user, root included, into the same `StrictSoftFileLock` critical section at a moment of the other user's choosing, with no exception or other signal to either. Two processes then modify whatever the lock protects at the same time. Against `SoftReadWriteLock` the same position decides when two writers, or a writer and readers, are inside together and keeps `compromise` silent in all of them. It also gets file operations made with the lock user's privileges (root's, for a root service) in any directory the lock user can write: a file named `HEAD` replaced, files named `epoch` and `0000000000000000000N` created, files with 32-hex-digit names deleted. The bytes written are filelock's own records, not content the other user chooses. It can keep the lock user's acquire or release running past any `timeout`, or grow the lock user's memory by 1 MiB per planted file. It is **not** a disclosure of the lock user's data, and it does not reach a path the lock user has already used while that tree still exists. ## Workarounds Keep strict and soft read-write locks in a directory other users cannot write, rather than `/tmp` or another sticky shared directory. Where a shared directory is unavoidable, create `<path>.filelock/claims` (or `<path>.rw` with `gen` and `holders`) as the service user before any other account can, for example at install or boot, and check `stat -c %U` of an existing tree before the first use. A tmp cleaner, a reboot or the `<path>.rw` recovery removes the tree and reopens the window. The kernel refuses a planted `0755` strict tree (the lock user's acquire fails with `PermissionError`), so an acquire that raises that error on a new path without a known cause is worth investigating. ## Patched version 4.2.1. Fix: https://github.com/tox-dev/filelock/pull/839 ## Credits Reported by Anthropic's OSS Scanner (automated analysis), 15 October 2026; reviewed by the maintainers. - 2026-10-15 23:08 UTC: report received (finding `ANT-2026-Q5VK5JEE`, scanned commit b831496b0273) - 2026-10-16 00:46 UTC: draft advisory; later narrowed to this finding, with `ANT-2026-AD6SUOON`, `ANT-2026-SVFEYX89`, `ANT-2026-M9OPCLPB` and `ANT-2026-6BC3LKF9` moved to advisories of their own - 2026-10-16 01:31 UTC: fix PR #839 opened

Exploitation Scenario

On a shared ML training server, a low-privileged user watches for a new lock path used by a root-owned model-cache or checkpoint job in /tmp. Before the job's first run, they create <path>.filelock/claims with mode 0777. When the nightly job starts and holds the lock, the attacker deletes its claim files; a second instance of the same job, such as a retry or a parallel worker, then enters the critical section simultaneously with no error, corrupting a shared checkpoint or model cache. Alternatively, with a planted SoftReadWriteLock tree, the attacker swaps the gen directory for a symlink to a directory the root process can write, causing filelock record files to be created in or over files like .git/HEAD, or pins the job in a busy loop.

Weaknesses (CWE)

CWE-283 — Unverified Ownership: The product does not properly verify that a critical resource is owned by the proper entity.

  • [Architecture and Design, Operation] Very carefully manage the setting, management, and handling of privileges. Explicitly manage trust zones in the software.
  • [Architecture and Design] Consider following the principle of separation of privilege. Require multiple conditions to be met before permitting access to a system resource.

Source: MITRE CWE corpus.

CVSS Vector

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

Timeline

Published
October 28, 2026
Last Modified
October 28, 2026
First Seen
October 29, 2026

Related Vulnerabilities