aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yml8
-rw-r--r--Documentation/performance.txt4
-rw-r--r--README.md2
-rw-r--r--cli_args.go3
-rw-r--r--contrib/findholes/holes/holes.go2
-rw-r--r--go.mod13
-rw-r--r--go.sum30
-rw-r--r--init_dir.go4
-rw-r--r--internal/cryptocore/hkdf.go15
-rw-r--r--internal/fusefrontend/args.go2
-rw-r--r--internal/fusefrontend/file.go4
-rw-r--r--internal/fusefrontend/node_dir_ops.go11
-rw-r--r--internal/fusefrontend/node_open_create.go29
-rw-r--r--internal/fusefrontend/node_xattr.go39
-rw-r--r--internal/fusefrontend/node_xattr_darwin.go17
-rw-r--r--internal/fusefrontend/node_xattr_linux.go3
-rw-r--r--internal/fusefrontend/root_node.go31
-rw-r--r--internal/fusefrontend_reverse/node_api_check.go6
-rw-r--r--internal/fusefrontend_reverse/node_helpers.go2
-rw-r--r--internal/fusefrontend_reverse/node_xattr.go89
-rw-r--r--internal/fusefrontend_reverse/node_xattr_darwin.go55
-rw-r--r--internal/fusefrontend_reverse/node_xattr_linux.go43
-rw-r--r--internal/fusefrontend_reverse/root_node.go39
-rw-r--r--internal/nametransform/names.go43
-rw-r--r--internal/nametransform/nfc_test.go29
-rw-r--r--internal/pathiv/pathiv.go2
-rw-r--r--internal/syscallcompat/quirks.go11
-rw-r--r--internal/syscallcompat/quirks_darwin.go2
-rw-r--r--internal/syscallcompat/quirks_linux.go44
-rw-r--r--internal/syscallcompat/sys_common.go74
-rw-r--r--internal/syscallcompat/sys_darwin.go4
-rw-r--r--internal/syscallcompat/sys_linux.go4
-rw-r--r--internal/tlog/log.go2
-rw-r--r--main.go2
-rw-r--r--masterkey.go8
-rw-r--r--mount.go3
-rwxr-xr-xprofiling/tinyfiles.bash33
-rw-r--r--tests/defaults/acl_test.go3
-rw-r--r--tests/matrix/symlink_darwin_test.go39
-rw-r--r--tests/matrix/symlink_linux_test.go47
-rw-r--r--tests/plaintextnames/file_holes_test.go2
-rw-r--r--tests/reverse/xattr_test.go30
-rw-r--r--tests/root_test/btrfs_test.go65
-rw-r--r--tests/root_test/root_test.go3
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):
diff --git a/README.md b/README.md
index 4aced01..c5228dc 100644
--- a/README.md
+++ b/README.md
@@ -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))
diff --git a/go.mod b/go.mod
index 0af8ef1..66584d4 100644
--- a/go.mod
+++ b/go.mod
@@ -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
)
diff --git a/go.sum b/go.sum
index e22b76b..6d0cd6c 100644
--- a/go.sum
+++ b/go.sum
@@ -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)
}
diff --git a/main.go b/main.go
index cd643b5..62a7985 100644
--- a/main.go
+++ b/main.go
@@ -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)
diff --git a/mount.go b/mount.go
index 5c0f064..f7378aa 100644
--- a/mount.go
+++ b/mount.go
@@ -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")