GitHub Advisoryadvisory · PyPI

GHSA-2g95-wg33-c5x2 filelock: SoftFileLock evicts a fresh lock file whose watched age another local user's pre-dated file passed on through inode reuse

MEDIUM Published 2026-10-23 Modified 2026-10-23 Official page ↗
Aliases
CVE-2026-112906
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-694
Reviewed
2026-10-23

Affected packages

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

Description

`SoftFileLock` and `MarkerSoftFileLock` evict an unparsable marker (the lock file at ``, whose presence says a process holds the lock) once a waiting contender has watched it unchanged for two seconds. `SoftFileLease` expires a lease the same way. The watch tells two looks apart by the marker's modification time (mtime) and inode number (the kernel's identity for a file, independent of its name) alone. 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), parks an empty file of its own at the lock path. It dates that file to a future tick of the kernel's coarse clock (a clock that advances in steps of 4 ms), lets the waiters of the user running the lock (the lock user below) watch it past two seconds, and unlinks it in that tick. On a filesystem that hands a freed inode number straight back (ext4, overlayfs on ext4) the lock user's next marker repeats both values. It inherits the time spent watching the other user's file, and a second waiter removes it while its owner holds the lock. Two of the lock user's processes then run the critical section together with no error. A `SoftFileLease` waiter takes a live lease that its owner wrote moments before, within milliseconds instead of after `lease_duration`. Affected versions: filelock >= 4.1.0 and /stat`, so for about 75 µs (measured) the marker exists and is empty. A contender that polls in that gap must not take the marker for garbage, and the check that protects it is the two-second watch. `_try_break_stale_lock` reads the marker, finds no holder, and calls `break_lock_file(lock_file, mtime, ino)` only if `_marker_unchanged_for(mtime, ino)` reaches `MALFORMED_LOCK_AGE_THRESHOLD` (2.0 s). That function keeps `self._marker_seen = ((mtime, ino), first seen)` across polls and returns the elapsed monotonic time (time from a clock that only moves forward) while the pair repeats. Neither half of the pair belongs to one file. A file's owner sets its mtime to any value with `utime`, a future time included, and ext4 gives the inode number of an unlinked file to the next file created in the directory. 1. While the lock is free, the other user creates an empty file at the lock path (inode `I`) and sets its mtime to `T`, a tick of the coarse clock about 2.2 s in the future. 2. Every waiting process of the lock user polls each 50 ms, reads an empty file and records `_marker_seen = ((T, I), first seen)`. After 2 s each poll calls `break_lock_file`, whose `rename` fails with `EPERM` (operation not permitted; seen with `strace`): the directory is sticky and the file belongs to the other user. `suppress(OSError, ...)` swallows the error, the file stays, and nothing resets `_marker_seen`. 3. When the coarse clock reaches `T`, the other user unlinks its file. Inode `I` is free. 4. Process F of the lock user polls during tick `T`. Its `O_EXCL` create succeeds, and its marker is inode `I` with mtime `T`. 5. Process S polls before F has written its record. It reads an empty file with pair `(T, I)`, `_marker_unchanged_for` returns the 2.2 s that S spent watching the other user's file, and S calls `break_lock_file(lock_file, T, I)`. 6. `break_lock_file` compares mtime and inode before and after its rename, but with …

Related

Alias · CVE (1)

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

Weakness · CWE (1)

  • CWE-694 Use of Multiple Resources with Duplicate Identifier weakness

References