diff --git a/.changeset/mellow-hawks-greet.md b/.changeset/mellow-hawks-greet.md new file mode 100644 index 000000000..f988e2461 --- /dev/null +++ b/.changeset/mellow-hawks-greet.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 3705 +--- +Repair Windows-only TOCTOU regression in acquireStateLock — last-retry stale-lock recovery now re-acquires the lock atomically before proceeding, so concurrent state writes to different fields both persist (fixes main CI red on locking-bugs regression test). diff --git a/get-shit-done/bin/lib/state.cjs b/get-shit-done/bin/lib/state.cjs index e0b2c284e..84e9828eb 100644 --- a/get-shit-done/bin/lib/state.cjs +++ b/get-shit-done/bin/lib/state.cjs @@ -942,7 +942,18 @@ function acquireStateLock(statePath) { } catch { /* lock was released between check — retry */ } if (i === maxRetries - 1) { - try { fs.unlinkSync(lockPath); } catch {} + // Stale-lock recovery: delete the lock and do one final acquisition + // attempt. Returning lockPath without holding the lock (the previous + // behaviour) allowed two concurrent processes to both "acquire" a + // phantom lock, breaking mutual exclusion on Windows-24 where slower + // process spawn means concurrent writers overlap for longer (#3705). + try { fs.unlinkSync(lockPath); } catch { /* already gone — proceed */ } + try { + const fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY); + fs.writeSync(fd, String(process.pid)); + fs.closeSync(fd); + _heldStateLocks.add(lockPath); + } catch { /* another process raced us — proceed without lock as last resort */ } return lockPath; } const jitter = Math.floor(Math.random() * 50);