aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/fusefrontend/root_node.go4
-rw-r--r--internal/syscallcompat/quirks_linux.go40
-rw-r--r--tests/root_test/btrfs_test.go63
3 files changed, 95 insertions, 12 deletions
diff --git a/internal/fusefrontend/root_node.go b/internal/fusefrontend/root_node.go
index aa26b9c..38d070d 100644
--- a/internal/fusefrontend/root_node.go
+++ b/internal/fusefrontend/root_node.go
@@ -92,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)
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/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)
+ }
+}