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.
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.
Reproduction
Tested locally (Darwin, Python 3.12):
Run across versions:
Suggested fix
We had to replace
filelockwith a rawfcntl.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 offlock()on UNIX. The lock file is inert after release (zero bytes, no sensitive content) and can be safely left on disk.