aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorMarios Titas2026-07-21 20:27:38 +0300
committerJakob Unterwurzacher2026-07-22 21:57:57 +0200
commit790362e8f8eabd1bdf75f98328daf94cf623c600 (patch)
treefcebc1fa556e8335a2fc0aef5df2bc573fd064d1 /tests
parentb3b292a8a3c1478b769e213e0e590f38cb1b923c (diff)
fusefrontend: expose birth time via statxHEADmaster
Forward the backing filesystem's STATX_BTIME on Linux while preserving existing metadata translations and fallback behavior. On Linux 6.6 and newer, this allows statx consumers such as GNU stat to report birth time when the backing filesystem provides it. Older kernels and backing filesystems without birth-time support retain the existing behavior. No on-disk format change or migration is required. Reverse mode remains unchanged. Fixes #868. Related context: #732
Diffstat (limited to 'tests')
-rw-r--r--tests/matrix/statx_linux_test.go156
1 files changed, 156 insertions, 0 deletions
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))
+ }
+ })
+}