filelock, the Python file-locking library used by 5,436 downstream packages, has a flaw where its Unix lock empties and, with an explicit mode such as 0o666, chmods any regular file that another local user renames or hard-links onto the lock path. The rated severity is Medium (CVSS 6.1). The attacker needs a local account and a directory that is writable without the sticky bit (or fs.protected_hardlinks=0), so this matters mainly on shared hosts, multi-tenant ML build or training boxes and shared cache directories. Exploitation likelihood is low in absolute terms (EPSS 0.00212, though in the top 89th percentile), there is no public exploit or Nuclei template, and it is not in CISA KEV. Upgrade filelock to 4.2.1, keep lock files in directories no other account can write, avoid permissive explicit modes unless accounts really share the lock, and review shared-directory permissions on AI/ML hosts.
What is the risk?
Medium overall. Exploitation is easy once the preconditions hold (one rename or link, no race, no user interaction), but it requires local access plus a writable non-sticky directory or relaxed hard-link protection. Integrity impact is high because a file the lock user owns is truncated and, with a permissive mode, becomes writable by the other user. Confidentiality impact is limited to data written after the acquire. There is no availability impact and no effect on mutual exclusion. Exposure is highest on shared multi-user systems such as research clusters, shared notebooks and CI runners. Single-tenant containers are largely unaffected. No active exploitation is known.
How does the attack unfold?
What systems are affected?
| Package | Ecosystem | Vulnerable Range | Patched |
|---|---|---|---|
| Anthropic Python | pip | <= 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: PR #835) and rebuild images and lockfiles that pin earlier versions. 2) Keep lock files in directories that no other account can write, or in sticky directories on a filesystem where no directory holding the lock user's files is writable by others. 3) On Linux keep fs.protected_hardlinks=1, which closes the hard-link route. 4) Do not pass permissive explicit modes (0o666, 0o660, 0o644) unless accounts share the lock. 5) Use preserve_lock_file=True so an existing lock file in a sticky directory cannot be replaced. 6) Where flock semantics are not needed, consider SoftFileLock. 7) Detection: audit shared ML cache and work directories for world- or group-writable non-sticky permissions, and look for unexpected zero-length files or mode changes to *.lock paths and renames onto lock names. 8) Inventory with pip list or SBOM tooling for filelock <= 4.2.0.
How is it classified?
Which compliance frameworks are affected?
This CVE is relevant to:
Frequently Asked Questions
What is CVE-2026-112909?
filelock, the Python file-locking library used by 5,436 downstream packages, has a flaw where its Unix lock empties and, with an explicit mode such as 0o666, chmods any regular file that another local user renames or hard-links onto the lock path. The rated severity is Medium (CVSS 6.1). The attacker needs a local account and a directory that is writable without the sticky bit (or fs.protected_hardlinks=0), so this matters mainly on shared hosts, multi-tenant ML build or training boxes and shared cache directories. Exploitation likelihood is low in absolute terms (EPSS 0.00212, though in the top 89th percentile), there is no public exploit or Nuclei template, and it is not in CISA KEV. Upgrade filelock to 4.2.1, keep lock files in directories no other account can write, avoid permissive explicit modes unless accounts really share the lock, and review shared-directory permissions on AI/ML hosts.
Is CVE-2026-112909 actively exploited?
No confirmed active exploitation of CVE-2026-112909 has been reported, but organizations should still patch proactively.
How to fix CVE-2026-112909?
1) Upgrade filelock to 4.2.1 or later (fix: PR #835) and rebuild images and lockfiles that pin earlier versions. 2) Keep lock files in directories that no other account can write, or in sticky directories on a filesystem where no directory holding the lock user's files is writable by others. 3) On Linux keep fs.protected_hardlinks=1, which closes the hard-link route. 4) Do not pass permissive explicit modes (0o666, 0o660, 0o644) unless accounts share the lock. 5) Use preserve_lock_file=True so an existing lock file in a sticky directory cannot be replaced. 6) Where flock semantics are not needed, consider SoftFileLock. 7) Detection: audit shared ML cache and work directories for world- or group-writable non-sticky permissions, and look for unexpected zero-length files or mode changes to *.lock paths and renames onto lock names. 8) Inventory with pip list or SBOM tooling for filelock <= 4.2.0.
What systems are affected by CVE-2026-112909?
This vulnerability affects the following AI/ML architecture patterns: training pipelines, model serving, shared notebook environments, CI/CD build pipelines, model download and caching.
What is the CVSS score for CVE-2026-112909?
CVE-2026-112909 has a CVSS v3.1 base score of 6.1 (MEDIUM). The EPSS exploitation probability is 0.22%.
What is the AI security impact?
Affected AI Architectures
MITRE ATLAS Techniques
AML.T0010.001AI SoftwareAML.T0037Data from Local SystemCompliance Controls Affected
What are the technical details?
Original Advisory
`UnixFileLock`, the backend of `FileLock` and `AsyncFileLock` on Unix, empties whatever regular file it finds at the lock path once it holds the `flock` (a kernel lock on an open file). With an explicit `mode` it also chmods that file (sets its permission bits), whether or not this acquire created it. Another local user who can rename or hard-link one of the files of the user running the lock (below: the lock user) onto the lock path gets that file truncated by the lock user's next `acquire()`. A hard link is a second name for the same file. With a mode such as `0o666` the file, still owned by the lock user, also becomes readable and writable by that other user. The other user can then read and alter everything the lock user writes to it afterwards. `O_NOFOLLOW`, the defence added for the symlink variant (CVE-2025-68146), does not see a renamed file or a hard link. The rename route needs the file's directory to be writable without the sticky bit (a directory flag that lets only a file's owner remove or rename it). The hard-link route into a sticky directory such as `/tmp` needs `fs.protected_hardlinks=0`, a Linux kernel setting. Affected versions are every filelock release up to and including 4.2.0 (the truncation is as old as the project; the chmod since 3.10.2). Anthropic's OSS Scanner reported this in private. Finding covered by this advisory: - `ANT-2026-KO573NWI` [high, CWE-706]: UnixFileLock truncates and chmods victim files renamed onto the lock path. ## Severity **CVSS 3.1 `CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:N` scores 6.1 (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:L` (low attack complexity): one rename or link, then wait for the lock user's next acquire; no race. `I:H` (high integrity impact): the lock user's own process truncates a file it did not mean to touch, including content behind a second name the other user could neither modify nor delete. With a permissive explicit `mode` the other user can then write to it. If that file is one the lock user later executes or trusts, that is code execution as the lock user. `C:L` (partial confidentiality impact): the old content is gone before the other user can read anything, but what the lock user writes afterwards through a descriptor (the number the kernel gives a process for an open file) it still holds is readable. `A:N` (no availability impact). `S:U` (scope unchanged). Classification from the review: **security, conditional** on another user controlling names in a non-sticky directory on the lock's filesystem (or on `fs.protected_hardlinks=0`). The scanner rated this high. The maintainers score it medium because the reachable targets depend on the directory layout and kernel settings described below. ## Scope and preconditions - Lock classes: `FileLock` and `AsyncFileLock` on Unix (`UnixFileLock`). `SoftFileLock` applies an explicit mode only to a file it created with `O_CREAT | O_EXCL` and does not truncate an existing file. `WindowsFileLock` has not truncated at any version. - The other user must put a regular file of the lock user at the lock path before the lock user's next acquire, while the lock file does not exist yet. That is the case on first use, after a reboot, after a `/tmp` cleaner, or after a release with the default `preserve_lock_file=False`, which unlinks the file. The scanner checked three ways at the scanned commit, with default kernel settings unless stated: - rename inside a directory without the sticky bit (a group-writable project or cache directory, a root-created `0777` directory); - rename from such a directory into a sticky one (`1777`) on the same filesystem. The sticky bit only protects entries that already exist, so the move succeeds while the lock path is free. Once the lock user's lock file exists there, the same move fails with `EPERM`; - `link(2)` into a sticky directory. This needs a system that lets a user hard-link a file it cannot write, which on Linux means `fs.protected_hardlinks=0`. Mainstream distributions set 1, which refuses the link. The scanner did not test macOS or the BSDs. - What the lock user loses: with the default `FileLock(path)` the lock truncates the file, which keeps its mode. With an explicit `mode` (a lock shared between users needs `0o666` or `0o660`, because `UnixFileLock` opens the file read-write), the lock also chmods the file to that mode. `_apply_explicit_mode` suppresses `PermissionError` only for files the lock user does not own. The lock user owns its own files, so the `fchmod` succeeds. - Trust boundary: the protection against another local user holds only when no other user can rename entries in the lock's directory or, for the rename-into-sticky route, in any directory on the same filesystem that holds a file of the lock user. 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. - Not affected: a symlink (a file that points at another path) at the lock path, because `O_NOFOLLOW` makes the open fail with `ELOOP`; mutual exclusion between cooperating processes. ## Affected code Scanned commit `b831496b0273`: [`UnixFileLock._finalize_locked_fd`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_unix.py#L144-L163), whose [`os.ftruncate` and `_apply_explicit_mode` calls](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_unix.py#L150-L154) have only the `st_nlink != 0` guard (the file still has at least one name); [`_apply_explicit_mode`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_unix.py#L165-L168); [`_open_lock_file`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_unix.py#L95-L121), which learns from its `O_EXCL` create whether this attempt made the file but hands that flag only to the `ENOSYS` fallback. ## Introduced The truncation of whatever is at the lock path dates from the initial commit ([2157efb](https://github.com/tox-dev/filelock/commit/2157efb8b57666699fcf15bf03c3771c1571279c), which opened with `O_TRUNC`). The chmod of a pre-existing inode (the kernel's identity for a file, independent of its name) came with [d7e1e84](https://github.com/tox-dev/filelock/commit/d7e1e84e71ad) "changed from os.umask to os.chmod" ([#206](https://github.com/tox-dev/filelock/pull/206), 2023-03-22), first tag 3.10.2 (verified with `git tag --contains`). Before it the code passed the mode only to `open()`, which applies it to a file this call creates. The advisory range is therefore every release up to 4.2.0. The earlier group range `>= 3.10.0` understated it, as the review noted. ## Mechanism `_open_lock_file` first tries `os.open(path, O_RDWR | O_NOFOLLOW | O_CREAT | O_EXCL, mode)`. With the lock user's file renamed to the lock path that fails with `FileExistsError`. The second `os.open(path, O_RDWR | O_NOFOLLOW)` succeeds, because the entry is a regular file the lock user owns, and returns `created = False`. `_acquire_native` passes `created` only to the `ENOSYS` branch. No process holds a `flock` on the renamed file, so `_lock_fd_nonblocking(fd)` succeeds at once and `_finalize_locked_fd(fd)` runs: `os.fstat`, `st_nlink != 0`, `os.ftruncate(fd, 0)`, then `os.fchmod(fd, mode)` when the lock has an explicit mode. Nothing in that path tells a lock file from any other regular file; a link count of 2 and a non-empty file both pass. `O_NOFOLLOW` does not help because the entry is the file itself (or a hard link, an ordinary directory entry for the same inode), not a symlink to it. There is no race: the other user renames or links once and waits for the next acquire. `AsyncUnixFileLock` inherits `_finalize_locked_fd` unchanged. The scanner measured three variants at the same commit, same accounts (`victim` uid 1000, `attacker` uid 1001). First, a rename within a `0777` directory: the next `FileLock("jobs.lock", mode=0o666)` of `victim` empties the `0600` journal of `victim` renamed to `jobs.lock` and sets it to `0666`; `attacker` then reads what `victim` keeps writing through its open descriptor. Second, a rename of a two-name file (`ln /tmp/cache/blob ~/env/blob`, `0644`) from a `0777` directory, `fs.protected_hardlinks=1`: after `mv /tmp/cache/blob /tmp/cache/app.lock` and `FileLock("/tmp/cache/app.lock", mode=0o666)` as `victim`, `~/env/blob` has `2 links, mode 666, 0 bytes` and `echo pwned >> /home/victim/env/blob` as `attacker` succeeds, in a directory `attacker` cannot write. Third, `os.link(notes, lock_path)` in a `1777` directory with `fs.protected_hardlinks=0`: `notes.txt` (`0644`, in the home of `victim`) ends up 0 bytes with mode `0666`, and `open(notes, "w")` as `attacker` then succeeds. With plain `FileLock(lock_path)` a `0600` file ends up empty with its mode unchanged. The planted name stays after release, so every later acquire truncates the file again. ## Reproduction The scanner's reproducer for `ANT-2026-KO573NWI`, quoted as received. It ran against commit `b831496b0273`. **`repro`** ```python #!/usr/bin/env python3 import os import secrets import shutil import stat import subprocess import sys # set up: a directory every user may write, without the sticky bit shared = "/tmp/renamed-lock-file-demo" shutil.rmtree(shared, ignore_errors=True) os.mkdir(shared) os.chmod(shared, 0o777) journal = f"{shared}/victim-journal.log" lock_path = f"{shared}/jobs.lock" victim = subprocess.Popen( ["runuser", "-u", "victim", "--", sys.executable, "/tmp/repro.d/victim_job_runner.py", shared], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, ) def victim_runs(job): victim.stdin.write(job + "\n") victim.stdin.flush() assert victim.stdout.readline() == "done\n" def as_attacker(script, argument): command = ["runuser", "-u", "attacker", "--", sys.executable, f"/tmp/repro.d/{script}", argument] return subprocess.run(command, capture_output=True, text=True, check=True).stdout # before the attack: one job leaves the lock file and a 0600 journal with one line in it victim_runs("job 1") journal_before = os.stat(journal) journal_content_before = open(journal).read() attacker_read_before = as_attacker("attacker_read_file.py", journal) print( f"CONTROL journal: inode {journal_before.st_ino}, uid {journal_before.st_uid}, " f"mode {stat.S_IMODE(journal_before.st_mode):o}, content {journal_content_before!r}" ) print(f"CONTROL attacker reads the journal: {attacker_read_before!r}") # attack as_attacker("attacker_rename_journal.py", shared) # the victim takes its lock again, then notes a secret in its journal through the descriptor it holds secret = secrets.token_hex(8) victim_runs(f"job 2 {secret}") victim.stdin.close() victim.wait() # check: the victim's private journal must not be emptied, widened to 0666, or readable by the attacker lock_after = os.stat(lock_path) lock_content_after = open(lock_path).read() attacker_read_after = as_attacker("attacker_read_file.py", lock_path) print( f"OBSERVED lock path: inode {lock_after.st_ino}, uid {lock_after.st_uid}, " f"mode {stat.S_IMODE(lock_after.st_mode):o}, content {lock_content_after!r}" ) print(f"OBSERVED attacker reads the lock path: {attacker_read_after!r}") attacker_was_denied = attacker_read_before == "permission denied\n" lock_path_is_the_journal = (lock_after.st_dev, lock_after.st_ino) == (journal_before.st_dev, journal_before.st_ino) journal_was_emptied = "job 1" in journal_content_before and "job 1" not in lock_content_after journal_was_widened = stat.S_IMODE(journal_before.st_mode) == 0o600 and stat.S_IMODE(lock_after.st_mode) == 0o666 attacker_reads_the_secret = secret in attacker_read_after bug = ( attacker_was_denied and lock_path_is_the_journal and journal_was_emptied and journal_was_widened and attacker_reads_the_secret ) print("BUG" if bug else "OK") ``` **`repro.d/attacker_read_file.py`** ```python import sys try: print(open(sys.argv[1]).read(), end="") except PermissionError: print("permission denied") ``` **`repro.d/attacker_rename_journal.py`** ```python import os import sys shared_directory = sys.argv[1] # needs only write access to the directory: no sticky bit, so any user may rename any entry in it os.rename(f"{shared_directory}/victim-journal.log", f"{shared_directory}/jobs.lock") ``` **`repro.d/victim_job_runner.py`** ```python import os import sys from filelock import FileLock shared_directory = sys.argv[1] journal_fd = os.open(f"{shared_directory}/victim-journal.log", os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600) # UnixFileLock opens the lock file O_RDWR, so a lock that other users of the directory also take needs mode=0o666 lock = FileLock(f"{shared_directory}/jobs.lock", mode=0o666, timeout=5) for job in sys.stdin: with lock: os.write(journal_fd, f"ran {job}".encode()) print("done", flush=True) ``` ## Attack sequence ```mermaid sequenceDiagram participant A as other account participant D as shared directory (writable, not sticky) participant V as lock user: FileLock("jobs.lock", mode=0o666) participant K as kernel Note over V,D: victim-journal.log, owner victim, mode 0600 (the other account cannot read it) A->>D: mv victim-journal.log jobs.lock (one rename, no race) V->>K: open(jobs.lock, O_RDWR|O_NOFOLLOW|O_CREAT|O_EXCL) -> EEXIST V->>K: open(jobs.lock, O_RDWR|O_NOFOLLOW) -> fd (regular file, created=False) V->>K: flock(fd, LOCK_EX|LOCK_NB) -> ok (no holder) rect rgb(255, 214, 214) V->>K: fstat(fd), st_nlink != 0 V->>K: ftruncate(fd, 0) V->>K: fchmod(fd, 0o666) Note over V,K: the journal's inode is now empty and world read-write, still owned by victim end V->>K: write("ran job 2 <secret>") through the journal descriptor it still holds A->>D: read jobs.lock -> "ran job 2 <secret>" ``` ## Evidence The scanner reproducer (`victim` and `attacker` accounts, root-created `0777` directory without the sticky bit under `/tmp`, default sysctls, Linux 6.1, CPython 3.14.8, overlayfs; lock `FileLock(path, mode=0o666, timeout=5)`). It printed `CONTROL journal: inode 296383, uid 1000, mode 600, content 'ran job 1\n'` and `CONTROL attacker reads the journal: 'permission denied\n'`. After the rename and the second job of `victim` it printed `OBSERVED lock path: inode 296383, uid 1000, mode 666, content 'ran job 2 f4bc9a4e04c3f556\n'` and `OBSERVED attacker reads the lock path: 'ran job 2 f4bc9a4e04c3f556\n'`, last line `BUG`. The verdict requires that the earlier read by `attacker` failed, that the lock path is the journal's inode, that the old line is gone, that the mode went from `0600` to `0666`, and that the later read by `attacker` contains the secret. It runs in under a second with no race. The scanner ran the two-name and `link(2)` variants above by hand at the same commit (commands and results in its report), not through the script. Maintainer triage: the reproducer prints `BUG` on the scanned commit `b831496` and on the 4.2.0 release image. The independent review (`soft-open`) confirmed the mechanism and that the hard-link and two-name variants go through the same two calls. ## Impact The file's content is lost in every configuration, including for processes that hold the file open and under every other name the file has. With an explicit permissive `mode`, the other user can read and modify everything the lock user writes afterwards, while the file still belongs to the lock user. The other user can rename the file back to its old name after the acquire. It stays there with the lock's mode, and the lock user's next acquire creates a fresh lock file. Somebody who may rename a single-name file may also unlink it, so losing such a file under its name is not new. What the lock adds are two operations on the inode that the other user cannot perform, `ftruncate` and `fchmod`, run with the lock user's privileges. Those matter for a descriptor the lock user still holds, for a second name of the file, and for programs that trust the file's ownership. The other user does not choose the mode, and the lock user writes no bytes the other user chose. Mutual exclusion is not affected. ## Workarounds - Keep the lock file in a directory no other account can write, or in a sticky directory on a filesystem where no directory holding the lock user's files is writable by others. On Linux keep `fs.protected_hardlinks=1` (the default on mainstream distributions), which closes the hard-link route. - Do not pass a permissive explicit `mode` (`0o666`, `0o660`, `0o644`) unless accounts share the lock; without a mode the exposure is the truncation only. - Keep the lock file in place (`preserve_lock_file=True`). Once a lock file owned by the lock user exists in a sticky directory, another user cannot rename or link over it. - Where the lock does not need `flock` semantics, `SoftFileLock` applies its mode only to the marker it created with `O_EXCL` and does not truncate an existing file. ## Patched version 4.2.1. Fix: https://github.com/tox-dev/filelock/pull/835 ## Credits Reported by Anthropic's OSS Scanner (automated analysis), 15 October 2026; reviewed by the maintainers. - 2026-10-15 23:08 UTC: report received (36 findings; this one rated high by the scanner) - 2026-10-16 00:46 UTC: draft advisory created (first as a group of four findings, then narrowed to `ANT-2026-KO573NWI`; the other three security findings have their own advisories) - 2026-10-16 01:16 UTC: fix PR #835 opened
Exploitation Scenario
On a shared GPU server, a low-privileged researcher notices that a colleague's training job takes a lock named jobs.lock with mode=0o666 in a group-writable project directory that lacks the sticky bit. The attacker renames the colleague's private 0600 run journal (or a script or config the colleague later uses) to jobs.lock. On the next job start, the colleague's filelock truncates the file and sets it to 0666. The attacker then reads everything the job subsequently writes through its open descriptor, or modifies a file the colleague later executes or trusts, which can lead to code execution under the colleague's account.
Weaknesses (CWE)
CWE-706 — Use of Incorrectly-Resolved Name or Reference: The product uses a name or reference to access a resource, but the name/reference resolves to a resource that is outside of the intended control sphere.
Source: MITRE CWE corpus.
CVSS Vector
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:NReferences
Timeline
Related Vulnerabilities
CVE-2026-9460310.0Podman: checkpoint images override sandbox on run
Same package: anthropicCVE-2026-544498.8LangBot: RCE via arbitrary STDIO MCP command
Same package: anthropicCVE-2026-277758.8Gitea: cached permission check allows repo takeover
Same package: anthropicCVE-2026-554298.7Coder: cross-workspace agent hijack via app ID reuse
Same package: anthropicCVE-2026-75748.7Claude Desktop: VM integrity bypass enables RCE
Same package: anthropic