Skip to content

UnixFileLock deleting the lock file on release can break mutual exclusion #574

Description

@antoinebrl

Summary

Since version 3.20.4, UnixFileLock._release() deletes the lock file after releasing. This breaks use cases where multiple processes coordinate via the same lock file path, because a process that already has an open fd on the old inode can hold a lock on a deleted file while another process creates and locks a new file under the same name.

The issue

Citing the comment in #27 (comment), which is precisely the issue I faced.

Deleting lockfiles on UNIX causes a race condition. Consider the following set of events:

  • Program A creates and opens (as an atomic operation) a file A.lck.
  • Program A grabs a lock on the file handle it holds on that file; this operation succeeds.
  • Program B opens A.lck.
  • Program B requests a lock on the file handle; this operation blocks.
  • Program A deletes A.lck.
  • Program A closes its file handle, which makes program B able to get a lock on the file handle it already holds, on the now-deleted copy of A.lck.
  • Program C creates and opens (as an atomic operation) a new file under the name A.lck.
  • Program C grabs a lock on the file handle it holds on this file; because they're two different files (even if under the same name), this succeeds.

...thus, you have both programs B and C thinking they hold the same lock.

Reproduction

Tested locally (Darwin, Python 3.12):

# /// script
# requires-python = ">=3.12"
# dependencies = ["filelock"]
# ///
import platform, tempfile
from pathlib import Path
import filelock

p = Path(tempfile.gettempdir()) / "filelock_delete_test.lock"
p.unlink(missing_ok=True)
lock = filelock.FileLock(p)
lock.acquire()
lock.release()
print(
    f"filelock=={filelock.__version__}  "
    f"class={type(lock).__name__}  "
    f"platform={platform.system()}  "
    f"lock_file_survives_release={p.exists()}"
)
p.unlink(missing_ok=True)

Run across versions:

for v in 3.20.3 3.20.4 3.29.4; do
  uv run --with "filelock==$v" test_filelock_race.py
done
filelock==3.20.3  lock_file_survives_release=True
filelock==3.20.4  lock_file_survives_release=False  ← regression
filelock==3.29.4  lock_file_survives_release=False

Suggested fix

We had to replace filelock with a raw fcntl.flock-based lock that intentionally leaves the lock file on disk. Ideally, UnixFileLock._release() should not delete the lock file, or have an option to keep it. This was the behavior through 3.20.3 and matches the semantics of flock() on UNIX. The lock file is inert after release (zero bytes, no sensitive content) and can be safely left on disk.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions