Skip to content

fix(api): keep the hold count when the lock path cannot be resolved - #856

Merged
gaborbernat merged 3 commits into
tox-dev:mainfrom
spicysharkco:fix/acquire-counter-unresolved-path
Oct 18, 2026
Merged

gaborbernat merged 3 commits into
tox-dev:mainfrom
spicysharkco:fix/acquire-counter-unresolved-path

Conversation

@spicysharkco

Copy link
Copy Markdown
Contributor

A program holds FileLock("resource.lock") with a relative path, and its working directory is removed before lock.acquire() runs, for example a temporary directory that a cleanup step deleted. The call raises FileNotFoundError(2, 'No such file or directory'), as it should. But lock.lock_counter is then 1 and lock.is_locked is False. After the program changes to a directory that exists, with lock: locks the file (lock_counter 2, is_locked True) and when the block ends it reports lock_counter 1 and is_locked True. The lock stays held for the life of the process. The same count error happens when the lock is already held and a nested acquire() runs after the directory and the lock file were removed: the nested call raises and the outer release() no longer unlocks.

Cause: acquire raises lock_counter first (_api.py#L1220-L1221) and only then calls _canonical (L1223). _canonical resolves os.curdir with _resolve_dir, which is realpath except on Windows (L336), which raises when the working directory is gone. The try block whose handler rolls the count back starts below that line, so a failure in _canonical skips it. BaseAsyncFileLock resolves the path first and raises the count after (asyncio.py#L379-L380).

Fix: move the _canonical call above the counter increase. The error leaves the count as it was.

The new test_acquire_without_working_directory_leaves_no_hold in tests/test_filelock.py runs for the four relative-path lock types. It removes the working directory, expects the FileNotFoundError, locks again from a directory that exists, and checks (lock_counter, is_locked) == (0, False).

pytest -q tests/test_filelock.py -k test_acquire_without_working_directory_leaves_no_hold
pytest -q tests/test_filelock.py -k "relative or working_directory or singleton_after or singleton_held or chdir or deadlock or counter"

Before the fix the new test fails 4/4 with assert (1, True) == (0, False). After the fix it passes (4 passed), and the second command passes (40 passed). Putting back the old order fails the new test again. ruff check and ruff format --check are clean on both files, git diff --check is clean, and diff coverage of the changed lines is 100%. towncrier check passes. I did not run the whole suite.

Overlap checked: searched issues and PRs in tox-dev/filelock for deleted working directory, lock_counter FileNotFoundError cwd, _canonical counter acquire raises, getcwd, chdir, canonical, lock_counter, relative path, and listed open PRs with is:pr is:open. There is no open PR. #841 keeps a relative path across a chdir but does not cover a removed working directory, and #586 added the _canonical call at this spot. The branches ci-release-gate, fix-regressions-4012 and fix/async-rw-fork-exit still carry the old order; there is no release branch.

spicysharkco and others added 2 commits October 17, 2026 17:58
acquire() raised the hold count before it resolved the lock path. A relative
path cannot be resolved once the working directory has been removed, so the
call raised FileNotFoundError with the count still one too high and nothing
rolled it back. A later `with lock:` then locked the file and left it locked
when the block ended. A nested acquire on a held lock did the same.

Resolve the path first, as BaseAsyncFileLock.acquire already does.
@gaborbernat
gaborbernat enabled auto-merge (squash) October 18, 2026 06:19
@gaborbernat
gaborbernat merged commit caaff8a into tox-dev:main Oct 18, 2026
44 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