From 8ecf15cc3fde9d18a21e663c0d326ed6ee6d8997 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 19 Aug 2026 16:54:04 +0200 Subject: [PATCH] fslocking: fix broken exclusivity on cygwin, see #7218 ExclusiveLock.acquire() considers a failed rename() as "already locked" and then returns successfully if by_me() tells that the lock is ours. by_me() was implemented via a stat() call (Path.exists()). On cygwin, stat() transiently succeeds for a path inside a directory that is concurrently being replaced via rename(): it then disagrees with what reading that directory gives. So by_me() sometimes claimed a lock as ours that we never acquired and multiple threads ended up inside the critical section. Reading the directory does not show that inconsistency, so use that instead. Measured on cygwin (win11, py3.12), 709 probes, each of which must be False: by_me() via stat() 18 false positives by_me() via stat() twice 1 false positive by_me() via reading the dir 0 false positives Note that checking twice is not good enough, so really read the directory. TestExclusiveLock.test_race_condition on cygwin: it failed in ~20% of the runs before and passes 30 of 30 runs now. Co-Authored-By: Claude Opus 5 --- src/borg/fslocking.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/borg/fslocking.py b/src/borg/fslocking.py index 104ab2a4ee..5a7f241ea0 100644 --- a/src/borg/fslocking.py +++ b/src/borg/fslocking.py @@ -202,7 +202,16 @@ def is_locked(self): return self.path.exists() def by_me(self): - return self.unique_name.exists() + # Do not use a stat() based check (e.g. self.unique_name.exists()) here: + # on cygwin, stat() transiently succeeds for a path inside a directory that is + # concurrently replaced via rename(), so we would believe we own a lock that + # actually is owned by somebody else and exclusivity would be broken, see #7218. + # Reading the directory does not show that inconsistency. + try: + return self.unique_name.name in (path_obj.name for path_obj in self.path.iterdir()) + except OSError: + # directory vanished (or is not readable) -> the lock is not ours. + return False def kill_stale_lock(self): try: