Review round 1 on PR #194:
- The edit lock did not exclude other processes (also on main): unlock
removes the lock file, so a waiter could wake holding a flock on the
orphaned inode while the next caller locked a fresh file. flockFile now
re-checks after flock that the locked file is still the one at the path
(brainfs LockIsCurrent) and retries otherwise; OpenLock reports a lock
replaced mid-open as ErrLockChanged, which is retried instead of
failing the edit. New cross-process test (3 re-executed processes x 400
rounds, O_EXCL marker under the lock): 0 overlaps; red without the
re-check.
- OpenLock unit test: relative dangling link (only the Lstat pre-check
stops it; os.Root refuses absolute links on its own), live link, FIFO,
directory, behind a symlinked dir, unclean path, positive control.
- Rollback test: a symlink swapped in for the note before the rollback
(test seam beforeStampHook) is refused, target untouched in and outside
the brain.
- The in-brain dangling-lock case uses a relative link and asserts the
refusal names the lock. OpenLock reuses #189's RealParent; RemoveLock is
dropped for brainfs.Remove. The doc comment names the swap residual.
Rebased onto main after #189.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brain_edit did its own I/O by path around notes.Edit:
- the pre-edit snapshot used os.ReadFile, which hung on a FIFO note held
open by a writer;
- the rollback (when the provenance stamp fails) used os.WriteFile;
- the edit lock "<note>.lock" was opened with O_CREATE by path, so a
planted dangling .lock symlink made a successful edit create a file at
the link's target, in the brain or outside it.
Snapshot is brainfs ReadRegular, rollback WriteRegular, and the lock is
opened and removed through new brainfs OpenLock/RemoveLock: clean path,
real parent directories, an existing lock must be regular, and the opened
handle must be the file at the path (SameFile).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>