trek-e's review found the M1 PID-liveness backport dropped two pieces of capability-lock.cts's steal-safety machinery, reopening the #500/#905/#1230 lost-update family: - Empty-body window (state.cts): acquireStateLock creates the lock with O_EXCL and writes the pid in a separate writeSync; a lock observed in that gap has an empty body, reads as not-verified-live, and was stolen at age ~0 — robbing a holder mid-creation. Add a fresh-create floor scoped to the unverifiable-body case: an empty/unparseable body that is fresh is treated as mid-creation and is NOT stolen, while a COMPLETE dead-pid body is still stolen promptly (preserves the prompt-dead-steal contract). planning-workspace writes its body atomically (flag:'wx') so it has no empty-body window. - Double-steal (both locks): the steal was a bare fs.unlinkSync with no identity re-confirm, so two waiters could both reclaim a dead holder and end up holding concurrently. Replace with an atomic renameSync (only one racer wins the inode) guarded by a (dev,ino,body) identity re-confirm immediately before the steal; body content is part of the identity to defeat inode reuse. Tests (seam-driven, no wall-clock, each proven RED-before-GREEN): - clock-seam: fresh empty-body lock is not stolen at age ~0; a racer-recreated live lock is not double-stolen (identity re-confirm). Adds a beforeSteal seam. - planning-workspace: racer-recreated live lock is not double-stolen. - Updated the two #1217 unlinkSync-failure tests to the renameSync steal path (the bounded-backoff/no-busy-spin guarantee is preserved and re-asserted). Uncontended acquire path is byte-for-byte unchanged. Claude-Session: https://claude.ai/code/session_01R88n7Q54bAaVHFkDbbH1yz
1.2 KiB
type, pr
| type | pr |
|---|---|
| Fixed | 1532 |
Core-path file locks now verify the holder process is alive before stealing a stale lock (#1532) — the STATE.md write lock (acquireStateLock) and the .planning/ workspace lock (withPlanningLock) previously stole locks on a bare mtime timer with no liveness check, so a live-but-slow holder (e.g. a deep .planning/ scan on slow NFS) could have its lock stolen mid-write, corrupting STATE.md or losing an update. Both locks now gate stealing on process.kill(pid,0) liveness with a deadman ceiling above the wait budget (pid-reuse backstop), withPlanningLock no longer force-steals a live holder on timeout (and can no longer leak an uncaught EEXIST), writeStateMd computes its disk scan inside the lock, and acquireStateLock no longer leaks a file descriptor or strands an empty lock on a recoverable write error. The steal itself is now race-safe: a lock is never stolen while its body is still being written (the create→pid-write window), and stealing uses an atomic rename with an identity re-confirm so two waiters can no longer both reclaim the same lock and end up holding it concurrently. The uncontended path is unchanged.