diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/cli/cli_test.go | 19 | ||||
| -rw-r--r-- | tests/cluster/cluster_test.go | 155 | ||||
| -rw-r--r-- | tests/cluster/poc_test.go | 230 | ||||
| -rw-r--r-- | tests/defaults/main_test.go | 35 | ||||
| -rw-r--r-- | tests/matrix/main_test.go | 1 | ||||
| -rw-r--r-- | tests/matrix/matrix_test.go | 15 | ||||
| -rw-r--r-- | tests/matrix/statx_linux_test.go | 156 | ||||
| -rw-r--r-- | tests/reverse/config_custom_test.go | 62 | ||||
| -rwxr-xr-x | tests/stress_tests/pjdfstest.bash | 34 | ||||
| -rw-r--r-- | tests/test_helpers/helpers.go | 2 |
10 files changed, 690 insertions, 19 deletions
diff --git a/tests/cli/cli_test.go b/tests/cli/cli_test.go index 01cc3b7..472941d 100644 --- a/tests/cli/cli_test.go +++ b/tests/cli/cli_test.go @@ -991,6 +991,25 @@ func TestInitNotEmpty(t *testing.T) { } } +// TestReaddirplus checks that mounting and listing work with -readdirplus. +func TestReaddirplus(t *testing.T) { + dir := test_helpers.InitFS(t) + mnt := dir + ".mnt" + test_helpers.MountOrFatal(t, dir, mnt, "-extpass=echo test", "-readdirplus") + defer test_helpers.UnmountPanic(mnt) + + if err := os.WriteFile(mnt+"/file", nil, 0600); err != nil { + t.Fatal(err) + } + entries, err := os.ReadDir(mnt) + if err != nil { + t.Fatal(err) + } + if len(entries) != 1 || entries[0].Name() != "file" { + t.Fatalf("unexpected directory entries: %v", entries) + } +} + // TestSharedstorage checks that `-sharedstorage` shows stable inode numbers to // userspace despite having hard link tracking disabled func TestSharedstorage(t *testing.T) { diff --git a/tests/cluster/cluster_test.go b/tests/cluster/cluster_test.go index af93bc4..70a2a02 100644 --- a/tests/cluster/cluster_test.go +++ b/tests/cluster/cluster_test.go @@ -7,44 +7,47 @@ package cluster_test import ( "bytes" + "errors" + "io" "math/rand" "os" "sync" + "syscall" "testing" "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" ) -// This test passes on XFS but fails on ext4 and tmpfs!!! +// With -sharedstorage (i.e. with fcntl byte-range locks) this test passes on all +// filesystems. Without, it passes on XFS but fails on ext4 and tmpfs. // // Quoting https://lists.samba.org/archive/samba-technical/2019-March/133050.html // // > It turns out that xfs respects POSIX w.r.t "atomic read/write" and // > this is implemented by taking a file-wide shared lock on every // > buffered read. -// > This behavior is unique to XFS on Linux and is not optional. -// > Other Linux filesystems only guaranty page level atomicity for -// > buffered read/write. +// +// Note that ext4 actually provides NO ATOMICITY AT ALL. +// Quoting https://stackoverflow.com/a/35256626 : +// +// > Linux 4.2.6 with ext4: update atomicity = 1 byte +// +// TestPoCTornWrite in this package confirms this. // // See also: // - https://lore.kernel.org/linux-xfs/20190325001044.GA23020@dastard/ // Dave Chinner: XFS is the only linux filesystem that provides this behaviour. func TestClusterConcurrentRW(t *testing.T) { - if os.Getenv("ENABLE_CLUSTER_TEST") != "1" { - t.Skipf("This test is disabled by default because it fails unless on XFS.\n" + - "Run it like this: ENABLE_CLUSTER_TEST=1 go test\n" + - "Choose a backing directory by setting TMPDIR.") - } - - const blocksize = 4096 - const fileSize = 25 * blocksize // 100 kiB + const fileSize = 100000 // arbitrary unaligned size with a partial block at the end + const writeSize = 5000 // arbitrary unaligned size that touches two ciphertext blocks + const readSize = 5000 cDir := test_helpers.InitFS(t) mnt1 := cDir + ".mnt1" mnt2 := cDir + ".mnt2" - test_helpers.MountOrFatal(t, cDir, mnt1, "-extpass=echo test", "-wpanic=0") + test_helpers.MountOrFatal(t, cDir, mnt1, "-extpass=echo test", "-wpanic=0", "-sharedstorage") defer test_helpers.UnmountPanic(mnt1) - test_helpers.MountOrFatal(t, cDir, mnt2, "-extpass=echo test", "-wpanic=0") + test_helpers.MountOrFatal(t, cDir, mnt2, "-extpass=echo test", "-wpanic=0", "-sharedstorage") defer test_helpers.UnmountPanic(mnt2) f1, err := os.Create(mnt1 + "/foo") @@ -68,12 +71,12 @@ func TestClusterConcurrentRW(t *testing.T) { const loops = 10000 writeThread := func(f *os.File) { defer wg.Done() - buf := make([]byte, blocksize) + buf := make([]byte, writeSize) for i := 0; i < loops; i++ { if t.Failed() { return } - off := rand.Int63n(fileSize / blocksize) + off := rand.Int63n(int64(fileSize - len(buf) - 1)) _, err := f.WriteAt(buf, off) if err != nil { t.Errorf("writeThread iteration %d: WriteAt failed: %v", i, err) @@ -83,13 +86,13 @@ func TestClusterConcurrentRW(t *testing.T) { } readThread := func(f *os.File) { defer wg.Done() - zeroBlock := make([]byte, blocksize) - buf := make([]byte, blocksize) + zeroBlock := make([]byte, readSize) + buf := make([]byte, len(zeroBlock)) for i := 0; i < loops; i++ { if t.Failed() { return } - off := rand.Int63n(fileSize / blocksize) + off := rand.Int63n(int64(fileSize - len(zeroBlock) - 1)) _, err := f.ReadAt(buf, off) if err != nil { t.Errorf("readThread iteration %d: ReadAt failed: %v", i, err) @@ -109,3 +112,117 @@ func TestClusterConcurrentRW(t *testing.T) { go readThread(f2) wg.Wait() } + +// Multiple hosts creating the same file at the same time could +// overwrite each other's file header, leading to data +// corruption. Passing "-sharedstorage" should prevent this. +func TestConcurrentCreate(t *testing.T) { + cDir := test_helpers.InitFS(t) + mnt1 := cDir + ".mnt1" + mnt2 := cDir + ".mnt2" + test_helpers.MountOrFatal(t, cDir, mnt1, "-extpass=echo test", "-wpanic=0", "-sharedstorage") + defer test_helpers.UnmountPanic(mnt1) + test_helpers.MountOrFatal(t, cDir, mnt2, "-extpass=echo test", "-wpanic=0", "-sharedstorage") + defer test_helpers.UnmountPanic(mnt2) + + var wg sync.WaitGroup + const loops = 10000 + + createOrOpen := func(path string) (f *os.File, err error) { + // Use the high-level os.Create/OpenFile instead of syscall.Open because we + // *want* Go's EINTR retry logic. glibc open(2) has similar logic. + f, err = os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_EXCL, 0600) + if err == nil { + return + } + if !errors.Is(err, os.ErrExist) { + t.Logf("POSIX compliance issue: exclusive create failed with unexpected error: err=%v", errors.Unwrap(err)) + } + f, err = os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600) + if err == nil { + return + } + t.Logf("POSIX compliance issue: non-exlusive create failed with err=%v", errors.Unwrap(err)) + return + } + + workerThread := func(path string) { + defer wg.Done() + buf := make([]byte, 10) + for i := 0; i < loops; i++ { + if t.Failed() { + return + } + f, err := createOrOpen(path) + if err != nil { + // retry + continue + } + defer f.Close() + _, err = f.WriteAt(buf, 0) + if err != nil { + t.Errorf("iteration %d: Pwrite: %v", i, err) + return + } + buf2 := make([]byte, len(buf)+1) + n, err := f.ReadAt(buf2, 0) + if err != nil && err != io.EOF { + t.Errorf("iteration %d: ReadAt: %v", i, err) + return + } + buf2 = buf2[:n] + if !bytes.Equal(buf, buf2) { + t.Errorf("iteration %d: corrupt data received: %x", i, buf2) + return + } + syscall.Unlink(path) + + // Close now (not only when the whole loop exists) to avoid exhausting fds. + // Double-close on happy path is harmless: "Close will return an error if it has already been called" + f.Close() + } + } + + wg.Add(2) + go workerThread(mnt1 + "/foo") + go workerThread(mnt2 + "/foo") + wg.Wait() +} + +// Check that opening with O_CREATE|O_TRUNC and writing always works +func TestOpenTruncate(t *testing.T) { + cDir := test_helpers.InitFS(t) + mnt1 := cDir + ".mnt1" + mnt2 := cDir + ".mnt2" + test_helpers.MountOrFatal(t, cDir, mnt1, "-extpass=echo test", "-wpanic=0", "-sharedstorage") + defer test_helpers.UnmountPanic(mnt1) + test_helpers.MountOrFatal(t, cDir, mnt2, "-extpass=echo test", "-wpanic=0", "-sharedstorage") + defer test_helpers.UnmountPanic(mnt2) + + var wg sync.WaitGroup + const loops = 100 + + writerThread := func(path string) { + defer wg.Done() + for i := 0; i < loops; i++ { + if t.Failed() { + return + } + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + t.Logf("POSIX compliance issue: non-exlusive create failed with err=%v", errors.Unwrap(err)) + continue + } + _, err = f.WriteAt([]byte("foo"), 0) + if err != nil { + t.Errorf("iteration %d: WriteAt: %v", i, err) + } + f.Close() + } + } + + wg.Add(2) + go writerThread(mnt1 + "/foo") + go writerThread(mnt2 + "/foo") + wg.Wait() +} 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() +} diff --git a/tests/defaults/main_test.go b/tests/defaults/main_test.go index a19f079..237dbd1 100644 --- a/tests/defaults/main_test.go +++ b/tests/defaults/main_test.go @@ -554,3 +554,38 @@ func TestSeekDir(t *testing.T) { t.Error("Seek did not have any effect") } } + +// Regression test for https://github.com/rfjakob/gocryptfs/issues/1024 +// +// truncate(2) goes through node.Setattr, which opens its own file handle. That +// handle must take ContentLock like file.Setattr does: the lock doubles as the +// global write-operation counter that isConsecutiveWrite() uses to notice that +// somebody else changed the file. Without it, an already-open handle keeps +// believing its next write appends, skips writePadHole(), and leaves the last +// block short while the file grows past it - the block then fails to decrypt. +func TestConcurrentTruncateViaPath(t *testing.T) { + path := test_helpers.DefaultPlainDir + "/" + t.Name() + f, err := os.Create(path) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + b := make([]byte, 4096) + _, err = f.Write(b) + if err != nil { + t.Fatal(err) + } + // Truncate via path used to not increment writeOpCount... + if err = os.Truncate(path, 8); err != nil { + t.Fatal(err) + } + // ...which means this write will not call writePadHole. + if _, err = f.Write([]byte("foo")); err != nil { + t.Fatal(err) + } + // First block is corrupt now. + if _, err = f.ReadAt(b, 0); err != nil { + t.Fatal(err) + } +} diff --git a/tests/matrix/main_test.go b/tests/matrix/main_test.go index cf0b6c4..126adaf 100644 --- a/tests/matrix/main_test.go +++ b/tests/matrix/main_test.go @@ -59,6 +59,7 @@ func TestMain(m *testing.M) { {false, "auto", false, true, nil}, // -serialize_reads {false, "auto", false, false, []string{"-serialize_reads"}}, + {false, "auto", false, false, []string{"-readdirplus"}}, {false, "auto", false, false, []string{"-sharedstorage"}}, {false, "auto", false, false, []string{"-deterministic-names"}}, // Test xchacha with and without openssl diff --git a/tests/matrix/matrix_test.go b/tests/matrix/matrix_test.go index a2ec549..2f097fb 100644 --- a/tests/matrix/matrix_test.go +++ b/tests/matrix/matrix_test.go @@ -993,3 +993,18 @@ func TestRenameExchangeOnGocryptfs(t *testing.T) { t.Errorf("file2 content wrong after exchange. Expected: %s, Got: %s", content1, newContent2) } } + +func TestUnlinkedO_SYNC(t *testing.T) { + path := test_helpers.DefaultPlainDir + "/" + t.Name() + fd, err := syscall.Open(path, syscall.O_CREAT|syscall.O_RDWR|syscall.O_SYNC, 0600) + if err != nil { + t.Fatal(err) + } + defer syscall.Close(fd) + if err = os.Remove(path); err != nil { + t.Fatal(err) + } + if _, err = syscall.Write(fd, make([]byte, 10)); err != nil { + t.Error(err) + } +} diff --git a/tests/matrix/statx_linux_test.go b/tests/matrix/statx_linux_test.go new file mode 100644 index 0000000..4d62663 --- /dev/null +++ b/tests/matrix/statx_linux_test.go @@ -0,0 +1,156 @@ +package matrix + +import ( + "os" + "path/filepath" + "strconv" + "strings" + "testing" + "time" + + "golang.org/x/sys/unix" + + "github.com/rfjakob/gocryptfs/v2/ctlsock" + "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" +) + +const testStatxMask = unix.STATX_BASIC_STATS | unix.STATX_BTIME + +func statxAt(t *testing.T, dirfd int, path string, flags int) unix.Statx_t { + t.Helper() + var st unix.Statx_t + if err := unix.Statx(dirfd, path, flags, testStatxMask, &st); err != nil { + t.Fatal(err) + } + return st +} + +func encryptedPath(t *testing.T, plainPath string) string { + t.Helper() + resp := test_helpers.QueryCtlSock(t, ctlsockPath, ctlsock.RequestStruct{ + EncryptPath: plainPath, + }) + if resp.Result == "" { + t.Fatal(resp) + } + return filepath.Join(test_helpers.DefaultCipherDir, resp.Result) +} + +func requireFuseStatx(t *testing.T) { + t.Helper() + data, err := os.ReadFile("/proc/sys/kernel/osrelease") + if err != nil { + t.Fatal(err) + } + parts := strings.SplitN(strings.TrimSpace(string(data)), ".", 3) + if len(parts) < 2 { + t.Skipf("cannot parse kernel release %q", data) + } + major, err := strconv.Atoi(parts[0]) + if err != nil { + t.Skipf("cannot parse kernel release %q: %v", data, err) + } + minorString := parts[1] + if i := strings.IndexFunc(minorString, func(r rune) bool { + return r < '0' || r > '9' + }); i >= 0 { + minorString = minorString[:i] + } + minor, err := strconv.Atoi(minorString) + if err != nil { + t.Skipf("cannot parse kernel release %q: %v", data, err) + } + if major < 6 || major == 6 && minor < 6 { + t.Skip("FUSE_STATX requires Linux 6.6 or newer") + } +} + +func checkBtime(t *testing.T, plainPath string, cipherPath string, flags int) unix.Statx_t { + t.Helper() + cipherSt := statxAt(t, unix.AT_FDCWD, cipherPath, flags) + if cipherSt.Mask&unix.STATX_BTIME == 0 { + t.Skip("backing filesystem does not report STATX_BTIME") + } + plainSt := statxAt(t, unix.AT_FDCWD, plainPath, flags) + if plainSt.Mask&unix.STATX_BTIME == 0 { + t.Fatalf("mounted filesystem did not report STATX_BTIME: mask=%#x", plainSt.Mask) + } + if plainSt.Btime.Sec != cipherSt.Btime.Sec || plainSt.Btime.Nsec != cipherSt.Btime.Nsec { + t.Errorf("birth time mismatch: plain=%d.%09d cipher=%d.%09d", + plainSt.Btime.Sec, plainSt.Btime.Nsec, + cipherSt.Btime.Sec, cipherSt.Btime.Nsec) + } + return plainSt +} + +func TestStatxBtime(t *testing.T) { + requireFuseStatx(t) + + t.Run("root", func(t *testing.T) { + checkBtime(t, test_helpers.DefaultPlainDir, test_helpers.DefaultCipherDir, unix.AT_SYMLINK_NOFOLLOW) + }) + + t.Run("regular", func(t *testing.T) { + const content = "statx birth time" + relPath := strings.ReplaceAll(t.Name(), "/", "_") + plainPath := filepath.Join(test_helpers.DefaultPlainDir, relPath) + if err := os.WriteFile(plainPath, []byte(content), 0600); err != nil { + t.Fatal(err) + } + cipherPath := encryptedPath(t, relPath) + + before := checkBtime(t, plainPath, cipherPath, unix.AT_SYMLINK_NOFOLLOW) + if before.Size != uint64(len(content)) { + t.Errorf("wrong plaintext size: have=%d want=%d", before.Size, len(content)) + } + + // Check user-visible AT_EMPTY_PATH behavior. Current Linux kernels do + // not send the file handle in FUSE_STATX, so this still reaches + // Node.Statx rather than File.Statx. + f, err := os.Open(plainPath) + if err != nil { + t.Fatal(err) + } + defer f.Close() + fdSt := statxAt(t, int(f.Fd()), "", unix.AT_EMPTY_PATH) + if fdSt.Mask&unix.STATX_BTIME == 0 { + t.Fatalf("statx on open file did not report STATX_BTIME: mask=%#x", fdSt.Mask) + } + if fdSt.Btime.Sec != before.Btime.Sec || fdSt.Btime.Nsec != before.Btime.Nsec { + t.Errorf("statx on open file returned different birth time: path=%d.%09d fd=%d.%09d", + before.Btime.Sec, before.Btime.Nsec, fdSt.Btime.Sec, fdSt.Btime.Nsec) + } + + now := time.Now().Add(-time.Hour) + if err := os.Chtimes(plainPath, now, now); err != nil { + t.Fatal(err) + } + after := checkBtime(t, plainPath, cipherPath, unix.AT_SYMLINK_NOFOLLOW) + if after.Btime.Sec != before.Btime.Sec || after.Btime.Nsec != before.Btime.Nsec { + t.Errorf("birth time changed with mtime: before=%d.%09d after=%d.%09d", + before.Btime.Sec, before.Btime.Nsec, after.Btime.Sec, after.Btime.Nsec) + } + }) + + t.Run("directory", func(t *testing.T) { + relPath := strings.ReplaceAll(t.Name(), "/", "_") + plainPath := filepath.Join(test_helpers.DefaultPlainDir, relPath) + if err := os.Mkdir(plainPath, 0700); err != nil { + t.Fatal(err) + } + checkBtime(t, plainPath, encryptedPath(t, relPath), unix.AT_SYMLINK_NOFOLLOW) + }) + + t.Run("symlink", func(t *testing.T) { + const target = "/target/does/not/exist" + relPath := strings.ReplaceAll(t.Name(), "/", "_") + plainPath := filepath.Join(test_helpers.DefaultPlainDir, relPath) + if err := os.Symlink(target, plainPath); err != nil { + t.Fatal(err) + } + st := checkBtime(t, plainPath, encryptedPath(t, relPath), unix.AT_SYMLINK_NOFOLLOW) + if st.Size != uint64(len(target)) { + t.Errorf("wrong symlink size: have=%d want=%d", st.Size, len(target)) + } + }) +} diff --git a/tests/reverse/config_custom_test.go b/tests/reverse/config_custom_test.go new file mode 100644 index 0000000..a7883af --- /dev/null +++ b/tests/reverse/config_custom_test.go @@ -0,0 +1,62 @@ +package reverse_test + +import ( + "os" + "os/exec" + "testing" + + "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" +) + +// TestConfigCustomInsideCipherdir verifies that a custom config file (-config) +// located inside CIPHERDIR is hidden from the encrypted reverse view instead of +// leaking there in encrypted form. +// +// Regression test for https://github.com/rfjakob/gocryptfs/issues/1009 +func TestConfigCustomInsideCipherdir(t *testing.T) { + backingDir, err := os.MkdirTemp(test_helpers.TmpDir, t.Name()+".") + if err != nil { + t.Fatal(err) + } + // A regular file that must stay visible in the encrypted view. + if err := os.WriteFile(backingDir+"/secret.txt", []byte("hello"), 0600); err != nil { + t.Fatal(err) + } + // The custom config file lives *inside* the backing dir. + cfg := backingDir + "/my-custom.conf" + + // Init reverse fs with the config at the custom location. + initArgs := []string{"-q", "-init", "-reverse", "-extpass", "echo test", "-scryptn=10", "-config", cfg} + if plaintextnames { + initArgs = append(initArgs, "-plaintextnames") + } else if deterministic_names { + initArgs = append(initArgs, "-deterministic-names") + } + initArgs = append(initArgs, backingDir) + cmd := exec.Command(test_helpers.GocryptfsBinary, initArgs...) + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + if err := cmd.Run(); err != nil { + t.Fatalf("init failed: %v", err) + } + + mnt := backingDir + ".mnt" + sock := mnt + ".sock" + test_helpers.MountOrFatal(t, backingDir, mnt, "-reverse", "-extpass", "echo test", + "-config", cfg, "-ctlsock", sock) + defer test_helpers.UnmountPanic(mnt) + + // The custom config file must NOT show up (encrypted) in the view. + cCfg := ctlsockEncryptPath(t, sock, "my-custom.conf") + if test_helpers.VerifyExistence(t, mnt+"/"+cCfg) { + t.Errorf("custom config %q is exposed in the encrypted view (as %q), but should be hidden", cfg, cCfg) + } + // With a custom config file, no virtual gocryptfs.conf is presented. + if test_helpers.VerifyExistence(t, mnt+"/gocryptfs.conf") { + t.Errorf("gocryptfs.conf should not be present in the view when -config is used") + } + // Regular files must remain visible. + cSecret := ctlsockEncryptPath(t, sock, "secret.txt") + if !test_helpers.VerifyExistence(t, mnt+"/"+cSecret) { + t.Errorf("regular file secret.txt (as %q) is missing from the encrypted view", cSecret) + } +} diff --git a/tests/stress_tests/pjdfstest.bash b/tests/stress_tests/pjdfstest.bash new file mode 100755 index 0000000..a786e41 --- /dev/null +++ b/tests/stress_tests/pjdfstest.bash @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# +# Mount a gocryptfs filesystem in /var/tmp and run pjdfstest against it + +set -eu + +export TMPDIR=${TMPDIR:-/var/tmp} + +cd "$(dirname "$0")" +MYNAME=$(basename "$0") +source ../fuse-unmount.bash + +pjdfstest_dir=$(realpath ../../../pjdfstest) +if [[ ! -x $pjdfstest_dir/pjdfstest ]] +then + echo "$MYNAME: pjdfstest binary not found at $pjdfstest_dir/pjdfstest" + echo "Please clone and build https://github.com/pjd/pjdfstest" + exit 1 +fi + +# Backing directory + mountpoint +DIR=$(mktemp -d "$TMPDIR/$MYNAME.XXX") +MNT="$DIR.mnt" +mkdir "$MNT" + +../../gocryptfs -q -init -extpass "echo test" -scryptn=10 "$DIR" +sudo ../../gocryptfs -q -extpass "echo test" -nosyslog -allow_other "$DIR" "$MNT" +cd "$MNT" + +# Cleanup trap +trap "cd /tmp ; fuse-unmount -z $MNT" EXIT + +echo "Starting pjdfstest" +sudo prove -r "$pjdfstest_dir/tests"
\ No newline at end of file diff --git a/tests/test_helpers/helpers.go b/tests/test_helpers/helpers.go index c8cd151..fd2ea9c 100644 --- a/tests/test_helpers/helpers.go +++ b/tests/test_helpers/helpers.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "syscall" "testing" @@ -146,6 +147,7 @@ func InitFS(t *testing.T, extraArgs ...string) string { prefix := "x." if t != nil { prefix = t.Name() + "." + prefix = strings.ReplaceAll(prefix, "/", "_") } dir, err := os.MkdirTemp(TmpDir, prefix) if err != nil { |
