diff options
44 files changed, 712 insertions, 189 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5dbf76..792b879 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,8 +14,6 @@ jobs: # Each major Go release is supported until there are two newer major releases. # https://go.dev/doc/devel/release#policy go: - - "1.19.x" # Debian bookworm, bullseye-backports - - "1.22.x" # Ubuntu 24.04 LTS "noble" - "oldstable" # 2nd-latest Golang upstream stable - "stable" # Latest Go upstream stable os: @@ -26,7 +24,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 # Make "git describe" work @@ -44,11 +42,13 @@ jobs: # Fix "/usr/bin/fusermount: option allow_other only allowed if 'user_allow_other' is set in /etc/fuse.conf" # and "/usr/bin/fusermount3: too many FUSE filesystems mounted" + # The latter is likely a bug in fusermount, fixed in Oct 2025 by + # https://github.com/libfuse/libfuse/commit/bf3dd153fbfcd610d799562490f6555b9d708905 - run: echo -e 'user_allow_other\nmount_max = 10000' | sudo tee -a /etc/fuse.conf && cat /etc/fuse.conf # Build & upload static binary - run: ./build-without-openssl.bash - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v6 with: name: gocryptfs ${{ github.sha }} static ${{ runner.arch }} binary, Go ${{ matrix.go }} path: gocryptfs diff --git a/Documentation/performance.txt b/Documentation/performance.txt index 24265f5..7e964e0 100644 --- a/Documentation/performance.txt +++ b/Documentation/performance.txt @@ -73,6 +73,10 @@ v2.0-beta2-37-g24d5d39 558 1000 12.3 6.4 4.4 2.8 v2.0-beta2-42-g4a07d65 549 1000 8.2 4.7 1.8 2.4 fusefrontend: make dirCache work for "node itself" v2.0 420 1000 8.5 4.5 1.8 2.3 go1.16.5, Linux 5.11.21 v2.0.1-28-g49507ea 471 991 8.6 4.5 1.7 2.2 +v2.0.1-28-g49507ea 335 951 10.2 5.4 4.1 2.0 go1.25.4, Linux 6.18.6 +v2.6.1-22-gbc94538 432 950 10.0 5.4 3.8 2.0 +v2.6.1-24-gb239d51 426 941 9.9 5.5 3.7 2.0 go-fuse v2.9.0 +v2.6.1-26-g700432e 461 962 9.8 5.4 2.0 2.0 Results for EncFS for comparison (benchmark.bash -encfs): @@ -56,7 +56,7 @@ Standalone tools: is Python tool that can decrypt files & file names without using FUSE. -[gocryptfs-create-folder](https://codeberg.org/LGLQ/gocryptfs-create-folder) +[gocryptfs-create-folder](https://github.com/lquenti/gocryptfs-create-folder) is a Python tool can encrypt a directory without using FUSE. Installation diff --git a/cli_args.go b/cli_args.go index e690e15..707b453 100644 --- a/cli_args.go +++ b/cli_args.go @@ -31,7 +31,7 @@ type argContainer struct { longnames, allow_other, reverse, aessiv, nonempty, raw64, noprealloc, speed, hkdf, serialize_reads, hh, info, sharedstorage, fsck, one_file_system, deterministic_names, - xchacha bool + xchacha, noxattr bool // Mount options with opposites dev, nodev, suid, nosuid, exec, noexec, rw, ro, kernel_cache, acl bool masterkey, mountpoint, cipherdir, cpuprofile, @@ -188,6 +188,7 @@ func parseCliOpts(osArgs []string) (args argContainer) { flagSet.BoolVar(&args.one_file_system, "one-file-system", false, "Don't cross filesystem boundaries") flagSet.BoolVar(&args.deterministic_names, "deterministic-names", false, "Disable diriv file name randomisation") flagSet.BoolVar(&args.xchacha, "xchacha", false, "Use XChaCha20-Poly1305 file content encryption") + flagSet.BoolVar(&args.noxattr, "noxattr", false, "Disable extended attribute operations") // Mount options with opposites flagSet.BoolVar(&args.dev, "dev", false, "Allow device files") diff --git a/contrib/findholes/holes/holes.go b/contrib/findholes/holes/holes.go index d298efb..72f5114 100644 --- a/contrib/findholes/holes/holes.go +++ b/contrib/findholes/holes/holes.go @@ -8,7 +8,6 @@ import ( "math/rand" "os" "syscall" - "time" ) const ( @@ -176,7 +175,6 @@ func Create(path string) { } defer f.Close() - rand.Seed(time.Now().UnixNano()) offsets := make([]int64, 10) for i := range offsets { offsets[i] = int64(rand.Int31n(60000)) @@ -1,16 +1,17 @@ module github.com/rfjakob/gocryptfs/v2 -go 1.19 +go 1.24.0 require ( github.com/aperturerobotics/jacobsa-crypto v1.1.0 - github.com/hanwen/go-fuse/v2 v2.8.0 + github.com/hanwen/go-fuse/v2 v2.9.0 github.com/moby/sys/mountinfo v0.7.2 github.com/pkg/xattr v0.4.9 - github.com/rfjakob/eme v1.1.2 + github.com/rfjakob/eme v1.2.0 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 github.com/spf13/pflag v1.0.5 - golang.org/x/crypto v0.33.0 - golang.org/x/sys v0.30.0 - golang.org/x/term v0.29.0 + golang.org/x/crypto v0.45.0 + golang.org/x/sys v0.38.0 + golang.org/x/term v0.37.0 + golang.org/x/text v0.31.0 ) @@ -2,21 +2,26 @@ github.com/aperturerobotics/jacobsa-crypto v1.1.0 h1:0hig54FMzU80OHrqSfqmj/W8Hyd github.com/aperturerobotics/jacobsa-crypto v1.1.0/go.mod h1:buWU1iY+FjIcfpb1aYfFJZfl07WlS7O30lTyC2iwjv8= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/hanwen/go-fuse/v2 v2.8.0 h1:wV8rG7rmCz8XHSOwBZhG5YcVqcYjkzivjmbaMafPlAs= -github.com/hanwen/go-fuse/v2 v2.8.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI= +github.com/hanwen/go-fuse/v2 v2.9.0 h1:0AOGUkHtbOVeyGLr0tXupiid1Vg7QB7M6YUcdmVdC58= +github.com/hanwen/go-fuse/v2 v2.9.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI= github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd h1:9GCSedGjMcLZCrusBZuo4tyKLpKUPenUUqi34AkuFmA= +github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd/go.mod h1:TlmyIZDpGmwRoTWiakdr+HA1Tukze6C6XbRVidYq02M= github.com/jacobsa/oglemock v0.0.0-20150831005832-e94d794d06ff h1:2xRHTvkpJ5zJmglXLRqHiZQNjUoOkhUyhTAhEQvPAWw= +github.com/jacobsa/oglemock v0.0.0-20150831005832-e94d794d06ff/go.mod h1:gJWba/XXGl0UoOmBQKRWCJdHrr3nE0T65t6ioaj3mLI= github.com/jacobsa/ogletest v0.0.0-20170503003838-80d50a735a11 h1:BMb8s3ENQLt5ulwVIHVDWFHp8eIXmbfSExkvdn9qMXI= +github.com/jacobsa/ogletest v0.0.0-20170503003838-80d50a735a11/go.mod h1:+DBdDyfoO2McrOyDemRBq0q9CMEByef7sYl7JH5Q3BI= github.com/jacobsa/reqtrace v0.0.0-20150505043853-245c9e0234cb h1:uSWBjJdMf47kQlXMwWEfmc864bA1wAC+Kl3ApryuG9Y= +github.com/jacobsa/reqtrace v0.0.0-20150505043853-245c9e0234cb/go.mod h1:ivcmUvxXWjb27NsPEaiYK7AidlZXS7oQ5PowUS9z3I4= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE= github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rfjakob/eme v1.1.2 h1:SxziR8msSOElPayZNFfQw4Tjx/Sbaeeh3eRvrHVMUs4= -github.com/rfjakob/eme v1.1.2/go.mod h1:cVvpasglm/G3ngEfcfT/Wt0GwhkuO32pf/poW6Nyk1k= +github.com/rfjakob/eme v1.2.0 h1:8dAHL+WVAw06+7DkRKnRiFp1JL3QjcJEZFqDnndUaSI= +github.com/rfjakob/eme v1.2.0/go.mod h1:cVvpasglm/G3ngEfcfT/Wt0GwhkuO32pf/poW6Nyk1k= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -24,14 +29,17 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= -golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/init_dir.go b/init_dir.go index 3546084..870604a 100644 --- a/init_dir.go +++ b/init_dir.go @@ -68,14 +68,14 @@ func initDir(args *argContainer) { os.Exit(exitcodes.CipherDir) } if !args.xchacha && !stupidgcm.HasAESGCMHardwareSupport() { - tlog.Info.Printf(tlog.ColorYellow + + tlog.Info.Println(tlog.ColorYellow + "Notice: Your CPU does not have AES-GCM acceleration. Consider using -xchacha for better performance." + tlog.ColorReset) } } // Choose password for config file if len(args.extpass) == 0 && args.fido2 == "" { - tlog.Info.Printf("Choose a password for protecting your files.") + tlog.Info.Println("Choose a password for protecting your files.") } { var password []byte diff --git a/internal/cryptocore/hkdf.go b/internal/cryptocore/hkdf.go index b56f507..369616a 100644 --- a/internal/cryptocore/hkdf.go +++ b/internal/cryptocore/hkdf.go @@ -1,10 +1,9 @@ package cryptocore import ( + "crypto/hkdf" "crypto/sha256" "log" - - "golang.org/x/crypto/hkdf" ) const ( @@ -19,12 +18,10 @@ const ( // hkdfDerive derives "outLen" bytes from "masterkey" and "info" using // HKDF-SHA256 (RFC 5869). // It returns the derived bytes or panics. -func hkdfDerive(masterkey []byte, info string, outLen int) (out []byte) { - h := hkdf.New(sha256.New, masterkey, nil, []byte(info)) - out = make([]byte, outLen) - n, err := h.Read(out) - if n != outLen || err != nil { - log.Panicf("hkdfDerive: hkdf read failed, got %d bytes, error: %v", n, err) +func hkdfDerive(masterkey []byte, info string, outLen int) []byte { + key, err := hkdf.Key(sha256.New, masterkey, nil, info, outLen) + if err != nil { + log.Panicf("hkdfDerive: hkdf failed with error: %v", err) } - return out + return key } diff --git a/internal/fusefrontend/args.go b/internal/fusefrontend/args.go index 64a5923..ec3d1c2 100644 --- a/internal/fusefrontend/args.go +++ b/internal/fusefrontend/args.go @@ -51,4 +51,6 @@ type Args struct { OneFileSystem bool // DeterministicNames disables gocryptfs.diriv files DeterministicNames bool + // NoXattr disables extended attribute operations + NoXattr bool } diff --git a/internal/fusefrontend/file.go b/internal/fusefrontend/file.go index afee158..64c6ca0 100644 --- a/internal/fusefrontend/file.go +++ b/internal/fusefrontend/file.go @@ -115,7 +115,7 @@ func (f *File) createHeader() (fileID []byte, err error) { h := contentenc.RandomHeader() buf := h.Pack() // Prevent partially written (=corrupt) header by preallocating the space beforehand - if !f.rootNode.args.NoPrealloc && f.rootNode.quirks&syscallcompat.QuirkBrokenFalloc == 0 { + if !f.rootNode.args.NoPrealloc && f.rootNode.quirks&syscallcompat.QuirkBtrfsBrokenFalloc == 0 { err = syscallcompat.EnospcPrealloc(f.intFd(), 0, contentenc.HeaderLen) if err != nil { if !syscallcompat.IsENOSPC(err) { @@ -315,7 +315,7 @@ func (f *File) doWrite(data []byte, off int64) (uint32, syscall.Errno) { if cOff > math.MaxInt64 { return 0, syscall.EFBIG } - if !f.rootNode.args.NoPrealloc && f.rootNode.quirks&syscallcompat.QuirkBrokenFalloc == 0 { + if !f.rootNode.args.NoPrealloc && f.rootNode.quirks&syscallcompat.QuirkBtrfsBrokenFalloc == 0 { err = syscallcompat.EnospcPrealloc(f.intFd(), int64(cOff), int64(len(ciphertext))) if err != nil { if !syscallcompat.IsENOSPC(err) { diff --git a/internal/fusefrontend/node_dir_ops.go b/internal/fusefrontend/node_dir_ops.go index 11ff83d..97327ce 100644 --- a/internal/fusefrontend/node_dir_ops.go +++ b/internal/fusefrontend/node_dir_ops.go @@ -136,12 +136,6 @@ func (n *Node) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.En } defer syscall.Close(fd) - err = syscall.Fstat(fd, &st) - if err != nil { - tlog.Warn.Printf("Mkdir %q: Fstat failed: %v", cName, err) - return nil, fs.ToErrno(err) - } - // Fix permissions if origMode != mode { // Preserve SGID bit if it was set due to inheritance. @@ -150,7 +144,12 @@ func (n *Node) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.En if err != nil { tlog.Warn.Printf("Mkdir %q: Fchmod %#o -> %#o failed: %v", cName, mode, origMode, err) } + } + err = syscall.Fstat(fd, &st) + if err != nil { + tlog.Warn.Printf("Mkdir %q: Fstat failed: %v", cName, err) + return nil, fs.ToErrno(err) } // Create child node & return 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.go b/internal/fusefrontend/node_xattr.go index 44bc502..1470a2a 100644 --- a/internal/fusefrontend/node_xattr.go +++ b/internal/fusefrontend/node_xattr.go @@ -12,9 +12,6 @@ import ( "github.com/rfjakob/gocryptfs/v2/internal/tlog" ) -// -1 as uint32 -const minus1 = ^uint32(0) - // We store encrypted xattrs under this prefix plus the base64-encoded // encrypted original name. var xattrStorePrefix = "user.gocryptfs." @@ -35,6 +32,10 @@ func isAcl(attr string) bool { // This function is symlink-safe through Fgetxattr. func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, syscall.Errno) { rn := n.rootNode() + // If -noxattr is enabled, return ENOATTR for all getxattr calls + if rn.args.NoXattr { + return 0, noSuchAttributeError + } // If we are not mounted with -suid, reading the capability xattr does not // make a lot of sense, so reject the request and gain a massive speedup. // See https://github.com/rfjakob/gocryptfs/issues/515 . @@ -50,13 +51,13 @@ func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, var errno syscall.Errno data, errno = n.getXAttr(attr) if errno != 0 { - return minus1, errno + return 0, errno } } else { // encrypted user xattr cAttr, err := rn.encryptXattrName(attr) if err != nil { - return minus1, syscall.EIO + return 0, syscall.EIO } cData, errno := n.getXAttr(cAttr) if errno != 0 { @@ -65,15 +66,11 @@ func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, data, err = rn.decryptXattrValue(cData) if err != nil { tlog.Warn.Printf("GetXAttr: %v", err) - return minus1, syscall.EIO + return 0, syscall.EIO } } - // Caller passes size zero to find out how large their buffer should be - if len(dest) == 0 { - return uint32(len(data)), 0 - } if len(dest) < len(data) { - return minus1, syscall.ERANGE + return uint32(len(data)), syscall.ERANGE } l := copy(dest, data) return uint32(l), 0 @@ -84,6 +81,10 @@ func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, // This function is symlink-safe through Fsetxattr. func (n *Node) Setxattr(ctx context.Context, attr string, data []byte, flags uint32) syscall.Errno { rn := n.rootNode() + // If -noxattr is enabled, fail all setxattr calls + if rn.args.NoXattr { + return syscall.EOPNOTSUPP + } flags = uint32(filterXattrSetFlags(int(flags))) // ACLs are passed through without encryption @@ -109,6 +110,10 @@ func (n *Node) Setxattr(ctx context.Context, attr string, data []byte, flags uin // This function is symlink-safe through Fremovexattr. func (n *Node) Removexattr(ctx context.Context, attr string) syscall.Errno { rn := n.rootNode() + // If -noxattr is enabled, fail all removexattr calls + if rn.args.NoXattr { + return syscall.EOPNOTSUPP + } // ACLs are passed through without encryption if isAcl(attr) { @@ -126,11 +131,15 @@ func (n *Node) Removexattr(ctx context.Context, attr string) syscall.Errno { // // This function is symlink-safe through Flistxattr. func (n *Node) Listxattr(ctx context.Context, dest []byte) (uint32, syscall.Errno) { + rn := n.rootNode() + // If -noxattr is enabled, return zero results for listxattr + if rn.args.NoXattr { + return 0, 0 + } cNames, errno := n.listXAttr() if errno != 0 { return 0, errno } - rn := n.rootNode() var buf bytes.Buffer for _, curName := range cNames { // ACLs are passed through without encryption @@ -155,12 +164,8 @@ func (n *Node) Listxattr(ctx context.Context, dest []byte) (uint32, syscall.Errn } buf.WriteString(name + "\000") } - // Caller passes size zero to find out how large their buffer should be - if len(dest) == 0 { - return uint32(buf.Len()), 0 - } if buf.Len() > len(dest) { - return minus1, syscall.ERANGE + return uint32(buf.Len()), syscall.ERANGE } return uint32(copy(dest, buf.Bytes())), 0 } diff --git a/internal/fusefrontend/node_xattr_darwin.go b/internal/fusefrontend/node_xattr_darwin.go index a539847..1d25f3d 100644 --- a/internal/fusefrontend/node_xattr_darwin.go +++ b/internal/fusefrontend/node_xattr_darwin.go @@ -11,6 +11,9 @@ import ( "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" ) +// On Darwin, ENOATTR is returned when an attribute is not found. +const noSuchAttributeError = syscall.ENOATTR + // On Darwin we have to unset XATTR_NOSECURITY 0x0008 func filterXattrSetFlags(flags int) int { // See https://opensource.apple.com/source/xnu/xnu-1504.15.3/bsd/sys/xattr.h.auto.html @@ -26,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) } @@ -49,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) @@ -71,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) @@ -93,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/node_xattr_linux.go b/internal/fusefrontend/node_xattr_linux.go index 4a356a5..9964212 100644 --- a/internal/fusefrontend/node_xattr_linux.go +++ b/internal/fusefrontend/node_xattr_linux.go @@ -12,6 +12,9 @@ import ( "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" ) +// On Linux, ENODATA is returned when an attribute is not found. +const noSuchAttributeError = syscall.ENODATA + func filterXattrSetFlags(flags int) int { return flags } diff --git a/internal/fusefrontend/root_node.go b/internal/fusefrontend/root_node.go index 8464c5f..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" @@ -91,6 +90,12 @@ func NewRootNode(args Args, c *contentenc.ContentEnc, n *nametransform.NameTrans dirCache: dirCache{ivLen: ivLen}, quirks: syscallcompat.DetectQuirks(args.Cipherdir), } + // Suppress the message if the user has already specified -noprealloc + if rn.quirks&syscallcompat.QuirkBtrfsBrokenFalloc != 0 && !args.NoPrealloc { + 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) rn.rootIno = st.Ino @@ -104,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_api_check.go b/internal/fusefrontend_reverse/node_api_check.go index f8ec9ce..eb608f9 100644 --- a/internal/fusefrontend_reverse/node_api_check.go +++ b/internal/fusefrontend_reverse/node_api_check.go @@ -11,14 +11,8 @@ var _ = (fs.NodeReaddirer)((*Node)(nil)) var _ = (fs.NodeReadlinker)((*Node)(nil)) var _ = (fs.NodeOpener)((*Node)(nil)) var _ = (fs.NodeStatfser)((*Node)(nil)) - -/* -TODO but low prio. reverse mode in gocryptfs v1 did not have xattr support -either. - var _ = (fs.NodeGetxattrer)((*Node)(nil)) var _ = (fs.NodeListxattrer)((*Node)(nil)) -*/ /* Not needed var _ = (fs.NodeOpendirer)((*Node)(nil)) 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 new file mode 100644 index 0000000..b940339 --- /dev/null +++ b/internal/fusefrontend_reverse/node_xattr.go @@ -0,0 +1,89 @@ +// Package fusefrontend_reverse interfaces directly with the go-fuse library. +package fusefrontend_reverse + +import ( + "bytes" + "context" + "syscall" + + "github.com/rfjakob/gocryptfs/v2/internal/pathiv" +) + +// We store encrypted xattrs under this prefix plus the base64-encoded +// encrypted original name. +var xattrStorePrefix = "user.gocryptfs." + +// isAcl returns true if the attribute name is for storing ACLs +// +// ACLs are passed through without encryption +func isAcl(attr string) bool { + return attr == "system.posix_acl_access" || attr == "system.posix_acl_default" +} + +// GetXAttr - FUSE call. Reads the value of extended attribute "attr". +// +// This function is symlink-safe through Fgetxattr. +func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, syscall.Errno) { + rn := n.rootNode() + // If -noxattr is enabled, return ENOATTR for all getxattr calls + if rn.args.NoXattr { + return 0, noSuchAttributeError + } + var data []byte + // ACLs are passed through without encryption + if isAcl(attr) { + var errno syscall.Errno + data, errno = n.getXAttr(attr) + if errno != 0 { + return 0, errno + } + } else { + pAttr, err := rn.decryptXattrName(attr) + if err != nil { + return 0, noSuchAttributeError + } + pData, errno := n.getXAttr(pAttr) + if errno != 0 { + return 0, errno + } + nonce := pathiv.Derive(n.Path()+"\000"+attr, pathiv.PurposeXattrIV) + data = rn.encryptXattrValue(pData, nonce) + } + if len(dest) < len(data) { + return uint32(len(data)), syscall.ERANGE + } + l := copy(dest, data) + return uint32(l), 0 +} + +// ListXAttr - FUSE call. Lists extended attributes on the file at "relPath". +// +// This function is symlink-safe through Flistxattr. +func (n *Node) Listxattr(ctx context.Context, dest []byte) (uint32, syscall.Errno) { + rn := n.rootNode() + // If -noxattr is enabled, return zero results for listxattr + if rn.args.NoXattr { + return 0, 0 + } + pNames, errno := n.listXAttr() + if errno != 0 { + return 0, errno + } + var buf bytes.Buffer + for _, pName := range pNames { + // ACLs are passed through without encryption + if isAcl(pName) { + buf.WriteString(pName + "\000") + continue + } + cName, err := rn.encryptXattrName(pName) + if err != nil { + continue + } + buf.WriteString(cName + "\000") + } + if buf.Len() > len(dest) { + return uint32(buf.Len()), syscall.ERANGE + } + return uint32(copy(dest, buf.Bytes())), 0 +} diff --git a/internal/fusefrontend_reverse/node_xattr_darwin.go b/internal/fusefrontend_reverse/node_xattr_darwin.go new file mode 100644 index 0000000..6816a18 --- /dev/null +++ b/internal/fusefrontend_reverse/node_xattr_darwin.go @@ -0,0 +1,55 @@ +package fusefrontend_reverse + +import ( + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + + "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" +) + +// On Darwin, ENOATTR is returned when an attribute is not found. +const noSuchAttributeError = syscall.ENOATTR + +func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) { + d, errno := n.prepareAtSyscall("") + if errno != 0 { + return + } + defer syscall.Close(d.dirfd) + + // O_NONBLOCK to not block on FIFOs. + fd, err := syscallcompat.Openat(d.dirfd, d.pName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + if err != nil { + return nil, fs.ToErrno(err) + } + defer syscall.Close(fd) + + cData, err := syscallcompat.Fgetxattr(fd, cAttr) + if err != nil { + return nil, fs.ToErrno(err) + } + + return cData, 0 +} + +func (n *Node) listXAttr() (out []string, errno syscall.Errno) { + d, errno := n.prepareAtSyscall("") + if errno != 0 { + return + } + defer syscall.Close(d.dirfd) + + // O_NONBLOCK to not block on FIFOs. + fd, err := syscallcompat.Openat(d.dirfd, d.pName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + if err != nil { + return nil, fs.ToErrno(err) + } + defer syscall.Close(fd) + + pNames, err := syscallcompat.Flistxattr(fd) + if err != nil { + return nil, fs.ToErrno(err) + } + return pNames, 0 +} diff --git a/internal/fusefrontend_reverse/node_xattr_linux.go b/internal/fusefrontend_reverse/node_xattr_linux.go new file mode 100644 index 0000000..3c574f5 --- /dev/null +++ b/internal/fusefrontend_reverse/node_xattr_linux.go @@ -0,0 +1,43 @@ +package fusefrontend_reverse + +import ( + "fmt" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + + "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" +) + +// On Linux, ENODATA is returned when an attribute is not found. +const noSuchAttributeError = syscall.ENODATA + +func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) { + d, errno := n.prepareAtSyscall("") + if errno != 0 { + return + } + defer syscall.Close(d.dirfd) + + procPath := fmt.Sprintf("/proc/self/fd/%d/%s", d.dirfd, d.pName) + pData, err := syscallcompat.Lgetxattr(procPath, cAttr) + if err != nil { + return nil, fs.ToErrno(err) + } + return pData, 0 +} + +func (n *Node) listXAttr() (out []string, errno syscall.Errno) { + d, errno := n.prepareAtSyscall("") + if errno != 0 { + return + } + defer syscall.Close(d.dirfd) + + procPath := fmt.Sprintf("/proc/self/fd/%d/%s", d.dirfd, d.pName) + pNames, err := syscallcompat.Llistxattr(procPath) + if err != nil { + return nil, fs.ToErrno(err) + } + return pNames, 0 +} diff --git a/internal/fusefrontend_reverse/root_node.go b/internal/fusefrontend_reverse/root_node.go index 9c2de28..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) @@ -182,3 +184,38 @@ func (rn *RootNode) uniqueStableAttr(mode uint32, ino uint64) fs.StableAttr { func (rn *RootNode) RootIno() uint64 { return rn.rootIno } + +// encryptXattrValue encrypts the xattr value "data". +// The data is encrypted like a file content block, but without binding it to +// a file location (block number and file id are set to zero). +// Special case: an empty value is encrypted to an empty value. +func (rn *RootNode) encryptXattrValue(data []byte, nonce []byte) (cData []byte) { + if len(data) == 0 { + return []byte{} + } + return rn.contentEnc.EncryptBlockNonce(data, 0, nil, nonce) +} + +// encryptXattrName transforms "user.foo" to "user.gocryptfs.a5sAd4XAa47f5as6dAf" +func (rn *RootNode) encryptXattrName(attr string) (string, error) { + // xattr names are encrypted like file names, but with a fixed IV. + cAttr, err := rn.nameTransform.EncryptXattrName(attr) + if err != nil { + return "", err + } + return xattrStorePrefix + cAttr, nil +} + +func (rn *RootNode) decryptXattrName(cAttr string) (attr string, err error) { + // Reject anything that does not start with "user.gocryptfs." + if !strings.HasPrefix(cAttr, xattrStorePrefix) { + return "", syscall.EINVAL + } + // Strip "user.gocryptfs." prefix + cAttr = cAttr[len(xattrStorePrefix):] + attr, err = rn.nameTransform.DecryptXattrName(cAttr) + if err != nil { + return "", err + } + return attr, nil +} diff --git a/internal/nametransform/names.go b/internal/nametransform/names.go index 3313a7c..0389c95 100644 --- a/internal/nametransform/names.go +++ b/internal/nametransform/names.go @@ -7,9 +7,12 @@ import ( "errors" "math" "path/filepath" + "runtime" "strings" "syscall" + "golang.org/x/text/unicode/norm" + "github.com/rfjakob/eme" "github.com/rfjakob/gocryptfs/v2/internal/tlog" @@ -32,6 +35,12 @@ type NameTransform struct { // Patterns to bypass decryption badnamePatterns []string deterministicNames bool + // Convert filenames to NFC before encrypting, + // and to NFD when decrypting. + // For MacOS compatibility. + // Automatically enabled on MacOS, off otherwise, + // except in tests (see nfc_test.go). + nfd2nfc bool } // New returns a new NameTransform instance. @@ -55,34 +64,44 @@ func New(e *eme.EMECipher, longNames bool, longNameMax uint8, raw64 bool, badnam effectiveLongNameMax = int(longNameMax) } } + nfd2nfc := runtime.GOOS == "darwin" + if nfd2nfc { + tlog.Info.Printf("Running on MacOS, enabling Unicode normalization") + } return &NameTransform{ emeCipher: e, longNameMax: effectiveLongNameMax, B64: b64, badnamePatterns: badname, deterministicNames: deterministicNames, + nfd2nfc: nfd2nfc, } } // DecryptName calls decryptName to try and decrypt a base64-encoded encrypted // filename "cipherName", and failing that checks if it can be bypassed -func (n *NameTransform) DecryptName(cipherName string, iv []byte) (string, error) { - res, err := n.decryptName(cipherName, iv) +func (n *NameTransform) DecryptName(cipherName string, iv []byte) (plainName string, err error) { + plainName, err = n.decryptName(cipherName, iv) if err != nil && n.HaveBadnamePatterns() { - res, err = n.decryptBadname(cipherName, iv) + plainName, err = n.decryptBadname(cipherName, iv) } if err != nil { return "", err } - if err := IsValidName(res); err != nil { + if err := IsValidName(plainName); err != nil { tlog.Warn.Printf("DecryptName %q: invalid name after decryption: %v", cipherName, err) return "", syscall.EBADMSG } - return res, err + if n.nfd2nfc { + // MacOS expects file names in NFD form. Present them as NFD. + // They are converted back to NFC in EncryptName. + plainName = norm.NFD.String(plainName) + } + return plainName, err } -// decryptName decrypts a base64-encoded encrypted filename "cipherName" using the -// initialization vector "iv". +// decryptName decrypts a base64-encoded encrypted file- or xattr-name "cipherName" +// using the initialization vector "iv". func (n *NameTransform) decryptName(cipherName string, iv []byte) (string, error) { // From https://pkg.go.dev/encoding/base64#Encoding.Strict : // > Note that the input is still malleable, as new line characters @@ -126,6 +145,16 @@ func (n *NameTransform) EncryptName(plainName string, iv []byte) (cipherName64 s tlog.Warn.Printf("EncryptName %q: invalid plainName: %v", plainName, err) return "", syscall.EBADMSG } + if n.nfd2nfc { + // MacOS GUI apps expect Unicode in NFD form. + // But MacOS CLI apps, Linux and Windows use NFC form. + // We normalize to NFC for two reasons: + // 1) Make sharing gocryptfs filesystems from MacOS to other systems + // less painful + // 2) Enable DecryptName to normalize to NFD, which works for both + // GUI and CLI on MacOS. + plainName = norm.NFC.String(plainName) + } return n.encryptName(plainName, iv), nil } diff --git a/internal/nametransform/nfc_test.go b/internal/nametransform/nfc_test.go new file mode 100644 index 0000000..aad1d7d --- /dev/null +++ b/internal/nametransform/nfc_test.go @@ -0,0 +1,29 @@ +package nametransform + +import ( + "strconv" + "testing" + + "golang.org/x/text/unicode/norm" +) + +func TestNFD2NFC(t *testing.T) { + n := newLognamesTestInstance(NameMax) + n.nfd2nfc = true + iv := make([]byte, DirIVLen) + srcNFC := "Österreich Café" + srcNFD := norm.NFD.String(srcNFC) + + // cipherName should get normalized to NFC + cipherName, _ := n.EncryptName(srcNFD, iv) + // Decrypt without changing normalization + decryptedRaw, _ := n.decryptName(cipherName, iv) + if srcNFC != decryptedRaw { + t.Errorf("want %s have %s", strconv.QuoteToASCII(srcNFC), strconv.QuoteToASCII(decryptedRaw)) + } + // Decrypt with normalizing to NFD + decrypted, _ := n.DecryptName(cipherName, iv) + if srcNFD != decrypted { + t.Errorf("want %s have %s", strconv.QuoteToASCII(srcNFD), strconv.QuoteToASCII(decrypted)) + } +} diff --git a/internal/pathiv/pathiv.go b/internal/pathiv/pathiv.go index 48f8426..11a7189 100644 --- a/internal/pathiv/pathiv.go +++ b/internal/pathiv/pathiv.go @@ -20,6 +20,8 @@ const ( PurposeSymlinkIV Purpose = "SYMLINKIV" // PurposeBlock0IV means the value will be used as the IV of ciphertext block #0. PurposeBlock0IV Purpose = "BLOCK0IV" + // PurposeXattrIV means the value will be used as a xattr IV + PurposeXattrIV Purpose = "XATTRIV" ) // Derive derives an IV from an encrypted path by hashing it with sha256 diff --git a/internal/syscallcompat/quirks.go b/internal/syscallcompat/quirks.go index 858f16d..36bcb9f 100644 --- a/internal/syscallcompat/quirks.go +++ b/internal/syscallcompat/quirks.go @@ -5,18 +5,17 @@ import ( ) const ( - // QuirkBrokenFalloc means the falloc is broken. + // QuirkBtrfsBrokenFalloc means the falloc is broken. // Preallocation on Btrfs is broken ( https://github.com/rfjakob/gocryptfs/issues/395 ) // and slow ( https://github.com/rfjakob/gocryptfs/issues/63 ). - QuirkBrokenFalloc = uint64(1 << iota) + QuirkBtrfsBrokenFalloc = uint64(1 << iota) // QuirkDuplicateIno1 means that we have duplicate inode numbers. // On MacOS ExFAT, all empty files share inode number 1: // https://github.com/rfjakob/gocryptfs/issues/585 QuirkDuplicateIno1 - // QuirkNoUserXattr means that user.* xattrs are not supported - QuirkNoUserXattr ) -func logQuirk(s string) { - tlog.Info.Printf(tlog.ColorYellow + "DetectQuirks: " + s + tlog.ColorReset) +// LogQuirk prints a yellow message about a detected quirk. +func LogQuirk(s string) { + tlog.Info.Println(tlog.ColorYellow + "DetectQuirks: " + s + tlog.ColorReset) } diff --git a/internal/syscallcompat/quirks_darwin.go b/internal/syscallcompat/quirks_darwin.go index 4adeea1..c4d5006 100644 --- a/internal/syscallcompat/quirks_darwin.go +++ b/internal/syscallcompat/quirks_darwin.go @@ -33,7 +33,7 @@ func DetectQuirks(cipherdir string) (q uint64) { // On MacOS ExFAT, all empty files share inode number 1: // https://github.com/rfjakob/gocryptfs/issues/585 if fstypename == FstypenameExfat { - logQuirk("ExFAT detected, disabling hard links. See https://github.com/rfjakob/gocryptfs/issues/585 for why.") + LogQuirk("ExFAT detected, disabling hard links. See https://github.com/rfjakob/gocryptfs/issues/585 for why.") q |= QuirkDuplicateIno1 } diff --git a/internal/syscallcompat/quirks_linux.go b/internal/syscallcompat/quirks_linux.go index 5ef2d8a..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,14 +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("Btrfs detected, forcing -noprealloc. See https://github.com/rfjakob/gocryptfs/issues/395 for why.") - q |= QuirkBrokenFalloc - } - - if uint32(st.Type) == unix.TMPFS_MAGIC { - logQuirk("tmpfs detected, no extended attributes except acls will work.") + 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 1aa6a6e..3cb9ffa 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 } } @@ -112,10 +112,10 @@ const XATTR_SIZE_MAX = 65536 // Make the buffer 1kB bigger so we can detect overflows. Unfortunately, // slices larger than 64kB are always allocated on the heap. -const XATTR_BUFSZ = XATTR_SIZE_MAX + 1024 +const GETXATTR_BUFSZ_BIG = XATTR_SIZE_MAX + 1024 // We try with a small buffer first - this one can be allocated on the stack. -const XATTR_BUFSZ_SMALL = 500 +const GETXATTR_BUFSZ_SMALL = 500 // Fgetxattr is a wrapper around unix.Fgetxattr that handles the buffer sizing. func Fgetxattr(fd int, attr string) (val []byte, err error) { @@ -135,7 +135,7 @@ func Lgetxattr(path string, attr string) (val []byte, err error) { func getxattrSmartBuf(fn func(buf []byte) (int, error)) ([]byte, error) { // Fastpaths. Important for security.capabilities, which gets queried a lot. - buf := make([]byte, XATTR_BUFSZ_SMALL) + buf := make([]byte, GETXATTR_BUFSZ_SMALL) sz, err := fn(buf) // Non-existing xattr if err == unix.ENODATA { @@ -159,7 +159,7 @@ func getxattrSmartBuf(fn func(buf []byte) (int, error)) ([]byte, error) { // We choose the simple approach of buffer that is bigger than the limit on // Linux, and return an error for everything that is bigger (which can // only happen on MacOS). - buf = make([]byte, XATTR_BUFSZ) + buf = make([]byte, GETXATTR_BUFSZ_BIG) sz, err = fn(buf) if err == syscall.ERANGE { // Do NOT return ERANGE - the user might retry ad inifinitum! @@ -182,42 +182,44 @@ out: // Flistxattr is a wrapper for unix.Flistxattr that handles buffer sizing and // parsing the returned blob to a string slice. func Flistxattr(fd int) (attrs []string, err error) { - // See the buffer sizing comments in getxattrSmartBuf. - // TODO: smarter buffer sizing? - buf := make([]byte, XATTR_BUFSZ) - sz, err := unix.Flistxattr(fd, buf) - if err == syscall.ERANGE { - // Do NOT return ERANGE - the user might retry ad inifinitum! - return nil, syscall.EOVERFLOW + listxattrSyscall := func(buf []byte) (int, error) { + return unix.Flistxattr(fd, buf) } - if err != nil { - return nil, err - } - if sz >= XATTR_SIZE_MAX { - return nil, syscall.EOVERFLOW - } - attrs = parseListxattrBlob(buf[:sz]) - return attrs, nil + return listxattrSmartBuf(listxattrSyscall) } // Llistxattr is a wrapper for unix.Llistxattr that handles buffer sizing and // parsing the returned blob to a string slice. func Llistxattr(path string) (attrs []string, err error) { - // TODO: smarter buffer sizing? - buf := make([]byte, XATTR_BUFSZ) - sz, err := unix.Llistxattr(path, buf) - if err == syscall.ERANGE { - // Do NOT return ERANGE - the user might retry ad inifinitum! - return nil, syscall.EOVERFLOW + listxattrSyscall := func(buf []byte) (int, error) { + return unix.Llistxattr(path, buf) } - if err != nil { - return nil, err - } - if sz >= XATTR_SIZE_MAX { - return nil, syscall.EOVERFLOW + return listxattrSmartBuf(listxattrSyscall) +} + +// listxattrSmartBuf handles smart buffer sizing for Flistxattr and Llistxattr +func listxattrSmartBuf(listxattrSyscall func([]byte) (int, error)) ([]string, error) { + const LISTXATTR_BUFSZ_SMALL = 100 + + // Blindly try with the small buffer first + buf := make([]byte, LISTXATTR_BUFSZ_SMALL) + sz, err := listxattrSyscall(buf) + if err == syscall.ERANGE { + // Did not fit. Find the actual size + sz, err = listxattrSyscall(nil) + if err != nil { + return nil, err + } + // ...and allocate the buffer to fit + buf = make([]byte, sz) + sz, err = listxattrSyscall(buf) + if err != nil { + // When an xattr got added between the size probe and here, + // we could fail with ERANGE. This is ok as the caller will retry. + return nil, err + } } - attrs = parseListxattrBlob(buf[:sz]) - return attrs, nil + return parseListxattrBlob(buf[:sz]), nil } func parseListxattrBlob(buf []byte) (attrs []string) { diff --git a/internal/syscallcompat/sys_darwin.go b/internal/syscallcompat/sys_darwin.go index 0ebdd3b..ef19f24 100644 --- a/internal/syscallcompat/sys_darwin.go +++ b/internal/syscallcompat/sys_darwin.go @@ -27,6 +27,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_linux.go b/internal/syscallcompat/sys_linux.go index 19d2c56..71478af 100644 --- a/internal/syscallcompat/sys_linux.go +++ b/internal/syscallcompat/sys_linux.go @@ -28,6 +28,10 @@ const ( RENAME_NOREPLACE = unix.RENAME_NOREPLACE RENAME_WHITEOUT = unix.RENAME_WHITEOUT RENAME_EXCHANGE = unix.RENAME_EXCHANGE + + // 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/internal/tlog/log.go b/internal/tlog/log.go index 62d791d..02c760c 100644 --- a/internal/tlog/log.go +++ b/internal/tlog/log.go @@ -73,7 +73,7 @@ func (l *toggledLogger) Printf(format string, v ...interface{}) { return } msg := trimNewline(fmt.Sprintf(format, v...)) - l.Logger.Printf(l.prefix + msg + l.postfix) + l.Logger.Print(l.prefix + msg + l.postfix) if l.Wpanic { l.Logger.Panic(wpanicMsg + msg) } @@ -120,7 +120,7 @@ func changePassword(args *argContainer) { tlog.Fatal.Println(err) os.Exit(exitcodes.WriteConf) } - tlog.Info.Printf(tlog.ColorGreen + "Password changed." + tlog.ColorReset) + tlog.Info.Println(tlog.ColorGreen + "Password changed." + tlog.ColorReset) } func main() { diff --git a/masterkey.go b/masterkey.go index d488441..c67c5d5 100644 --- a/masterkey.go +++ b/masterkey.go @@ -24,9 +24,9 @@ func unhexMasterKey(masterkey string, fromStdin bool) []byte { tlog.Fatal.Printf("Master key has length %d but we require length %d", len(key), cryptocore.KeyLen) os.Exit(exitcodes.MasterKey) } - tlog.Info.Printf("Using explicit master key.") + tlog.Info.Println("Using explicit master key.") if !fromStdin { - tlog.Info.Printf(tlog.ColorYellow + + tlog.Info.Println(tlog.ColorYellow + "THE MASTER KEY IS VISIBLE VIA \"ps ax\" AND MAY BE STORED IN YOUR SHELL HISTORY!\n" + "ONLY USE THIS MODE FOR EMERGENCIES" + tlog.ColorReset) } @@ -52,8 +52,8 @@ func handleArgsMasterkey(args *argContainer) (masterkey []byte) { } // "-zerokey" if args.zerokey { - tlog.Info.Printf("Using all-zero dummy master key.") - tlog.Info.Printf(tlog.ColorYellow + + tlog.Info.Println("Using all-zero dummy master key.") + tlog.Info.Println(tlog.ColorYellow + "ZEROKEY MODE PROVIDES NO SECURITY AT ALL AND SHOULD ONLY BE USED FOR TESTING." + tlog.ColorReset) return make([]byte, cryptocore.KeyLen) @@ -284,6 +284,7 @@ func initFuseFrontend(args *argContainer) (rootNode fs.InodeEmbedder, wipeKeys f SharedStorage: args.sharedstorage, OneFileSystem: args.one_file_system, DeterministicNames: args.deterministic_names, + NoXattr: args.noxattr, } // confFile is nil when "-zerokey" or "-masterkey" was used if confFile != nil { @@ -412,7 +413,7 @@ func initGoFuse(rootNode fs.InodeEmbedder, args *argContainer) *fuse.Server { mOpts := &fuseOpts.MountOptions opts := make(map[string]string) if args.allow_other { - tlog.Info.Printf(tlog.ColorYellow + "The option \"-allow_other\" is set. Make sure the file " + + tlog.Info.Println(tlog.ColorYellow + "The option \"-allow_other\" is set. Make sure the file " + "permissions protect your data from unwanted access." + tlog.ColorReset) mOpts.AllowOther = true // Make the kernel check the file permissions for us 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/defaults/acl_test.go b/tests/defaults/acl_test.go index 0dae018..bddb5e5 100644 --- a/tests/defaults/acl_test.go +++ b/tests/defaults/acl_test.go @@ -7,7 +7,6 @@ import ( "path/filepath" "syscall" "testing" - "time" "golang.org/x/sys/unix" @@ -21,8 +20,6 @@ func TestCpA(t *testing.T) { fn1 := filepath.Join(test_helpers.TmpDir, t.Name()) fn2 := filepath.Join(test_helpers.DefaultPlainDir, t.Name()) - rand.Seed(int64(time.Now().Nanosecond())) - { // Need unrestricted umask old := syscall.Umask(000) 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/plaintextnames/file_holes_test.go b/tests/plaintextnames/file_holes_test.go index ea47113..7a356c0 100644 --- a/tests/plaintextnames/file_holes_test.go +++ b/tests/plaintextnames/file_holes_test.go @@ -7,7 +7,6 @@ import ( "os/exec" "syscall" "testing" - "time" "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" @@ -140,7 +139,6 @@ func TestFileHoleCopy(t *testing.T) { return } - rand.Seed(time.Now().UnixNano()) for k := 0; k < 100; k++ { c1 := make([]int64, 10) for i := range c1 { diff --git a/tests/reverse/xattr_test.go b/tests/reverse/xattr_test.go index c70f623..b6a7b34 100644 --- a/tests/reverse/xattr_test.go +++ b/tests/reverse/xattr_test.go @@ -4,10 +4,12 @@ import ( "fmt" "os" "path/filepath" + "strings" "syscall" "testing" "github.com/pkg/xattr" + "golang.org/x/sys/unix" ) func xattrSupported(path string) bool { @@ -20,8 +22,6 @@ func xattrSupported(path string) bool { } func TestXattrList(t *testing.T) { - t.Skip("TODO: not implemented yet in reverse mode") - if !xattrSupported(dirA) { t.Skip() } @@ -32,7 +32,7 @@ func TestXattrList(t *testing.T) { } val := []byte("xxxxxxxxyyyyyyyyyyyyyyyzzzzzzzzzzzzz") num := 20 - var namesA map[string]string + namesA := map[string]string{} for i := 1; i <= num; i++ { attr := fmt.Sprintf("user.TestXattrList.%02d", i) err = xattr.LSet(fnA, attr, val) @@ -46,9 +46,14 @@ func TestXattrList(t *testing.T) { if err != nil { t.Fatal(err) } - var namesC map[string]string + namesC := map[string]string{} for _, n := range tmp { - namesC[n] = string(val) + if strings.HasPrefix(n, "security.") { + t.Logf("Ignoring xattr %q", n) + continue + } + v, _ := xattr.LGet(fnC, n) + namesC[n] = string(v) } if len(namesA) != len(namesC) { t.Errorf("wrong number of names, want=%d have=%d", len(namesA), len(namesC)) @@ -61,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 4f2527a..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.QuirkBrokenFalloc { + 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) + } +} diff --git a/tests/root_test/root_test.go b/tests/root_test/root_test.go index c531ebb..650d802 100644 --- a/tests/root_test/root_test.go +++ b/tests/root_test/root_test.go @@ -305,9 +305,6 @@ func TestOverlay(t *testing.T) { t.Skip("must run as root") } cDir := test_helpers.InitFS(t) - if syscallcompat.DetectQuirks(cDir)&syscallcompat.QuirkNoUserXattr != 0 { - t.Logf("No user xattrs! overlay mount will likely fail.") - } os.Chmod(cDir, 0755) pDir := cDir + ".mnt" test_helpers.MountOrFatal(t, cDir, pDir, "-allow_other", "-extpass=echo test") |
