//go:build !windows package mount import ( "math" "runtime" "sync" "sync/atomic" "syscall" "testing" "time" "github.com/seaweedfs/go-fuse/v2/fuse" ) func TestNonOverlappingLocksFromDifferentOwners(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) s1 := plt.SetLk(inode, lockRange{Start: 0, End: 49, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) if s1 != fuse.OK { t.Fatalf("expected OK, got %v", s1) } s2 := plt.SetLk(inode, lockRange{Start: 50, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}) if s2 != fuse.OK { t.Fatalf("expected OK, got %v", s2) } } func TestOverlappingReadLocksFromDifferentOwners(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) s1 := plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_RDLCK, Owner: 1, Pid: 10}) if s1 != fuse.OK { t.Fatalf("expected OK, got %v", s1) } s2 := plt.SetLk(inode, lockRange{Start: 50, End: 149, Typ: syscall.F_RDLCK, Owner: 2, Pid: 20}) if s2 != fuse.OK { t.Fatalf("expected OK, got %v", s2) } } func TestOverlappingWriteReadConflict(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) s := plt.SetLk(inode, lockRange{Start: 50, End: 149, Typ: syscall.F_RDLCK, Owner: 2, Pid: 20}) if s != fuse.EAGAIN { t.Fatalf("expected EAGAIN, got %v", s) } } func TestOverlappingWriteWriteConflict(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) s := plt.SetLk(inode, lockRange{Start: 50, End: 149, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}) if s != fuse.EAGAIN { t.Fatalf("expected EAGAIN, got %v", s) } } func TestSameOwnerUpgradeReadToWrite(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_RDLCK, Owner: 1, Pid: 10}) s := plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) if s != fuse.OK { t.Fatalf("expected OK for same-owner upgrade, got %v", s) } // Verify the lock is now a write lock. var out fuse.LkOut plt.GetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}, &out) if out.Lk.Typ != syscall.F_WRLCK { t.Fatalf("expected conflicting write lock, got type %d", out.Lk.Typ) } } func TestSameOwnerDowngradeWriteToRead(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) s := plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_RDLCK, Owner: 1, Pid: 10}) if s != fuse.OK { t.Fatalf("expected OK for same-owner downgrade, got %v", s) } // Another owner should now be able to get a read lock. s2 := plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_RDLCK, Owner: 2, Pid: 20}) if s2 != fuse.OK { t.Fatalf("expected OK for shared read lock, got %v", s2) } } func TestLockCoalescing(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) plt.SetLk(inode, lockRange{Start: 0, End: 9, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) plt.SetLk(inode, lockRange{Start: 10, End: 19, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) il := plt.getInodeLocks(inode) il.mu.Lock() ownerLocks := 0 for _, lk := range il.locks { if lk.Owner == 1 { ownerLocks++ if lk.Start != 0 || lk.End != 19 { t.Errorf("expected coalesced lock [0,19], got [%d,%d]", lk.Start, lk.End) } } } il.mu.Unlock() if ownerLocks != 1 { t.Fatalf("expected 1 coalesced lock, got %d", ownerLocks) } } func TestLockSplitting(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) // Unlock the middle portion. plt.SetLk(inode, lockRange{Start: 40, End: 59, Typ: syscall.F_UNLCK, Owner: 1, Pid: 10}) il := plt.getInodeLocks(inode) il.mu.Lock() ownerLocks := 0 for _, lk := range il.locks { if lk.Owner == 1 { ownerLocks++ } } if ownerLocks != 2 { il.mu.Unlock() t.Fatalf("expected 2 locks after split, got %d", ownerLocks) } // Check the ranges. if il.locks[0].Start != 0 || il.locks[0].End != 39 { t.Errorf("expected left lock [0,39], got [%d,%d]", il.locks[0].Start, il.locks[0].End) } if il.locks[1].Start != 60 || il.locks[1].End != 99 { t.Errorf("expected right lock [60,99], got [%d,%d]", il.locks[1].Start, il.locks[1].End) } il.mu.Unlock() } func TestGetLkConflict(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) plt.SetLk(inode, lockRange{Start: 10, End: 50, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) var out fuse.LkOut plt.GetLk(inode, lockRange{Start: 30, End: 70, Typ: syscall.F_RDLCK, Owner: 2, Pid: 20}, &out) if out.Lk.Typ != syscall.F_WRLCK { t.Fatalf("expected conflicting write lock, got type %d", out.Lk.Typ) } if out.Lk.Pid != 10 { t.Fatalf("expected holder PID 10, got %d", out.Lk.Pid) } if out.Lk.Start != 10 || out.Lk.End != 50 { t.Fatalf("expected conflict [10,50], got [%d,%d]", out.Lk.Start, out.Lk.End) } } func TestGetLkNoConflict(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) plt.SetLk(inode, lockRange{Start: 10, End: 50, Typ: syscall.F_RDLCK, Owner: 1, Pid: 10}) var out fuse.LkOut plt.GetLk(inode, lockRange{Start: 30, End: 70, Typ: syscall.F_RDLCK, Owner: 2, Pid: 20}, &out) if out.Lk.Typ != syscall.F_UNLCK { t.Fatalf("expected F_UNLCK (no conflict), got type %d", out.Lk.Typ) } } func TestGetLkSameOwnerNoConflict(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) var out fuse.LkOut plt.GetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}, &out) if out.Lk.Typ != syscall.F_UNLCK { t.Fatalf("same owner should not conflict with itself, got type %d", out.Lk.Typ) } } func TestReleaseOwner(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) plt.SetLk(inode, lockRange{Start: 0, End: 49, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) plt.SetLk(inode, lockRange{Start: 50, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) plt.SetLk(inode, lockRange{Start: 200, End: 299, Typ: syscall.F_RDLCK, Owner: 2, Pid: 20}) plt.ReleaseOwner(inode, 1) // Owner 1's locks should be gone. var out fuse.LkOut plt.GetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 3, Pid: 30}, &out) if out.Lk.Typ != syscall.F_UNLCK { t.Fatalf("expected no conflict after ReleaseOwner, got type %d", out.Lk.Typ) } // Owner 2's lock should still exist. plt.GetLk(inode, lockRange{Start: 200, End: 299, Typ: syscall.F_WRLCK, Owner: 3, Pid: 30}, &out) if out.Lk.Typ != syscall.F_RDLCK { t.Fatalf("expected owner 2's read lock to remain, got type %d", out.Lk.Typ) } } func TestDifferentLockKindsDoNotConflict(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) s1 := plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) if s1 != fuse.OK { t.Fatalf("expected POSIX lock OK, got %v", s1) } s2 := plt.SetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20, IsFlock: true}) if s2 != fuse.OK { t.Fatalf("expected flock lock OK in separate namespace, got %v", s2) } } func TestReleasePosixOwnerReleasesPosixLocksAndWakesWaiters(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) done := make(chan fuse.Status, 1) go func() { cancel := make(chan struct{}) done <- plt.SetLkw(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}, cancel) }() time.Sleep(50 * time.Millisecond) plt.ReleasePosixOwner(inode, 1) select { case s := <-done: if s != fuse.OK { t.Fatalf("expected OK after ReleasePosixOwner, got %v", s) } case <-time.After(2 * time.Second): t.Fatal("SetLkw did not unblock after ReleasePosixOwner") } } func TestReleasePosixOwnerDoesNotReleaseFlockLocks(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) plt.SetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10, IsFlock: true}) plt.ReleasePosixOwner(inode, 1) var out fuse.LkOut plt.GetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20, IsFlock: true}, &out) if out.Lk.Typ != syscall.F_WRLCK { t.Fatalf("expected flock lock to remain after ReleasePosixOwner, got type %d", out.Lk.Typ) } } func TestHasPosixOwnerIgnoresMissingOwnerAndFlock(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) if plt.HasPosixOwner(inode, 1) { t.Fatal("missing owner should not be reported as holding POSIX locks") } if s := plt.SetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10, IsFlock: true}); s != fuse.OK { t.Fatalf("set flock: %v", s) } if plt.HasPosixOwner(inode, 1) { t.Fatal("flock owner should not be reported as a POSIX lock owner") } if s := plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}); s != fuse.OK { t.Fatalf("set POSIX lock: %v", s) } if !plt.HasPosixOwner(inode, 2) { t.Fatal("POSIX lock owner was not reported") } if plt.HasPosixOwner(inode, 0) { t.Fatal("zero owner should not be reported") } } func TestWakeEligibleWaitersKeepsInodeUntilWakeRefReleased(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) il := plt.getOrCreateInodeLocks(inode) waiter := &lockWaiter{ requested: lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}, ch: make(chan struct{}), } il.mu.Lock() il.waiters = append(il.waiters, waiter) il.mu.Unlock() plt.releaseMatching(inode, func(lockRange) bool { return false }) select { case <-waiter.ch: // Expected. default: t.Fatal("expected waiter to be woken") } plt.mu.Lock() _, exists := plt.inodes[inode] plt.mu.Unlock() if !exists { t.Fatal("inodeLocks should remain while a woken waiter still holds a wake ref") } il.mu.Lock() releaseWakeRef(il, waiter) il.mu.Unlock() plt.maybeCleanupInode(inode, il) plt.mu.Lock() _, exists = plt.inodes[inode] plt.mu.Unlock() if exists { t.Fatal("inodeLocks should be cleaned up after the final wake ref is released") } } func TestReleaseFlockOwnerDoesNotReleasePosixLocks(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) plt.SetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 2, Pid: 10, IsFlock: true}) plt.ReleaseFlockOwner(inode, 2) var out fuse.LkOut plt.GetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 3, Pid: 30}, &out) if out.Lk.Typ != syscall.F_WRLCK { t.Fatalf("expected POSIX lock to remain after ReleaseFlockOwner, got type %d", out.Lk.Typ) } plt.GetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 4, Pid: 40, IsFlock: true}, &out) if out.Lk.Typ != syscall.F_UNLCK { t.Fatalf("expected flock lock to be removed after ReleaseFlockOwner, got type %d", out.Lk.Typ) } } func TestReleaseOwnerWakesWaiters(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) done := make(chan fuse.Status, 1) go func() { cancel := make(chan struct{}) s := plt.SetLkw(inode, lockRange{Start: 50, End: 60, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}, cancel) done <- s }() // Give the goroutine time to block. time.Sleep(50 * time.Millisecond) plt.ReleaseOwner(inode, 1) select { case s := <-done: if s != fuse.OK { t.Fatalf("expected OK after ReleaseOwner woke waiter, got %v", s) } case <-time.After(2 * time.Second): t.Fatal("SetLkw did not unblock after ReleaseOwner") } } func TestSetLkwBlocksAndSucceeds(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) done := make(chan fuse.Status, 1) go func() { cancel := make(chan struct{}) s := plt.SetLkw(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}, cancel) done <- s }() // Give the goroutine time to block. time.Sleep(50 * time.Millisecond) // Release the conflicting lock. plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_UNLCK, Owner: 1, Pid: 10}) select { case s := <-done: if s != fuse.OK { t.Fatalf("expected OK, got %v", s) } case <-time.After(2 * time.Second): t.Fatal("SetLkw did not unblock after conflicting lock was released") } } func TestSetLkwCancellation(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) cancel := make(chan struct{}) done := make(chan fuse.Status, 1) go func() { s := plt.SetLkw(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}, cancel) done <- s }() // Give the goroutine time to block. time.Sleep(50 * time.Millisecond) close(cancel) select { case s := <-done: if s != fuse.EINTR { t.Fatalf("expected EINTR on cancel, got %v", s) } case <-time.After(2 * time.Second): t.Fatal("SetLkw did not unblock after cancel") } } func TestWholeFileLock(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) // Simulate flock() — whole-file exclusive lock. s1 := plt.SetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) if s1 != fuse.OK { t.Fatalf("expected OK, got %v", s1) } // Second owner should be blocked. s2 := plt.SetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}) if s2 != fuse.EAGAIN { t.Fatalf("expected EAGAIN, got %v", s2) } // Even a partial overlap should fail. s3 := plt.SetLk(inode, lockRange{Start: 100, End: 200, Typ: syscall.F_RDLCK, Owner: 2, Pid: 20}) if s3 != fuse.EAGAIN { t.Fatalf("expected EAGAIN for partial overlap with whole-file lock, got %v", s3) } } func TestUnlockNoExistingLocks(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) // Unlock on an inode with no locks should succeed silently. s := plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_UNLCK, Owner: 1, Pid: 10}) if s != fuse.OK { t.Fatalf("expected OK for unlock with no existing locks, got %v", s) } } func TestMultipleInodesIndependent(t *testing.T) { plt := NewPosixLockTable() // Write lock on inode 1 should not affect inode 2. plt.SetLk(1, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) s := plt.SetLk(2, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}) if s != fuse.OK { t.Fatalf("locks on different inodes should be independent, got %v", s) } } func TestMemoryCleanup(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) plt.ReleaseOwner(inode, 1) plt.mu.Lock() _, exists := plt.inodes[inode] plt.mu.Unlock() if exists { t.Fatal("expected inode entry to be cleaned up after all locks released") } } func TestSelectiveWaking(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) // Owner 1 holds write lock on [0, 99], owner 2 holds write lock on [200, 299]. plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) plt.SetLk(inode, lockRange{Start: 200, End: 299, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}) // Owner 3 waits for [50, 60] (blocked by owner 1). done3 := make(chan fuse.Status, 1) go func() { cancel := make(chan struct{}) s := plt.SetLkw(inode, lockRange{Start: 50, End: 60, Typ: syscall.F_WRLCK, Owner: 3, Pid: 30}, cancel) done3 <- s }() // Owner 4 waits for [250, 260] (blocked by owner 2). done4 := make(chan fuse.Status, 1) go func() { cancel := make(chan struct{}) s := plt.SetLkw(inode, lockRange{Start: 250, End: 260, Typ: syscall.F_WRLCK, Owner: 4, Pid: 40}, cancel) done4 <- s }() time.Sleep(50 * time.Millisecond) // Release owner 1's lock. Only owner 3 should be woken; owner 4 is still blocked. plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_UNLCK, Owner: 1, Pid: 10}) select { case s := <-done3: if s != fuse.OK { t.Fatalf("expected OK for owner 3, got %v", s) } case <-time.After(2 * time.Second): t.Fatal("owner 3 was not woken after owner 1 released") } // Owner 4 should still be blocked. select { case s := <-done4: t.Fatalf("owner 4 should still be blocked, but got %v", s) case <-time.After(100 * time.Millisecond): // Expected — still blocked. } // Now release owner 2's lock. Owner 4 should wake. plt.SetLk(inode, lockRange{Start: 200, End: 299, Typ: syscall.F_UNLCK, Owner: 2, Pid: 20}) select { case s := <-done4: if s != fuse.OK { t.Fatalf("expected OK for owner 4, got %v", s) } case <-time.After(2 * time.Second): t.Fatal("owner 4 was not woken after owner 2 released") } } func TestSameOwnerReplaceDifferentType(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) // Lock [0, 99] as write. plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) // Replace middle portion [30, 60] with read lock. plt.SetLk(inode, lockRange{Start: 30, End: 60, Typ: syscall.F_RDLCK, Owner: 1, Pid: 10}) il := plt.getInodeLocks(inode) il.mu.Lock() defer il.mu.Unlock() // Should have 3 locks: write [0,29], read [30,60], write [61,99]. if len(il.locks) != 3 { t.Fatalf("expected 3 locks after partial type change, got %d", len(il.locks)) } if il.locks[0].Typ != syscall.F_WRLCK || il.locks[0].Start != 0 || il.locks[0].End != 29 { t.Errorf("expected write [0,29], got type=%d [%d,%d]", il.locks[0].Typ, il.locks[0].Start, il.locks[0].End) } if il.locks[1].Typ != syscall.F_RDLCK || il.locks[1].Start != 30 || il.locks[1].End != 60 { t.Errorf("expected read [30,60], got type=%d [%d,%d]", il.locks[1].Typ, il.locks[1].Start, il.locks[1].End) } if il.locks[2].Typ != syscall.F_WRLCK || il.locks[2].Start != 61 || il.locks[2].End != 99 { t.Errorf("expected write [61,99], got type=%d [%d,%d]", il.locks[2].Typ, il.locks[2].Start, il.locks[2].End) } } func TestNonAdjacentRangesNotCoalesced(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) // Lock [5, MaxUint64] then [0, 2] — gap at [3,4] must prevent coalescing. plt.SetLk(inode, lockRange{Start: 5, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) s := plt.SetLk(inode, lockRange{Start: 0, End: 2, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) if s != fuse.OK { t.Fatalf("expected OK, got %v", s) } il := plt.getInodeLocks(inode) il.mu.Lock() defer il.mu.Unlock() if len(il.locks) != 2 { t.Fatalf("expected 2 separate locks (gap [3,4] prevents coalescing), got %d", len(il.locks)) } if il.locks[0].Start != 0 || il.locks[0].End != 2 { t.Errorf("expected first lock [0,2], got [%d,%d]", il.locks[0].Start, il.locks[0].End) } if il.locks[1].Start != 5 || il.locks[1].End != math.MaxUint64 { t.Errorf("expected second lock [5,MaxUint64], got [%d,%d]", il.locks[1].Start, il.locks[1].End) } } func TestAdjacencyNoOverflowAtMaxUint64(t *testing.T) { plt := NewPosixLockTable() inode := uint64(1) // Lock to EOF (End = MaxUint64), then lock [0, 0] same type. // Without the overflow guard, MaxUint64+1 wraps to 0, falsely merging. plt.SetLk(inode, lockRange{Start: 100, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) plt.SetLk(inode, lockRange{Start: 0, End: 0, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}) il := plt.getInodeLocks(inode) il.mu.Lock() defer il.mu.Unlock() // Should remain 2 separate locks, not merged. ownerLocks := 0 for _, lk := range il.locks { if lk.Owner == 1 { ownerLocks++ } } if ownerLocks != 2 { t.Fatalf("expected 2 separate locks (no overflow merge), got %d", ownerLocks) } } // TestSetLkRetriesPastDeadInodeLocks deterministically exercises the // getOrCreateInodeLocks vs maybeCleanupInode race: a caller holding a // pointer to an inodeLocks that is concurrently marked dead must refetch // from the map instead of mutating the orphaned instance (which would be // invisible to subsequent callers and let two exclusive flock holders // coexist). The test bypasses scheduling by hand-installing a dead il into // the table and asserting that the next SetLk routes around it. func TestSetLkRetriesPastDeadInodeLocks(t *testing.T) { plt := NewPosixLockTable() inode := uint64(42) // Acquire and release a lock so maybeCleanupInode marks the il dead and // removes it from the map. lock := lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 1, IsFlock: true} if s := plt.SetLk(inode, lock); s != fuse.OK { t.Fatalf("prime SetLk: got %v", s) } dead := plt.getInodeLocks(inode) unlock := lock unlock.Typ = syscall.F_UNLCK if s := plt.SetLk(inode, unlock); s != fuse.OK { t.Fatalf("prime unlock: got %v", s) } if !dead.dead { t.Fatal("expected il to be marked dead after unlock+cleanup") } plt.mu.Lock() _, stillMapped := plt.inodes[inode] plt.mu.Unlock() if stillMapped { t.Fatal("expected the dead il to be removed from the map") } // Simulate the race: the next caller's getOrCreateInodeLocks races with // the cleanup and ends up holding a pointer to the dead il. We force that // state by re-publishing `dead` into the map. plt.mu.Lock() plt.inodes[inode] = dead plt.mu.Unlock() // SetLk must notice dead, refetch, and install the new lock in a fresh il. if s := plt.SetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 2, IsFlock: true}); s != fuse.OK { t.Fatalf("SetLk after dead: got %v", s) } dead.mu.Lock() if n := len(dead.locks); n != 0 { t.Fatalf("dead il should not have accepted the insert, found %d locks", n) } dead.mu.Unlock() plt.mu.Lock() live := plt.inodes[inode] plt.mu.Unlock() if live == nil || live == dead { t.Fatalf("expected a fresh live il, got %v", live) } // A conflicting owner must see the new lock and be rejected. if s := plt.SetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 3, IsFlock: true}); s != fuse.EAGAIN { t.Fatalf("second owner should conflict with owner 2, got %v", s) } // GetLk must report the conflict as well: without the dead-recheck the // GetLk path would answer F_UNLCK off the orphaned il. var out fuse.LkOut plt.GetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 4, IsFlock: true}, &out) if out.Lk.Typ != syscall.F_WRLCK { t.Fatalf("GetLk should report the live conflict, got Typ=%d", out.Lk.Typ) } } // TestGetInodeLocksEvictsDeadEntry verifies that a dead inodeLocks which // somehow ends up in the map (e.g. through a future refactor that reorders // delete and dead=true) is dropped on read so callers never observe one. // This is the backstop that lets GetLk's and SetLk's retry loops terminate. func TestGetInodeLocksEvictsDeadEntry(t *testing.T) { plt := NewPosixLockTable() inode := uint64(42) lock := lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 1, IsFlock: true} if s := plt.SetLk(inode, lock); s != fuse.OK { t.Fatalf("prime SetLk: got %v", s) } dead := plt.getInodeLocks(inode) unlock := lock unlock.Typ = syscall.F_UNLCK if s := plt.SetLk(inode, unlock); s != fuse.OK { t.Fatalf("prime unlock: got %v", s) } if !dead.dead { t.Fatal("expected dead after cleanup") } // Force the broken state that production cannot reach but tests and // future refactors might: dead entry still in the map. plt.mu.Lock() plt.inodes[inode] = dead plt.mu.Unlock() if il := plt.getInodeLocks(inode); il != nil { t.Fatalf("getInodeLocks should drop a dead map entry, got %p", il) } plt.mu.Lock() _, stillMapped := plt.inodes[inode] plt.mu.Unlock() if stillMapped { t.Fatal("expected dead entry to be removed from the map") } // getOrCreateInodeLocks must also self-heal (replace the dead entry with // a fresh live one) so SetLk's retry path cannot spin. plt.mu.Lock() plt.inodes[inode] = dead plt.mu.Unlock() fresh := plt.getOrCreateInodeLocks(inode) if fresh == dead { t.Fatal("getOrCreateInodeLocks should not return a dead entry") } if fresh.dead { t.Fatal("fresh entry should not be dead") } } // TestConcurrentFlockChurnPreservesMutualExclusion is a stress companion to // the deterministic tests above. It uses a Swap+CAS detector that flags // overlap at two points (on acquire and on release), so a second granted // holder is caught even if it sneaks in after the first goroutine's claim // but before its release. 16 goroutines churn whole-file exclusive flock on // one inode; with the race the detector fires hundreds of times per run, // with the fix it stays at zero. func TestConcurrentFlockChurnPreservesMutualExclusion(t *testing.T) { plt := NewPosixLockTable() const ( inode = uint64(42) numWorkers = 16 iterations = 500 ) var ( wg sync.WaitGroup holder atomic.Int64 // 0 = nobody; otherwise = holder's claim token overlapSeen atomic.Int32 ) for w := 0; w < numWorkers; w++ { wg.Add(1) go func(id int) { defer wg.Done() owner := uint64(100 + id) lock := lockRange{ Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: owner, Pid: uint32(id + 1), IsFlock: true, } unlock := lock unlock.Typ = syscall.F_UNLCK token := int64(id + 1) for i := 0; i < iterations; i++ { // SetLk(WRLCK) may only return OK (granted) or EAGAIN // (conflict); anything else indicates a bug and the test // must fail rather than spin. Use Errorf + return because // Fatalf is not safe from a non-test goroutine. for { s := plt.SetLk(inode, lock) if s == fuse.OK { break } if s != fuse.EAGAIN { t.Errorf("worker %d iter %d: unexpected SetLk(WRLCK) status %v", id, i, s) return } runtime.Gosched() } // Claim the slot. If Swap observes a non-zero predecessor, // another goroutine already believes it holds the lock. if prev := holder.Swap(token); prev != 0 { overlapSeen.Add(1) } // Widen the window so a concurrently-granted peer has a // chance to race into its own Swap before we release. runtime.Gosched() runtime.Gosched() // Release the slot. If CAS fails someone else overwrote our // claim, which only happens when two holders raced. if !holder.CompareAndSwap(token, 0) { overlapSeen.Add(1) } if s := plt.SetLk(inode, unlock); s != fuse.OK { t.Errorf("worker %d iter %d: unexpected SetLk(UNLCK) status %v", id, i, s) return } } }(w) } wg.Wait() if n := overlapSeen.Load(); n != 0 { t.Fatalf("flock overlap detected %d times: two owners simultaneously granted the same exclusive lock", n) } }