diff options
| author | Ankush Patel | 2026-02-14 03:41:23 +1300 |
|---|---|---|
| committer | Ankush Patel | 2026-02-14 03:41:23 +1300 |
| commit | 8d8fb15f0b3680add1f3b28c062b573a92221ab0 (patch) | |
| tree | fbd54302af652c38eb292167919125a8b57f5d2a | |
| parent | 903fc9d077a81d9224de4207d1672c0b1127cf42 (diff) | |
| parent | 5f5c34ac78cb9d1765ce9cabe87420c32f9d867e (diff) | |
Merge branch 'master' into freebsd-support
| -rw-r--r-- | internal/fusefrontend/node_open_create.go | 29 | ||||
| -rw-r--r-- | internal/fusefrontend/node_xattr_darwin.go | 14 | ||||
| -rw-r--r-- | internal/fusefrontend/root_node.go | 29 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/node_helpers.go | 2 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/node_xattr.go | 2 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/root_node.go | 4 | ||||
| -rw-r--r-- | internal/syscallcompat/quirks_linux.go | 40 | ||||
| -rw-r--r-- | internal/syscallcompat/sys_common.go | 8 | ||||
| -rw-r--r-- | internal/syscallcompat/sys_darwin.go | 4 | ||||
| -rw-r--r-- | internal/syscallcompat/sys_freebsd.go | 3 | ||||
| -rw-r--r-- | internal/syscallcompat/sys_linux.go | 4 | ||||
| -rwxr-xr-x | profiling/tinyfiles.bash | 33 | ||||
| -rw-r--r-- | tests/matrix/symlink_darwin_test.go | 39 | ||||
| -rw-r--r-- | tests/matrix/symlink_linux_test.go | 47 | ||||
| -rw-r--r-- | tests/reverse/xattr_test.go | 16 | ||||
| -rw-r--r-- | tests/root_test/btrfs_test.go | 63 |
16 files changed, 284 insertions, 53 deletions
diff --git a/internal/fusefrontend/node_open_create.go b/internal/fusefrontend/node_open_create.go index 9598559..622d5dc 100644 --- a/internal/fusefrontend/node_open_create.go +++ b/internal/fusefrontend/node_open_create.go @@ -2,6 +2,7 @@ package fusefrontend import ( "context" + "os" "syscall" "github.com/hanwen/go-fuse/v2/fs" @@ -12,6 +13,30 @@ import ( "github.com/rfjakob/gocryptfs/v2/internal/tlog" ) +// mangleOpenCreateFlags is used by Create() and Open() to convert the open flags the user +// wants to the flags we internally use to open the backing file using Openat(). +// The returned flags always contain O_NOFOLLOW/O_SYMLINK. +func mangleOpenCreateFlags(flags uint32) (newFlags int) { + newFlags = int(flags) + // Convert WRONLY to RDWR. We always need read access to do read-modify-write cycles. + if (newFlags & syscall.O_ACCMODE) == syscall.O_WRONLY { + newFlags = newFlags ^ os.O_WRONLY | os.O_RDWR + } + // We also cannot open the file in append mode, we need to seek back for RMW + newFlags = newFlags &^ os.O_APPEND + // O_DIRECT accesses must be aligned in both offset and length. Due to our + // crypto header, alignment will be off, even if userspace makes aligned + // accesses. Running xfstests generic/013 on ext4 used to trigger lots of + // EINVAL errors due to missing alignment. Just fall back to buffered IO. + newFlags = newFlags &^ syscallcompat.O_DIRECT + // Create and Open are two separate FUSE operations, so O_CREAT should usually not + // be part of the Open() flags. Create() will add O_CREAT back itself. + newFlags = newFlags &^ syscall.O_CREAT + // We always want O_NOFOLLOW/O_SYMLINK to be safe against symlink races + newFlags |= syscallcompat.OpenatFlagNofollowSymlink + return newFlags +} + // Open - FUSE call. Open already-existing file. // // Symlink-safe through Openat(). @@ -23,7 +48,7 @@ func (n *Node) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFl defer syscall.Close(dirfd) rn := n.rootNode() - newFlags := rn.mangleOpenFlags(flags) + newFlags := mangleOpenCreateFlags(flags) // Taking this lock makes sure we don't race openWriteOnlyFile() rn.openWriteOnlyLock.RLock() defer rn.openWriteOnlyLock.RUnlock() @@ -71,7 +96,7 @@ func (n *Node) Create(ctx context.Context, name string, flags uint32, mode uint3 if !rn.args.PreserveOwner { ctx = nil } - newFlags := rn.mangleOpenFlags(flags) + newFlags := mangleOpenCreateFlags(flags) // Handle long file name ctx2 := toFuseCtx(ctx) if !rn.args.PlaintextNames && nametransform.IsLongContent(cName) { diff --git a/internal/fusefrontend/node_xattr_darwin.go b/internal/fusefrontend/node_xattr_darwin.go index f8f224f..1d25f3d 100644 --- a/internal/fusefrontend/node_xattr_darwin.go +++ b/internal/fusefrontend/node_xattr_darwin.go @@ -29,8 +29,8 @@ func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) { } defer syscall.Close(dirfd) - // O_NONBLOCK to not block on FIFOs. - fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + // O_NONBLOCK to not block on FIFOs, O_SYMLINK to open the symlink itself (if it is one). + fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0) if err != nil { return nil, fs.ToErrno(err) } @@ -52,10 +52,10 @@ func (n *Node) setXAttr(context *fuse.Context, cAttr string, cData []byte, flags defer syscall.Close(dirfd) // O_NONBLOCK to not block on FIFOs. - fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0) // Directories cannot be opened read-write. Retry. if err == syscall.EISDIR { - fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0) } if err != nil { fs.ToErrno(err) @@ -74,10 +74,10 @@ func (n *Node) removeXAttr(cAttr string) (errno syscall.Errno) { defer syscall.Close(dirfd) // O_NONBLOCK to not block on FIFOs. - fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0) // Directories cannot be opened read-write. Retry. if err == syscall.EISDIR { - fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0) } if err != nil { return fs.ToErrno(err) @@ -96,7 +96,7 @@ func (n *Node) listXAttr() (out []string, errno syscall.Errno) { defer syscall.Close(dirfd) // O_NONBLOCK to not block on FIFOs. - fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0) if err != nil { return nil, fs.ToErrno(err) } diff --git a/internal/fusefrontend/root_node.go b/internal/fusefrontend/root_node.go index 489e3c6..38d070d 100644 --- a/internal/fusefrontend/root_node.go +++ b/internal/fusefrontend/root_node.go @@ -1,7 +1,6 @@ package fusefrontend import ( - "os" "strings" "sync" "sync/atomic" @@ -93,7 +92,9 @@ func NewRootNode(args Args, c *contentenc.ContentEnc, n *nametransform.NameTrans } // Suppress the message if the user has already specified -noprealloc if rn.quirks&syscallcompat.QuirkBtrfsBrokenFalloc != 0 && !args.NoPrealloc { - syscallcompat.LogQuirk("Btrfs detected, forcing -noprealloc. See https://github.com/rfjakob/gocryptfs/issues/395 for why.") + syscallcompat.LogQuirk("Btrfs detected, forcing -noprealloc. " + + "Use \"chattr +C\" on the backing directory to enable NOCOW and allow preallocation. " + + "See https://github.com/rfjakob/gocryptfs/issues/395 for details.") } if statErr == nil { rn.inoMap.TranslateStat(&st) @@ -108,30 +109,6 @@ func (rn *RootNode) AfterUnmount() { rn.dirCache.stats() } -// mangleOpenFlags is used by Create() and Open() to convert the open flags the user -// wants to the flags we internally use to open the backing file. -// The returned flags always contain O_NOFOLLOW. -func (rn *RootNode) mangleOpenFlags(flags uint32) (newFlags int) { - newFlags = int(flags) - // Convert WRONLY to RDWR. We always need read access to do read-modify-write cycles. - if (newFlags & syscall.O_ACCMODE) == syscall.O_WRONLY { - newFlags = newFlags ^ os.O_WRONLY | os.O_RDWR - } - // We also cannot open the file in append mode, we need to seek back for RMW - newFlags = newFlags &^ os.O_APPEND - // O_DIRECT accesses must be aligned in both offset and length. Due to our - // crypto header, alignment will be off, even if userspace makes aligned - // accesses. Running xfstests generic/013 on ext4 used to trigger lots of - // EINVAL errors due to missing alignment. Just fall back to buffered IO. - newFlags = newFlags &^ syscallcompat.O_DIRECT - // Create and Open are two separate FUSE operations, so O_CREAT should not - // be part of the open flags. - newFlags = newFlags &^ syscall.O_CREAT - // We always want O_NOFOLLOW to be safe against symlink races - newFlags |= syscall.O_NOFOLLOW - return newFlags -} - // reportMitigatedCorruption is used to report a corruption that was transparently // mitigated and did not return an error to the user. Pass the name of the corrupt // item (filename for OpenDir(), xattr name for ListXAttr() etc). diff --git a/internal/fusefrontend_reverse/node_helpers.go b/internal/fusefrontend_reverse/node_helpers.go index 3165db6..f733689 100644 --- a/internal/fusefrontend_reverse/node_helpers.go +++ b/internal/fusefrontend_reverse/node_helpers.go @@ -134,7 +134,7 @@ func (n *Node) lookupLongnameName(ctx context.Context, nameFile string, out *fus if errno != 0 { return } - if rn.isExcludedPlain(filepath.Join(d.cPath, pName)) { + if rn.isExcludedPlain(filepath.Join(d.pPath, pName)) { errno = syscall.EPERM return } diff --git a/internal/fusefrontend_reverse/node_xattr.go b/internal/fusefrontend_reverse/node_xattr.go index f22764a..b940339 100644 --- a/internal/fusefrontend_reverse/node_xattr.go +++ b/internal/fusefrontend_reverse/node_xattr.go @@ -40,7 +40,7 @@ func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, } else { pAttr, err := rn.decryptXattrName(attr) if err != nil { - return 0, syscall.EINVAL + return 0, noSuchAttributeError } pData, errno := n.getXAttr(pAttr) if errno != 0 { diff --git a/internal/fusefrontend_reverse/root_node.go b/internal/fusefrontend_reverse/root_node.go index 420ed22..1a668af 100644 --- a/internal/fusefrontend_reverse/root_node.go +++ b/internal/fusefrontend_reverse/root_node.go @@ -13,6 +13,7 @@ import ( "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" + "github.com/rfjakob/gocryptfs/v2/internal/configfile" "github.com/rfjakob/gocryptfs/v2/internal/contentenc" "github.com/rfjakob/gocryptfs/v2/internal/exitcodes" "github.com/rfjakob/gocryptfs/v2/internal/fusefrontend" @@ -136,7 +137,8 @@ func (rn *RootNode) findLongnameParent(fd int, diriv []byte, longname string) (p // excluded (used when -exclude is passed by the user). func (rn *RootNode) isExcludedPlain(pPath string) bool { // root dir can't be excluded - if pPath == "" { + // Don't exclude gocryptfs.conf too + if pPath == "" || pPath == configfile.ConfReverseName { return false } return rn.excluder != nil && rn.excluder.MatchesPath(pPath) diff --git a/internal/syscallcompat/quirks_linux.go b/internal/syscallcompat/quirks_linux.go index 87af03d..35f754d 100644 --- a/internal/syscallcompat/quirks_linux.go +++ b/internal/syscallcompat/quirks_linux.go @@ -1,11 +1,38 @@ package syscallcompat import ( + "syscall" + "golang.org/x/sys/unix" "github.com/rfjakob/gocryptfs/v2/internal/tlog" ) +// FS_NOCOW_FL is the flag set by "chattr +C" to disable copy-on-write on +// btrfs. Not exported by golang.org/x/sys/unix, value from linux/fs.h. +const FS_NOCOW_FL = 0x00800000 + +// dirHasNoCow checks whether the directory at the given path has the +// NOCOW (No Copy-on-Write) attribute set (i.e. "chattr +C"). +// When a directory has this attribute, files created within it inherit +// NOCOW, which makes fallocate work correctly on btrfs because writes +// go in-place rather than through COW. +func dirHasNoCow(path string) bool { + fd, err := syscall.Open(path, syscall.O_RDONLY|syscall.O_DIRECTORY, 0) + if err != nil { + tlog.Debug.Printf("dirHasNoCow: Open %q failed: %v", path, err) + return false + } + defer syscall.Close(fd) + + flags, err := unix.IoctlGetInt(fd, unix.FS_IOC_GETFLAGS) + if err != nil { + tlog.Debug.Printf("dirHasNoCow: FS_IOC_GETFLAGS on %q failed: %v", path, err) + return false + } + return flags&FS_NOCOW_FL != 0 +} + // DetectQuirks decides if there are known quirks on the backing filesystem // that need to be workarounded. // @@ -21,10 +48,19 @@ func DetectQuirks(cipherdir string) (q uint64) { // Preallocation on Btrfs is broken ( https://github.com/rfjakob/gocryptfs/issues/395 ) // and slow ( https://github.com/rfjakob/gocryptfs/issues/63 ). // + // The root cause is that btrfs COW allocates new blocks on write even for + // preallocated extents, defeating the purpose of fallocate. However, if the + // backing directory has the NOCOW attribute (chattr +C), writes go in-place + // and fallocate works correctly. + // // Cast to uint32 avoids compile error on arm: "constant 2435016766 overflows int32" if uint32(st.Type) == unix.BTRFS_SUPER_MAGIC { - // LogQuirk is called in fusefrontend/root_node.go - q |= QuirkBtrfsBrokenFalloc + if dirHasNoCow(cipherdir) { + tlog.Debug.Printf("DetectQuirks: Btrfs detected but cipherdir has NOCOW attribute (chattr +C), fallocate should work correctly") + } else { + // LogQuirk is called in fusefrontend/root_node.go + q |= QuirkBtrfsBrokenFalloc + } } return q diff --git a/internal/syscallcompat/sys_common.go b/internal/syscallcompat/sys_common.go index e95373a..4f84d98 100644 --- a/internal/syscallcompat/sys_common.go +++ b/internal/syscallcompat/sys_common.go @@ -54,10 +54,10 @@ func Openat(dirfd int, path string, flags int, mode uint32) (fd int, err error) flags |= syscall.O_EXCL } } else { - // If O_CREAT is not used, we should use O_NOFOLLOW - if flags&syscall.O_NOFOLLOW == 0 { - tlog.Warn.Printf("Openat: O_NOFOLLOW missing: flags = %#x", flags) - flags |= syscall.O_NOFOLLOW + // If O_CREAT is not used, we should use O_NOFOLLOW or O_SYMLINK + if flags&(unix.O_NOFOLLOW|OpenatFlagNofollowSymlink) == 0 { + tlog.Warn.Printf("Openat: O_NOFOLLOW/O_SYMLINK missing: flags = %#x", flags) + flags |= unix.O_NOFOLLOW } } diff --git a/internal/syscallcompat/sys_darwin.go b/internal/syscallcompat/sys_darwin.go index 248b5a2..52d4e0f 100644 --- a/internal/syscallcompat/sys_darwin.go +++ b/internal/syscallcompat/sys_darwin.go @@ -29,6 +29,10 @@ const ( // Only exists on Linux. Define here to fix build failure, even though // we will never see this flag. RENAME_WHITEOUT = 1 << 30 + + // On Darwin we use O_SYMLINK which allows opening a symlink itself. + // On Linux, we only have O_NOFOLLOW. + OpenatFlagNofollowSymlink = unix.O_SYMLINK ) // Unfortunately fsetattrlist does not have a syscall wrapper yet. diff --git a/internal/syscallcompat/sys_freebsd.go b/internal/syscallcompat/sys_freebsd.go index ec2c402..874e920 100644 --- a/internal/syscallcompat/sys_freebsd.go +++ b/internal/syscallcompat/sys_freebsd.go @@ -26,6 +26,9 @@ const ( // ENODATA is only defined on Linux, but FreeBSD provides ENOATTR ENODATA = unix.ENOATTR + + // On FreeBSD, we only have O_NOFOLLOW. + OpenatFlagNofollowSymlink = unix.O_NOFOLLOW ) // EnospcPrealloc is supposed to preallocate ciphertext space without diff --git a/internal/syscallcompat/sys_linux.go b/internal/syscallcompat/sys_linux.go index faa7ffc..4669d8a 100644 --- a/internal/syscallcompat/sys_linux.go +++ b/internal/syscallcompat/sys_linux.go @@ -31,6 +31,10 @@ const ( // Only defined on Linux ENODATA = unix.ENODATA + + // On Darwin we use O_SYMLINK which allows opening a symlink itself. + // On Linux, we only have O_NOFOLLOW. + OpenatFlagNofollowSymlink = unix.O_NOFOLLOW ) var preallocWarn sync.Once diff --git a/profiling/tinyfiles.bash b/profiling/tinyfiles.bash new file mode 100755 index 0000000..90820de --- /dev/null +++ b/profiling/tinyfiles.bash @@ -0,0 +1,33 @@ +#!/bin/bash -eu +# +# Create a tarball of 100k 1-byte files using reverse mode +# https://github.com/rfjakob/gocryptfs/issues/965 + +cd "$(dirname "$0")" + +T=$(mktemp -d) +mkdir "$T/a" "$T/b" + +../gocryptfs -init -reverse -quiet -scryptn 10 -extpass "echo test" "$@" "$T/a" + +# Cleanup trap +# shellcheck disable=SC2064 +trap "cd /; fusermount -u -z '$T/b'; rm -Rf '$T/a'" EXIT + +echo "Creating 100k 1-byte files" +SECONDS=0 +dd if=/dev/urandom bs=100k count=1 status=none | split --suffix-length=10 -b 1 - "$T/a/tinyfile." +echo "done, $SECONDS seconds" + +../gocryptfs -reverse -quiet -nosyslog -extpass "echo test" \ + -cpuprofile "$T/cprof" -memprofile "$T/mprof" \ + "$@" "$T/a" "$T/b" + +echo "Running tar under profiler..." +SECONDS=0 +tar -cf /dev/null "$T/b" +echo "done, $SECONDS seconds" + +echo +echo "Hint: go tool pprof ../gocryptfs $T/cprof" +echo " go tool pprof -alloc_space ../gocryptfs $T/mprof" diff --git a/tests/matrix/symlink_darwin_test.go b/tests/matrix/symlink_darwin_test.go new file mode 100644 index 0000000..be28d9d --- /dev/null +++ b/tests/matrix/symlink_darwin_test.go @@ -0,0 +1,39 @@ +package matrix + +import ( + "os" + "testing" + + "golang.org/x/sys/unix" + + "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" +) + +// TestOpenSymlinkDarwin checks that a symlink can be opened +// using O_SYMLINK. +func TestOpenSymlinkDarwin(t *testing.T) { + path := test_helpers.DefaultPlainDir + "/TestOpenSymlink" + target := "/target/does/not/exist" + err := os.Symlink(target, path) + if err != nil { + t.Fatal(err) + } + fd, err := unix.Open(path, unix.O_RDONLY|unix.O_SYMLINK, 0) + if err != nil { + t.Fatal(err) + } + defer unix.Close(fd) + var st unix.Stat_t + if err := unix.Fstat(fd, &st); err != nil { + t.Fatal(err) + } + if st.Size != int64(len(target)) { + t.Errorf("wrong size: have=%d want=%d", st.Size, len(target)) + } + if err := unix.Unlink(path); err != nil { + t.Fatal(err) + } + if err := unix.Fstat(fd, &st); err != nil { + t.Error(err) + } +} diff --git a/tests/matrix/symlink_linux_test.go b/tests/matrix/symlink_linux_test.go new file mode 100644 index 0000000..fdb7051 --- /dev/null +++ b/tests/matrix/symlink_linux_test.go @@ -0,0 +1,47 @@ +package matrix + +import ( + "os" + "testing" + + "golang.org/x/sys/unix" + + "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" +) + +// TestOpenSymlinkLinux checks that a symlink can be opened +// using O_PATH. +// Only works on Linux because is uses O_PATH and AT_EMPTY_PATH. +// MacOS has O_SYMLINK instead (see TestOpenSymlinkDarwin). +func TestOpenSymlinkLinux(t *testing.T) { + path := test_helpers.DefaultPlainDir + "/TestOpenSymlink" + target := "/target/does/not/exist" + err := os.Symlink(target, path) + if err != nil { + t.Fatal(err) + } + how := unix.OpenHow{ + Flags: unix.O_PATH | unix.O_NOFOLLOW, + } + fd, err := unix.Openat2(unix.AT_FDCWD, path, &how) + if err != nil { + t.Fatal(err) + } + defer unix.Close(fd) + var st unix.Stat_t + if err := unix.Fstatat(fd, "", &st, unix.AT_EMPTY_PATH); err != nil { + t.Fatal(err) + } + if st.Size != int64(len(target)) { + t.Errorf("wrong size: have=%d want=%d", st.Size, len(target)) + } + if err := unix.Unlink(path); err != nil { + t.Fatal(err) + } + if err = unix.Fstatat(fd, "", &st, unix.AT_EMPTY_PATH); err != nil { + // That's a bug, but I have never heard of a use case that would break because of this. + // Also I don't see how to fix it, as gocryptfs does not get informed about the earlier + // Openat2(). + t.Logf("posix compliance issue: deleted symlink cannot be accessed: Fstatat: %v", err) + } +} diff --git a/tests/reverse/xattr_test.go b/tests/reverse/xattr_test.go index 406d055..b6a7b34 100644 --- a/tests/reverse/xattr_test.go +++ b/tests/reverse/xattr_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/pkg/xattr" + "golang.org/x/sys/unix" ) func xattrSupported(path string) bool { @@ -65,3 +66,18 @@ func TestXattrList(t *testing.T) { } } } + +// Shouldn't get EINVAL when querying the mountpoint. +func TestXattrGetMountpoint(t *testing.T) { + _, err := xattr.LGet(dirB, "user.foo453465324") + if err == nil { + return + } + e2 := err.(*xattr.Error) + if e2.Unwrap() == unix.EINVAL { + t.Errorf("LGet: %v", err) + } + // Let's see what LList says + _, err = xattr.LList(dirB) + t.Logf("LList: err=%v", err) +} diff --git a/tests/root_test/btrfs_test.go b/tests/root_test/btrfs_test.go index 72b7751..898cd39 100644 --- a/tests/root_test/btrfs_test.go +++ b/tests/root_test/btrfs_test.go @@ -12,12 +12,20 @@ import ( "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" ) -// TestBtrfsQuirks needs root permissions because it creates a loop disk -func TestBtrfsQuirks(t *testing.T) { +// createBtrfsImage creates a btrfs image file, formats it, and mounts it. +// Returns the mount path and a cleanup function. +func createBtrfsImage(t *testing.T) (mnt string, cleanup func()) { + t.Helper() + if os.Getuid() != 0 { t.Skip("must run as root") } + _, err := exec.LookPath("mkfs.btrfs") + if err != nil { + t.Skip("mkfs.btrfs not found, skipping test") + } + img := filepath.Join(test_helpers.TmpDir, t.Name()+".img") f, err := os.Create(img) if err != nil { @@ -31,10 +39,6 @@ func TestBtrfsQuirks(t *testing.T) { } // Format as Btrfs - _, err = exec.LookPath("mkfs.btrfs") - if err != nil { - t.Skip("mkfs.btrfs not found, skipping test") - } cmd := exec.Command("mkfs.btrfs", img) out, err := cmd.CombinedOutput() if err != nil { @@ -44,7 +48,7 @@ func TestBtrfsQuirks(t *testing.T) { } // Mount - mnt := img + ".mnt" + mnt = img + ".mnt" err = os.Mkdir(mnt, 0600) if err != nil { t.Fatal(err) @@ -55,11 +59,52 @@ func TestBtrfsQuirks(t *testing.T) { t.Log(string(out)) t.Fatal(err) } - defer syscall.Unlink(img) - defer syscall.Unmount(mnt, 0) + + cleanup = func() { + syscall.Unmount(mnt, 0) + syscall.Unlink(img) + } + return mnt, cleanup +} + +// TestBtrfsQuirks needs root permissions because it creates a loop disk +func TestBtrfsQuirks(t *testing.T) { + mnt, cleanup := createBtrfsImage(t) + defer cleanup() quirk := syscallcompat.DetectQuirks(mnt) if quirk != syscallcompat.QuirkBtrfsBrokenFalloc { t.Errorf("wrong quirk: %v", quirk) } } + +// TestBtrfsQuirksNoCow verifies that when the backing directory has +// the NOCOW attribute (chattr +C), the QuirkBtrfsBrokenFalloc quirk +// is NOT set, because fallocate works correctly with NOCOW. +func TestBtrfsQuirksNoCow(t *testing.T) { + mnt, cleanup := createBtrfsImage(t) + defer cleanup() + + _, err := exec.LookPath("chattr") + if err != nil { + t.Skip("chattr not found, skipping test") + } + + // Create a subdirectory with NOCOW attribute + nocowDir := filepath.Join(mnt, "nocow") + err = os.Mkdir(nocowDir, 0700) + if err != nil { + t.Fatal(err) + } + cmd := exec.Command("chattr", "+C", nocowDir) + out, err := cmd.CombinedOutput() + if err != nil { + t.Log(string(out)) + t.Fatal(err) + } + + quirk := syscallcompat.DetectQuirks(nocowDir) + if quirk&syscallcompat.QuirkBtrfsBrokenFalloc != 0 { + t.Errorf("QuirkBtrfsBrokenFalloc should not be set on NOCOW directory, got quirks: %v", quirk) + } +} |
