low

CVE-2026-112907

PyPI · filelock

Summary

filelock: On Windows an oplock on a soft lock's marker holds SoftFileLock.acquire() past its timeout and blocks the stale break

Severity
low
CVSS
2.5
CWE
CWE-667
Also known as
GHSA-567q-5qvw-pf92
Published
2026-10-23
Updated
2026-10-23

Advisory details

On Windows the soft locks (SoftFileLock, MarkerSoftFileLock, SoftFileLease, their async forms, and a FileLock that fell back to a soft lock) read their marker by path. The marker is the lock file whose presence says a process holds the lock. Each acquire() attempt that finds a marker opens it with os.open to read the holder's process id, and the lock renames and deletes a marker it judges stale, by path. On Windows each of these calls waits for an oplock. An oplock (opportunistic lock) is a request by a process that holds a handle on a file to be told before another process opens, renames or deletes that file; the other process waits until the holder answers the break or closes its handle.

Another local account that can open the marker for reading can take a Level 1 oplock on it while no process holds it open, and leave the break unanswered. Every acquire() by the user running the lock (below: the lock user) then waits inside the open, past its timeout, blocking=False and cancel_check, and a stale marker under that oplock is not broken. Mutual exclusion holds and the lock touches no other file. Affected versions are filelock 3.22.0 through 4.2.0 on Windows. The maintainers found this while reviewing the fix for the report Anthropic's OSS Scanner sent on 15 October 2026 (review round 4, finding F4); it is not among the scanner's findings.

Severity

CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:L scores 2.5 (Low).

AV:L (local): the other account needs a logon on the same host. AC:H (high attack complexity): Windows grants a Level 1 or batch oplock only to the sole open handle on a file, so the marker must have no holder handle when the other account asks. That account cannot bring this about; it waits for a holder that died, closed its descriptor, or left a marker past its lifetime. PR:L (low privileges): an ordinary account with read access to the marker through the directory's inherited ACL (access control list). UI:N (no user interaction). S:U (scope unchanged). C:N and I:N: that account can read the marker's content (process id and host name) anyway, and mutual exclusion holds. A:L (partial availability impact): one thread, or the event loop with run_in_executor=False, waits until the holder answers; 10 s per attempt in the two-account runs, no crash. Metrics in doubt take the lower reading.

Classification from the review: security, conditional on an unheld marker that another account can open.

Scope and preconditions

Affected code

Scanned commit b831496b0273: the os.open in _read_lock_file (function), called from _try_break_stale_lock on every attempt that finds a marker, and from _unprobeable_holder, pid and is_lock_held_by_us. break_lock_file renames at L116 and unlinks at L118 and L122, by path. _poll_until_acquired and _check_give_up test the deadline, blocking and cancel_check only between attempts.

Introduced

e35c3af "feat(soft): detect and break stale locks" (#476, 2026-02-13), first tag 3.22.0 (verified with git tag --contains). Before it a soft lock created its marker with O_CREAT | O_EXCL and never opened an existing one. #476 read the marker on Windows through CreateFileW and broke a stale one with Path.rename and Path.unlink. These wait for an oplock as os.open does, because none of them passes FILE_COMPLETE_IF_OPLOCKED, an NtCreateFile option that returns instead of waiting. The read became the os.open in _read_lock_file with bfbfa76 (#548, 3.29.1) and the break moved into break_lock_file with d244c6d (#551, 3.29.2); the by-path calls stayed.

Mechanism

Each attempt of _acquire first calls os.open(path, O_WRONLY | O_CREAT | O_EXCL). On an existing marker that fails with EEXIST (or EACCES on Windows) at once; the same-account probe shows the failed exclusive create returning at once under an oplock too. _acquire then calls _try_break_stale_lock, which calls _read_lock_file: os.lstat, then os.open(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK). On Windows O_NOFOLLOW and O_NONBLOCK do not exist (the code reads them with getattr(os, ..., 0)), and os.open calls CreateFileW. When another handle holds a Level 1 or batch oplock on the file, that call sends the holder a break request and returns only when the holder acknowledges it or closes its handle. _check_give_up tests the deadline after the attempt returns, so the wait overruns timeout, blocking=False and cancel_check by its length. Once the open returns, the attempt ends and _check_give_up raises Timeout, because the deadline has passed.

A marker whose holder

References

Related advisories

Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.

Check my repo

Summarize with AI

ChatGPTClaudePerplexity

Sources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.