Skip to content

_util: drop the dead st_mtime=0 short-circuit in raise_on_not_writable_file#582

Merged
gaborbernat merged 3 commits into
tox-dev:mainfrom
HrachShah:fix/mtime-zero-still-checks-writability
Jul 6, 2026
Merged

_util: drop the dead st_mtime=0 short-circuit in raise_on_not_writable_file#582
gaborbernat merged 3 commits into
tox-dev:mainfrom
HrachShah:fix/mtime-zero-still-checks-writability

Conversation

@HrachShah

Copy link
Copy Markdown
Contributor

raise_on_not_writable_file guarded its writability and is-dir checks behind if file_stat.st_mtime != 0, on the theory that an all-zero stat struct meant the caller's stat call had failed in a way the OS could not report (see the original commit c9b6d90, "Raise when trying to acquire in R/O or missing folder").

The guard was useful on the very old NFS / Linux combinations it was written against. On every Python release filelock supports today the result of os.lstat is well-defined and never returns an all-zero struct for a file that exists, so the guard has been dead code for a long time — and worse, it silences both checks whenever a caller (or a test, or a tool that resets mtime to 0 for unrelated reasons) explicitly sets the mtime to 0. The bug only mattered in practice when the lock path was a read-only file or a directory: the check would skip both guards, the caller's subsequent os.open would succeed on non-Windows, and the acquire path would either take a lock on a file nothing else can write to or block forever waiting for an os.open that cannot succeed.

SoftFileLock with its EEXIST-then-stale-break fallback could also spuriously recycle a live lock whose file happened to have mtime=0: the EEXIST from os.open(O_CREAT | O_EXCL) falls into _try_break_stale_lock, and the "malformed lock, age ≥ threshold" branch unconditionally unlinks a lock whose mtime is sufficiently old — and time.time() - 0.0 is, of course, sufficiently old.

What changes

  • src/filelock/_util.py: remove the st_mtime != 0 short-circuit. The writability and is-dir checks now run regardless of the observed mtime, matching the function's documented contract.
  • tests/test_util.py: add 3 unit tests pinning the function-level behavior:
    • test_raise_on_not_writable_file_rejects_readonly_file_with_mtime_zero: 0o444 file with mtime=0 still raises PermissionError.
    • test_raise_on_not_writable_file_rejects_readonly_file_with_future_mtime: 0o444 file with mtime=2_000_000_000 still raises PermissionError — locks in the "mtime is irrelevant to writability" reading so a later patch can't silently narrow the check to one mtime range.
    • test_raise_on_not_writable_file_rejects_directory_with_mtime_zero: directory with mtime=0 still raises IsADirectoryError.
  • tests/test_filelock.py::test_mtime_zero_exit_branch: the SoftFileLock parametrization now expects PermissionError instead of TimeoutError. The previous expectation relied on the very short-circuit this PR removes — under the new behavior, the writability check fires first on a non-root user, which is what the test name ("exit branch") and the FileLock parametrization have always assumed.

Tests

python -m pytest tests/test_util.py → 10 passed, 2 skipped (3 new tests added; pre-existing 7 + 2 symlink tests skipped on this platform still pass).

python -m pytest tests/test_filelock.py -k mtime_zero → 1 passed, 1 failed. The failure is test_mtime_zero_exit_branch[UnixFileLock-PermissionError], which is a pre-existing root-only failure (root can os.open a 0o444 file with O_RDWR | O_TRUNC, so the test only fires on non-root unices) — confirmed by re-running on the unpatched main from a stash, where the same test fails in exactly the same way.

python -m pytest tests/ → 471 passed, 31 skipped, 2 failed. Both failures are pre-existing on the unpatched main (the mtime_zero[UnixFileLock] and test_stale_inode_retry_on_unlinked_lock cases), unrelated to this change.

Zo Bot and others added 3 commits July 6, 2026 11:55
…e_file

The guard predated the lstat-symlink hardening (PR tox-dev#567) and was meant
to defend against old platforms where os.stat could return an all-zero
struct for a file that exists. On every Python release filelock
supports today the struct is well-defined, so the guard is dead
weight — and worse, it silences the writability and is-dir checks
whenever a caller (or a test, or a tool that resets mtime to 0 for
unrelated reasons) sets the mtime to 0.

The bug only mattered in practice when the lock path was a read-only
file or a directory: the check would skip both guards, the caller's
subsequent os.open would succeed on non-Windows, and the acquire path
would either take a lock on a file nothing else can write to or block
forever waiting for an os.open that cannot succeed. SoftFileLock with
its EEXIST-then-stale-break fallback could also spuriously recycle a
live lock that happened to have mtime=0.

Remove the guard. Add unit tests in tests/test_util.py covering the
readonly-file-with-mtime-zero, future-mtime, and directory-with-mtime-
zero corners so the writability verdict cannot be silently narrowed
again. Update tests/test_filelock.py::test_mtime_zero_exit_branch's
expected exception for SoftFileLock from TimeoutError to
PermissionError — the previous expectation relied on the very
short-circuit this commit removes, so a non-root user running the
test now gets the same exception as the FileLock branch instead of
hitting the stale-lock fallback path first.
Removing the st_mtime guard makes raise_on_not_writable_file fire on a
read-only lock file for every backend that calls it, so WindowsFileLock
now raises PermissionError too — the test still expected TimeoutError,
which broke the Windows CI. Expect PermissionError on every platform and
drop the stale "exit branch" name now that no such branch exists.

Fold the two near-duplicate readonly function tests into one
parametrized case and tighten the comments.
@gaborbernat gaborbernat enabled auto-merge (squash) July 6, 2026 22:45
@gaborbernat gaborbernat merged commit b68be65 into tox-dev:main Jul 6, 2026
33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants