diff options
Diffstat (limited to 'tests/cluster/poc_test.go')
| -rw-r--r-- | tests/cluster/poc_test.go | 230 |
1 files changed, 230 insertions, 0 deletions
diff --git a/tests/cluster/poc_test.go b/tests/cluster/poc_test.go new file mode 100644 index 0000000..52b9ec9 --- /dev/null +++ b/tests/cluster/poc_test.go @@ -0,0 +1,230 @@ +package cluster + +// poc_test.go contains proof of concept tests for the byte-range locking logic. +// This goes directly to an underlying filesystem without going through gocryptfs. + +import ( + "bytes" + "errors" + "io" + "os" + "sync" + "sync/atomic" + "syscall" + "testing" + + "golang.org/x/sys/unix" + + "github.com/rfjakob/gocryptfs/v2/internal/contentenc" + "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" + "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" +) + +// Check that byte-range locks work on an empty file +func TestPoCFcntlFlock(t *testing.T) { + path := test_helpers.TmpDir + "/" + t.Name() + + fd1, err := syscall.Open(path, syscall.O_CREAT|syscall.O_WRONLY|syscall.O_EXCL, 0600) + if err != nil { + t.Fatal(err) + } + defer syscall.Close(fd1) + + // F_OFD_SETLK locks on the same fd always succeed, so we have to + // open a 2nd time. + fd2, err := syscall.Open(path, syscall.O_RDWR, 0) + if err != nil { + t.Fatal(err) + } + defer syscall.Close(fd2) + + lk := unix.Flock_t{ + Type: unix.F_WRLCK, + Whence: unix.SEEK_SET, + Start: 0, + Len: 0, + } + err = unix.FcntlFlock(uintptr(fd1), syscallcompat.F_OFD_SETLK, &lk) + if err != nil { + t.Fatal(err) + } + err = unix.FcntlFlock(uintptr(fd2), syscallcompat.F_OFD_SETLK, &lk) + if err == nil { + t.Fatal("double-lock succeeded but should have failed") + } +} + +// See if we can get garbage data when the file header is read and written concurrently. +// We should get either 0 bytes or 18 correct bytes. +func TestPoCHeaderCreation(t *testing.T) { + path := test_helpers.TmpDir + "/" + t.Name() + var wg sync.WaitGroup + // I ran this with 10000 iteration and no problems to be seen. Let's not waste too + // much testing time. + const loops = 100 + + var stats struct { + readOk int64 + readEmpty int64 + writes int64 + } + + writeBuf := []byte("123456789012345678") + if len(writeBuf) != contentenc.HeaderLen { + t.Fatal("BUG wrong header length") + } + + writerThread := func() { + defer wg.Done() + for i := 0; i < loops; i++ { + if t.Failed() { + return + } + f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_EXCL, 0600) + if err != nil { + t.Errorf("BUG: this should not happen: open err=%v", err) + return + } + // Do like gocryptfs does and prealloc the 18 bytes + err = syscallcompat.EnospcPrealloc(int(f.Fd()), 0, contentenc.HeaderLen) + if err != nil { + t.Error(err) + } + + _, err = f.WriteAt(writeBuf, 0) + if err != nil { + t.Errorf("iteration %d: Pwrite: %v", i, err) + } + atomic.AddInt64(&stats.writes, 1) + f.Close() + syscall.Unlink(path) + } + } + + readerThread := func() { + defer wg.Done() + for i := 0; i < loops; i++ { + if t.Failed() { + return + } + f, err := os.OpenFile(path, os.O_RDONLY, 0600) + if errors.Is(err, os.ErrNotExist) { + continue + } + if err != nil { + t.Error(err) + return + } + readBuf := make([]byte, contentenc.HeaderLen) + _, err = f.ReadAt(readBuf, 0) + if errors.Is(err, io.EOF) { + atomic.AddInt64(&stats.readEmpty, 1) + goto close + } + if err != nil { + t.Errorf("iteration %d: ReadAt: %v", i, err) + goto close + } + if !bytes.Equal(writeBuf, readBuf) { + t.Errorf("iteration %d: corrupt data received: %x", i, readBuf) + goto close + } + atomic.AddInt64(&stats.readOk, 1) + close: + f.Close() + } + } + + wg.Add(2) + go writerThread() + go readerThread() + wg.Wait() + + t.Logf("readEmpty=%d readOk=%d writes=%d", stats.readEmpty, stats.readOk, stats.writes) +} + +// TestPoCTornWrite simulates what TestConcurrentCreate does. +// +// Fails on ext4, quoting https://stackoverflow.com/a/35256626 : +// > Linux 4.2.6 with ext4: update atomicity = 1 byte +// +// Passes on XFS. +func TestPoCTornWrite(t *testing.T) { + if os.Getenv("ASSUME_XFS") != "1" { + t.Skipf("This test is disabled by default because it fails unless on XFS.\n" + + "Run it like this: ASSUME_XFS=1 go test -run TestPoCTornWrite\n" + + "Choose a backing directory by setting TMPDIR.") + } + doTestPoCTornWrite(t, false) +} + +// Same as TestPoCTornWrite but uses fcntl byte range locks +func TestPoCTornWriteLocked(t *testing.T) { + doTestPoCTornWrite(t, true) +} + +func doTestPoCTornWrite(t *testing.T, locking bool) { + path := test_helpers.TmpDir + "/" + t.Name() + var wg sync.WaitGroup + const loops = 10000 + + writerThread := func() { + defer wg.Done() + for i := 0; i < loops; i++ { + if t.Failed() { + return + } + + f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600) + if err != nil { + t.Errorf("BUG: this should not happen: open err=%v", err) + return + } + + // Write + blockData := bytes.Repeat([]byte{byte(i)}, 42) + if locking { + lk := unix.Flock_t{ + Type: unix.F_WRLCK, + Whence: unix.SEEK_SET, + Start: 0, + Len: int64(len(blockData)), + } + if err := unix.FcntlFlock(uintptr(f.Fd()), syscallcompat.F_OFD_SETLKW, &lk); err != nil { + t.Error(err) + return + } + // No need to unlock, lock is implicitely dropped on fd close + } + if _, err = f.WriteAt(blockData, 0); err != nil { + t.Errorf("iteration %d: WriteAt: %v", i, err) + return + } + + // Readback and verify + readBuf := make([]byte, 100) + if n, err := f.ReadAt(readBuf, 0); err == io.EOF { + readBuf = readBuf[:n] + } else if err != nil { + t.Error(err) + return + } + if len(readBuf) != len(blockData) { + t.Error("wrong length") + return + } + for _, v := range readBuf { + if v != readBuf[0] { + t.Errorf("iteration %d: inconsistent block: %x", i, readBuf) + return + } + } + f.Close() + } + } + + wg.Add(2) + go writerThread() + go writerThread() + wg.Wait() +} |
