GitHub Advisoryadvisory · PyPI

GHSA-fq56-3cx3-fjxp filelock: SoftFileLock stale-lock break removes a live lock file on a reused inode number and admits two holders

MEDIUM Published 2026-10-28 Modified 2026-10-28 Official page ↗
Aliases
CVE-2026-112935
Ecosystem
PyPI
Packages
filelock
Severity
Moderate
CVSS vector
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:H/A:N
Weaknesses
CWE-367
Reviewed
2026-10-28

Affected packages

• PyPI filelock: >= 3.22.0, < 4.2.1 → fixed in 4.2.1

Description

The stale-lock break shared by `SoftFileLock`, `MarkerSoftFileLock` and `SoftFileLease` (and their async variants, and a `FileLock` that has fallen back to a soft lock on `ENOSYS`, the "not implemented" error) recognises the marker it judged stale by two values. The marker is the lock file at ``, whose presence says a process holds the lock. The values are its inode number (the kernel's identity for a file, independent of its name) and its modification time (mtime) not being newer. The check runs after the contender has already closed the marker, so nothing keeps the inode allocated. On a filesystem that hands a freed inode number straight to the next file (ext4, xfs, overlayfs on them) the next holder's marker can carry that number. A contender still between its read and its break then renames and unlinks the live marker. Another local user (the other user below) who can create files in the lock's directory, a sticky `/tmp` included (the sticky bit is a directory flag that lets only a file's owner remove it), forces this at will. It plants a marker with a stale owner record and a modification time in the future and unlinks it as soon as a contender has read it. Two processes of the user running the lock (the lock user below) then hold the same default-configured lock at once, and nothing raises or logs. Affected versions: filelock >= 3.22.0 and mtime_before or current.st_ino != ino_before: return break_path: Final[str] = f"{lock_file}.break.{os.getpid()}.{secrets.token_hex(16)}" Path(lock_file).rename(break_path) if (st_after := os.lstat(break_path)).st_mtime \n\n1\n` and sets its time to the year 2100 with `os.utime()`. A live PID with a wrong start token (the value that identifies one incarnation of a PID) is a recycled PID, so every contender judges the owner stale. On its own this marker only makes the lock user's processes wait: they cannot rename another user's file in a sticky directory (`EPERM`, and `acquire(timeout=1)` raises `Timeout`). 2. A polls, reads `(record, mtime=2100, ino=I)`, closes the file and enters `owner_is_stale()`. 3. The other user unlinks its marker. Inode `I` is free. 4. B polls. The path is empty, so `O_EXCL` (create only if absent) succeeds and ext4 gives the new marker inode number `I`. B holds the lock. 5. A reaches line 113 and looks at B's marker: its time (now) is not newer than 2100 and its inode number is `I`. A renames it (it is the lock user's own file, so the sticky bit does not object), line 117 passes for the same reason, and line 118 unlinks the live marker. 6. The path is empty again while B is still inside. The next contender to poll, A included, creates its marker and holds the lock as well. When B releases, `_unlink_open_marker` finds another file at the path and leaves it alone, so neither side raises or logs anything. This is …

Related

Alias · CVE (1)

  • CVE-2026-112935 filelock is a platform-independent file lock for Python. vulnerability

Weakness · CWE (1)

  • CWE-367 Time-of-check Time-of-use (TOCTOU) Race Condition weakness

References