diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/cli/cli_test.go | 19 | ||||
| -rw-r--r-- | tests/defaults/main_test.go | 35 | ||||
| -rw-r--r-- | tests/matrix/main_test.go | 1 | ||||
| -rw-r--r-- | tests/matrix/statx_linux_test.go | 156 |
4 files changed, 211 insertions, 0 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/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/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)) + } + }) +} |
