---
id: "CVE-2026-112937"
kind: "vulnerability"
aliases: ["CVE-2026-112937", "GHSA-jqc4-5cvw-7w85"]
title: "filelock: ReadWriteLock on the hard-link route deletes any directory another user renames onto its .filelock-* link directory name"
severity: "medium"
cvss: 6.1
epss: 0.00096
exploited: false
status: "open"
category: "package"
products: [{"name": "tox-dev filelock", "fixed": ["4.2.1"], "affected": ">= 4.1.0, < 4.2.1", "ecosystem": "vendor"}, {"name": "filelock", "fixed": ["4.2.1"], "affected": ">= 4.1.0, <= 4.2.0", "ecosystem": "pypi"}]
ecosystems: ["pypi", "vendor"]
tags: ["ci", "containers", "cwe-367", "filelock", "macos", "package", "python", "shared-hosts"]
not_affected_if: ["filelock-below-4.1.0-or-4.2.1-plus", "linux-with-proc-mounted", "single-user-host", "sticky-or-private-lock-directory"]
published_at: "2026-10-20T15:17:16+00:00"
first_seen_at: "2026-10-23T21:57:20+00:00"
updated_at: "2026-10-25T04:04:08+00:00"
sources: [{"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-112937", "first_seen_at": "2026-10-23T21:57:20+00:00"}, {"source": "ghsa", "url": "https://github.com/advisories/GHSA-jqc4-5cvw-7w85", "first_seen_at": "2026-10-23T22:35:39+00:00"}]
---

# filelock: ReadWriteLock on the hard-link route deletes any directory another user renames onto its .filelock-* link directory name

filelock's ReadWriteLock and AsyncReadWriteLock recursively delete a remembered .filelock-* directory without checking it is still the one they created. A local user with write access to a shared, non-sticky parent directory can rename a victim's directory onto that name, so the lock user's process deletes the victim's tree with the lock user's permissions.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- vendor:tox-dev filelock affected >= 4.1.0, < 4.2.1 fixed 4.2.1
- pypi:filelock affected >= 4.1.0, <= 4.2.0 fixed 4.2.1

## How to check

- Run `python -c "import filelock; print(filelock.__version__)"` and check whether the version is between `4.1.0` and `4.2.0` inclusive.
- Check whether ReadWriteLock databases sit in a shared directory that is `0777` or group-writable without the sticky bit, for example `ls -ld <lock-dir>`.
- On macOS, any ReadWriteLock use takes the affected route; on Linux, check whether `/proc/self/fd` is missing in the process (chroot or container without /proc).

## Fix

- filelock: upgrade to `4.2.1` (fixed in 4.2.1 per the sources).
- Until upgraded, keep ReadWriteLock databases out of shared non-sticky directories, or set the sticky bit on the parent (`chmod +t <dir>`).

## Timeline

- 2026-10-23T21:57:20+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-112937
- 2026-10-23T22:35:39+00:00 ghsa https://github.com/advisories/GHSA-jqc4-5cvw-7w85

## From the sources

`ReadWriteLock` and `AsyncReadWriteLock` (the SQLite-backed reader/writer lock) remove their private `.filelock-*` link directories with `shutil.rmtree` (a recursive delete) on a remembered path. They do not check that the path still names the directory they created. This happens on the hard-link route (a hard link is a second name for the same file). That route is every connection on macOS since 4.1.1 and any POSIX process without a usable `/proc/self/fd` (a Linux path that names the process's own open files), such as a chroot or container that does not mount `/proc`.

Another local user who can rename entries in the directory that holds the lock database can rename one of the directories of the user running the lock (below: the lock user) onto the link directory's name, or onto a forged dead-holder name. Such a lock directory is a `0777` or group-writable cache, build or project directory without the sticky bit (a directory flag that lets only a file's owner remove or rename it). The lock user's process then deletes that tree and everything in it with the lock user's permissions, and reports no error. Stock Linux with `/proc` mounted is not affected. Affected versions are filelock 4.1.0 through 4.2.0.

Anthropic's OSS Scanner reported this in private as `ANT-2026-LSSZIUEO` [high, CWE-367]: ReadWriteLock deletes any directory another user renames to .filelock-*.

## Severity

**CVSS 3.1 `CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L` 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): two `rename` calls and no race against filelock's own code. `I:H` (high integrity impact): the other user destroys a directory tree owned by the lock user, and the lock user's program reports no error. `A:L` (partial availability loss): the Python 3.11 `RecursionError` variant makes the lock user's first lock construction fail. `C:N` (no confidentiality impact): the other user learns nothing. `S:U` (scope unchanged): the effect stays within the lock user's own privileges.

Classification from the external review: **conditional security**, a cross-user rename in a non-sticky shared parent that turns private-link cleanup into recursive deletion of the lock user's tree. The scanner rated it high. The review accepts the mechanism, notes that the macOS route rests on a Linux run with `/proc` hidden rather than on a run on macOS, and narrows the finding to that precondition and route.

## Scope and preconditions

- The other party is a different local account with write permission on the directory that holds the lock database, where that directory has no sticky bit. That is a `0777` directory, or a group-writable one (in a setgid `2775` directory a group member can rename the lock user's sub-directory the same way). Renaming an entry needs write permission on the parent only, not on the entry. Anyone who can write the parent can rename a `0700` directory of the lock user.
- Sticky directories (`/tmp` itself, any `1777` directory) stop the release variant: the rename of an entry owned by the lock user fails with `EPERM`. The dead-owner sweep still reaches a lock user's tree that sits inside a directory the other account owns there. That account may rename its own directory to a forged dead-owner name, and the lock user's sweep deletes everything inside it. The Python 3.11 `RecursionError` variant also works in a sticky directory.
- Platform and route. The lock user must connect through the private hard link. That is every `ReadWriteLock` or `AsyncReadWriteLock` on macOS (since 4.1.1, #820), or a POSIX process whose `/proc/self/fd/<n>` (`/dev/fd/<n>` elsewhere) does not exist, such as a chroot or container without `/proc`. Linux with `/proc` mounted connects through the descriptor and creates no `.filelock-*` directory (checked: none appears, also with two holders in one process), so stock Linux is not affected at 4.2.0. The macOS statement rests on the `descriptor_usable` branch at line 984 and `docs/how-to.rst`. The reproducer runs the same Python code on Linux with `/proc` hidden, which takes root and is test scaffolding, not a step the other user performs. Windows does not take this route.
- Target set. Only directories that sit in the shared parent itself, or that the other account can already write, are reachable. The kernel refuses to move a `0700` directory of the lock user in from another parent. A tree the other account can already write (a group-writable project tree created with umask `002`) is no gain, since that account can delete it without filelock. A lock user running as root deletes a tree of any owner.
- Ancestor boundary. Protection against another local user is possible only while no other user can replace the lock's directory or any of its ancestors. That holds when each ancestor belongs to the user or root and is either not writable by others or sticky.

## Affected code

At the scanned commit `b831496b0273`: [`_ForkSafeConnection.close` runs `shutil.rmtree` on the remembered `mkdtemp` path](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_read_write.py#L290-L296). The path comes from [`retain_until_close`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_read_write.py#L305-L307), set on the [link route in `_connect_through_descriptor`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_read_write.py#L984-L1011), where [`descriptor_usable` is `False` on macOS](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_read_write.py#L984). Three more `rmtree` sites sit in [`_link_beside`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_read_write.py#L1014-L1029) (lines 1023 and 1026) and the connect's `finally` (line 1011). The fifth is in [`_sweep_links_of_the_dead`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_read_write.py#L1032-L1051), with the name parser [`_owner_is_gone`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_read_write.py#L1054-L1065) and [`_link_prefix`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_read_write.py#L1068-L1071).

## Introduced

The link directory moved beside the database in `015d27c` ("fix(read-write): reject WAL databases and link beside the db (#805)"), and the dead-owner sweep came with `df06bbd` (#815). Both were first released in **4.1.0** (verified: `git tag --contains` gives 4.1.0 for both; 4.0.11 and 4.0.12 made the link in `TMPDIR`). macOS takes the link route for every connection since `5fd0c8f` (#820), first released in **4.1.1** (verified).

## Mechanism

When the lock connects through its private hard link, it disposes of `.filelock-*` directories with `shutil.rmtree` on a path. Nothing ties that path to the directory filelock created. `mkdtemp` makes the name unpredictable and the directory `0700`. The name is in the parent's listing as soon as it exists (`ls -a`), and renaming it needs write permission on the parent only. `0700` keeps the other user out of the directory; it does not stop a rename of the directory. There are two ways in, with the same preconditions.

**On release.** A connection lives for one acquisition: `_open_for_acquisition` opens it and `release()` reaches `close` through `_release` and `_finish_connection`. The code trusts the path from the acquire to the release, for as long as the application keeps the lock. While the lock user holds `write_lock()` on `/tmp/shared-cache/cache.db`, the directory lists `.filelock-<pid>--<host digest>-<random>/` (`0700`, owned by the lock user, containing `lock.db`). The other user runs two renames: that directory to `moved-aside`, t

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
