aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/configfile/config_file.go8
-rw-r--r--internal/configfile/feature_flags.go11
-rw-r--r--internal/fido2/fido2.go2
-rw-r--r--internal/fusefrontend/args.go4
-rw-r--r--internal/fusefrontend/file.go53
-rw-r--r--internal/fusefrontend/file_allocate_truncate.go62
-rw-r--r--internal/fusefrontend/file_lock.go51
-rw-r--r--internal/fusefrontend/node.go27
-rw-r--r--internal/fusefrontend/node_helpers.go18
-rw-r--r--internal/fusefrontend/node_open_create.go29
-rw-r--r--internal/fusefrontend/node_prepare_syscall.go4
-rw-r--r--internal/fusefrontend/node_xattr_darwin.go14
-rw-r--r--internal/fusefrontend/node_xattr_freebsd.go33
-rw-r--r--internal/fusefrontend/root_node.go31
-rw-r--r--internal/fusefrontend/statx_linux.go78
-rw-r--r--internal/fusefrontend_reverse/node_helpers.go16
-rw-r--r--internal/fusefrontend_reverse/node_xattr.go2
-rw-r--r--internal/fusefrontend_reverse/node_xattr_freebsd.go17
-rw-r--r--internal/fusefrontend_reverse/root_node.go32
-rw-r--r--internal/nametransform/diriv.go18
-rwxr-xr-xinternal/siv_aead/benchmark.bash2
-rwxr-xr-xinternal/speed/benchmark.bash2
-rwxr-xr-xinternal/stupidgcm/benchmark.bash2
-rw-r--r--internal/syscallcompat/asuser_freebsd.go24
-rw-r--r--internal/syscallcompat/emulate.go2
-rw-r--r--internal/syscallcompat/emulate_test.go2
-rw-r--r--internal/syscallcompat/quirks.go7
-rw-r--r--internal/syscallcompat/quirks_darwin.go2
-rw-r--r--internal/syscallcompat/quirks_freebsd.go22
-rw-r--r--internal/syscallcompat/quirks_linux.go40
-rw-r--r--internal/syscallcompat/sys_common.go76
-rw-r--r--internal/syscallcompat/sys_darwin.go11
-rw-r--r--internal/syscallcompat/sys_freebsd.go165
-rw-r--r--internal/syscallcompat/sys_linux.go21
-rw-r--r--internal/syscallcompat/unix2syscall.go (renamed from internal/syscallcompat/unix2syscall_darwin.go)2
35 files changed, 732 insertions, 158 deletions
diff --git a/internal/configfile/config_file.go b/internal/configfile/config_file.go
index 28a1ca5..ebd818e 100644
--- a/internal/configfile/config_file.go
+++ b/internal/configfile/config_file.go
@@ -31,7 +31,7 @@ type FIDO2Params struct {
// FIDO2 credential
CredentialID []byte
// FIDO2 hmac-secret salt
- HMACSalt []byte
+ HMACSalt []byte
AssertOptions []string
}
@@ -118,9 +118,9 @@ func Create(args *CreateArgs) error {
if len(args.Fido2CredentialID) > 0 {
cf.setFeatureFlag(FlagFIDO2)
cf.FIDO2 = &FIDO2Params{
- CredentialID: args.Fido2CredentialID,
- HMACSalt: args.Fido2HmacSalt,
- AssertOptions: args.Fido2AssertOptions,
+ CredentialID: args.Fido2CredentialID,
+ HMACSalt: args.Fido2HmacSalt,
+ AssertOptions: args.Fido2AssertOptions,
}
}
// Catch bugs and invalid cli flag combinations early
diff --git a/internal/configfile/feature_flags.go b/internal/configfile/feature_flags.go
index d6627a5..d5bd6d4 100644
--- a/internal/configfile/feature_flags.go
+++ b/internal/configfile/feature_flags.go
@@ -1,5 +1,9 @@
package configfile
+import (
+ "slices"
+)
+
type flagIota int
const (
@@ -64,10 +68,5 @@ func isFeatureFlagKnown(flag string) bool {
// IsFeatureFlagSet returns true if the feature flag "flagWant" is enabled.
func (cf *ConfFile) IsFeatureFlagSet(flagWant flagIota) bool {
flagString := knownFlags[flagWant]
- for _, flag := range cf.FeatureFlags {
- if flag == flagString {
- return true
- }
- }
- return false
+ return slices.Contains(cf.FeatureFlags, flagString)
}
diff --git a/internal/fido2/fido2.go b/internal/fido2/fido2.go
index e08e589..f47795b 100644
--- a/internal/fido2/fido2.go
+++ b/internal/fido2/fido2.go
@@ -44,7 +44,7 @@ func callFidoCommand(command fidoCommand, assertOptions []string, device string,
var args []string
args = append(args, "-G")
args = append(args, "-h")
- for i := range assertOptions{
+ for i := range assertOptions {
args = append(args, "-t")
args = append(args, assertOptions[i])
}
diff --git a/internal/fusefrontend/args.go b/internal/fusefrontend/args.go
index ec3d1c2..84e34af 100644
--- a/internal/fusefrontend/args.go
+++ b/internal/fusefrontend/args.go
@@ -24,6 +24,10 @@ type Args struct {
// location. If it is false, reverse mode maps ".gocryptfs.reverse.conf"
// to "gocryptfs.conf" in the plaintext dir.
ConfigCustom bool
+ // Config is the path to the config file. In reverse mode, a custom config
+ // file (-config) located inside Cipherdir is hidden from the encrypted view
+ // (https://github.com/rfjakob/gocryptfs/issues/1009).
+ Config string
// NoPrealloc disables automatic preallocation before writing
NoPrealloc bool
// Exclude is a list of paths to make inaccessible, starting match at
diff --git a/internal/fusefrontend/file.go b/internal/fusefrontend/file.go
index afee158..353029c 100644
--- a/internal/fusefrontend/file.go
+++ b/internal/fusefrontend/file.go
@@ -14,6 +14,8 @@ import (
"sync"
"syscall"
+ "golang.org/x/sys/unix"
+
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
@@ -90,6 +92,13 @@ func (f *File) readFileID() ([]byte, error) {
// and not only the header. A header-only file will be considered empty.
// This makes File ID poisoning more difficult.
readLen := contentenc.HeaderLen + 1
+ if f.rootNode.args.SharedStorage {
+ // With -sharedstorage, we consider a header-only file as valid, because
+ // another gocryptfs process may have either:
+ // 1) just created the header, and not written further data yet.
+ // 2) truncated the file down to just the header.
+ readLen = contentenc.HeaderLen
+ }
buf := make([]byte, readLen)
n, err := f.fd.ReadAt(buf, 0)
if err != nil {
@@ -115,7 +124,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) {
@@ -244,7 +253,23 @@ func (f *File) Read(ctx context.Context, buf []byte, off int64) (resultData fuse
tlog.Debug.Printf("ino%d: FUSE Read: offset=%d length=%d", f.qIno.Ino, off, len(buf))
out, errno := f.doRead(buf[:0], uint64(off), uint64(len(buf)))
if errno != 0 {
- return nil, errno
+ // With -sharedstorage, when we get a decryption error, we lock the
+ // byte range and try again.
+ if !(f.rootNode.args.SharedStorage && errno == syscall.EIO) {
+ return nil, errno
+ }
+ blocks := f.rootNode.contentEnc.ExplodePlainRange(uint64(off), uint64(len(buf)))
+ alignedOffset, alignedLength := blocks[0].JointCiphertextRange(blocks)
+ if err := f.LockSharedStorage(unix.F_RDLCK, int64(alignedOffset), int64(alignedLength)); err != nil {
+ tlog.Warn.Printf("ino%d: FUSE Read: LockSharedStorage(F_RDLCK, %d, %d) failed: %v", f.qIno.Ino, alignedOffset, alignedLength, err)
+ return nil, fs.ToErrno(err)
+ }
+ defer f.UnlockSharedStorage(int64(alignedOffset), int64(alignedLength))
+
+ out, errno = f.doRead(buf[:0], uint64(off), uint64(len(buf)))
+ if errno != 0 {
+ return nil, errno
+ }
}
tlog.Debug.Printf("ino%d: Read: errno=%d, returning %d bytes", f.qIno.Ino, errno, len(out))
return fuse.ReadResultData(out), errno
@@ -266,6 +291,12 @@ func (f *File) doWrite(data []byte, off int64) (uint32, syscall.Errno) {
//
// If the file ID is not cached, read it from disk
if f.fileTableEntry.ID == nil {
+ if err := f.LockSharedStorage(unix.F_WRLCK, 0, contentenc.HeaderLen); err != nil {
+ tlog.Warn.Printf("ino%d: doWrite: LockSharedStorage(F_WRLCK, %d, %d) failed: %v", f.qIno.Ino, 0, contentenc.HeaderLen, err)
+ return 0, fs.ToErrno(err)
+ }
+ defer f.UnlockSharedStorage(0, contentenc.HeaderLen)
+
var err error
fileID, err := f.readFileID()
// Write a new file header if the file is empty
@@ -285,7 +316,19 @@ func (f *File) doWrite(data []byte, off int64) (uint32, syscall.Errno) {
// Handle payload data
dataBuf := bytes.NewBuffer(data)
blocks := f.rootNode.contentEnc.ExplodePlainRange(uint64(off), uint64(len(data)))
+ cOff, lkLen := blocks[0].JointCiphertextRange(blocks)
toEncrypt := make([][]byte, len(blocks))
+
+ // As we must write complete ciphertext blocks (except at EOF), non-overlapping
+ // plaintext writes can overlap in the ciphertext.
+ // And because overlapping writes can turn the data into data soup (see
+ // TestPoCTornWrite) we serialize them using fcntl locking.
+ if err := f.LockSharedStorage(unix.F_WRLCK, int64(cOff), int64(lkLen)); err != nil {
+ tlog.Warn.Printf("ino%d: LockSharedStorage(F_WRLCK, %d, %d) failed: %v", f.qIno.Ino, cOff, int64(lkLen), err)
+ return 0, fs.ToErrno(err)
+ }
+ defer f.UnlockSharedStorage(int64(cOff), int64(lkLen))
+
for i, b := range blocks {
blockData := dataBuf.Next(int(b.Length))
// Incomplete block -> Read-Modify-Write
@@ -310,12 +353,11 @@ func (f *File) doWrite(data []byte, off int64) (uint32, syscall.Errno) {
// Preallocate so we cannot run out of space in the middle of the write.
// This prevents partially written (=corrupt) blocks.
var err error
- cOff := blocks[0].BlockCipherOff()
// f.fd.WriteAt & syscallcompat.EnospcPrealloc take int64 offsets!
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) {
@@ -413,9 +455,6 @@ func (f *File) Flush(ctx context.Context) syscall.Errno {
}
// Fsync: handles FUSE opcode FSYNC
-//
-// Unfortunately, as Node.Fsync is also defined and takes precedence,
-// File.Fsync is never called at the moment.
func (f *File) Fsync(ctx context.Context, flags uint32) (errno syscall.Errno) {
f.fdLock.RLock()
defer f.fdLock.RUnlock()
diff --git a/internal/fusefrontend/file_allocate_truncate.go b/internal/fusefrontend/file_allocate_truncate.go
index a3decf9..f4a078c 100644
--- a/internal/fusefrontend/file_allocate_truncate.go
+++ b/internal/fusefrontend/file_allocate_truncate.go
@@ -9,6 +9,10 @@ import (
"sync"
"syscall"
+ "golang.org/x/sys/unix"
+
+ "github.com/rfjakob/gocryptfs/v2/internal/contentenc"
+
"github.com/hanwen/go-fuse/v2/fs"
"github.com/rfjakob/gocryptfs/v2/internal/syscallcompat"
@@ -92,20 +96,62 @@ func (f *File) Allocate(ctx context.Context, off uint64, sz uint64, mode uint32)
return f.truncateGrowFile(oldPlainSz, newPlainSz)
}
-// truncate - called from Setattr.
+// truncate - called from node.Setattr and file.Setattr.
+//
+// The caller must hold f.fileTableEntry.ContentLock
func (f *File) truncate(newSize uint64) (errno syscall.Errno) {
var err error
// Common case first: Truncate to zero
if newSize == 0 {
- err = syscall.Ftruncate(int(f.fd.Fd()), 0)
- if err != nil {
- tlog.Warn.Printf("ino%d fh%d: Ftruncate(fd, 0) returned error: %v", f.qIno.Ino, f.intFd(), err)
- return fs.ToErrno(err)
+ if !f.rootNode.args.SharedStorage {
+ err = syscall.Ftruncate(int(f.fd.Fd()), 0)
+ if err != nil {
+ tlog.Warn.Printf("ino%d fh%d: Ftruncate(fd, 0) returned error: %v", f.qIno.Ino, f.intFd(), err)
+ return fs.ToErrno(err)
+ }
+ // Truncate to zero kills the file header
+ f.fileTableEntry.ID = nil
+ return 0
+ } else {
+ // Prevent reads and writes concurrent with the truncate operation. It's
+ // racy on tmpfs and ext4 ( https://lore.kernel.org/all/18e9fa0f-ec31-9107-459c-ae1694503f87@gmail.com/t/ )
+ // as evident by TestOpenTruncate test failures.
+ err = f.LockSharedStorage(unix.F_WRLCK, 0, 0)
+ if err != nil {
+ return fs.ToErrno(err)
+ }
+ defer f.UnlockSharedStorage(0, 0)
+
+ // With -sharedstorage, we keep the on-disk file header.
+ // Other mounts may have the file ID cached so we cannot mess with it.
+
+ // The file must have a header if we have the file ID cached,
+ // so we can blindly truncate.
+ if f.fileTableEntry.ID != nil {
+ return fs.ToErrno(syscall.Ftruncate(int(f.fd.Fd()), contentenc.HeaderLen))
+ }
+ // We don't have the file ID cached, so the file may be empty on disk.
+ // We don't want to grow it, as this will create an all-zero header.
+ fi, err := f.fd.Stat()
+ if err != nil {
+ return fs.ToErrno(err)
+ }
+ if fi.Size() == 0 {
+ // nothing to do
+ return 0
+ }
+ if fi.Size() == contentenc.HeaderLen {
+ // nothing to do
+ return 0
+ } else if fi.Size() > contentenc.HeaderLen {
+ return fs.ToErrno(syscall.Ftruncate(int(f.fd.Fd()), contentenc.HeaderLen))
+ } else {
+ tlog.Warn.Printf("truncate i%d: partial header, size=%d", f.qIno.Ino, fi.Size())
+ return syscall.EIO
+ }
}
- // Truncate to zero kills the file header
- f.fileTableEntry.ID = nil
- return 0
}
+
// We need the old file size to determine if we are growing or shrinking
// the file
oldSize, err := f.statPlainSize()
diff --git a/internal/fusefrontend/file_lock.go b/internal/fusefrontend/file_lock.go
new file mode 100644
index 0000000..c2965ae
--- /dev/null
+++ b/internal/fusefrontend/file_lock.go
@@ -0,0 +1,51 @@
+package fusefrontend
+
+import (
+ "golang.org/x/sys/unix"
+
+ "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat"
+ "github.com/rfjakob/gocryptfs/v2/internal/tlog"
+)
+
+// SharedStorageLock conveniently wraps F_OFD_SETLKW.
+// It is a no-op unless args.SharedStorage is set.
+//
+// See https://man7.org/linux/man-pages/man2/fcntl.2.html -> "Open file description locks (non-POSIX)"
+//
+// lkType is one of:
+// * unix.F_RDLCK (shared read lock)
+// * unix.F_WRLCK (exclusive write lock)
+// * unix.F_UNLCK (unlock)
+//
+// This function is a no-op if args.SharedStorage == false.
+func (f *File) LockSharedStorage(lkType int16, lkStart int64, lkLen int64) (err error) {
+ if !f.rootNode.args.SharedStorage {
+ return nil
+ }
+ lk := unix.Flock_t{
+ Type: lkType,
+ Whence: unix.SEEK_SET,
+ Start: lkStart,
+ Len: lkLen,
+ }
+ err = unix.FcntlFlock(uintptr(f.intFd()), syscallcompat.F_OFD_SETLK, &lk)
+ switch err {
+ case unix.EACCES, unix.EAGAIN:
+ tlog.Debug.Printf("LockSharedStorage: waiting for lock")
+ case nil:
+ return
+ }
+ for {
+ err = unix.FcntlFlock(uintptr(f.intFd()), syscallcompat.F_OFD_SETLKW, &lk)
+ if err == unix.EINTR {
+ tlog.Debug.Printf("LockSharedStorage: looping on EINTR")
+ continue
+ }
+ return
+ }
+}
+
+// UnlockSharedStorage calls LockSharedStorage with unix.F_UNLCK.
+func (f *File) UnlockSharedStorage(lkStart int64, lkLen int64) error {
+ return f.LockSharedStorage(unix.F_UNLCK, lkStart, lkLen)
+}
diff --git a/internal/fusefrontend/node.go b/internal/fusefrontend/node.go
index 95be48d..c531876 100644
--- a/internal/fusefrontend/node.go
+++ b/internal/fusefrontend/node.go
@@ -38,7 +38,7 @@ func (n *Node) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (ch
ch = n.newChild(ctx, st, out)
// Translate ciphertext size in `out.Attr.Size` to plaintext size
- n.translateSize(dirfd, cName, &out.Attr)
+ out.Size = n.translateSize(dirfd, cName, out.Mode, out.Size)
rn := n.rootNode()
if rn.args.ForceOwner != nil {
@@ -116,7 +116,7 @@ func (n *Node) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut)
out.Attr.FromStat(st)
// Translate ciphertext size in `out.Attr.Size` to plaintext size
- n.translateSize(dirfd, cName, &out.Attr)
+ out.Size = n.translateSize(dirfd, cName, out.Mode, out.Size)
out:
if rn.args.ForceOwner != nil {
@@ -246,6 +246,8 @@ func (n *Node) Setattr(ctx context.Context, f fs.FileHandle, in *fuse.SetAttrIn,
}
f2 := f.(*File)
defer f2.Release(ctx)
+ f2.fileTableEntry.ContentLock.Lock()
+ defer f2.fileTableEntry.ContentLock.Unlock()
errno = syscall.Errno(f2.truncate(sz))
if errno != 0 {
return errno
@@ -371,7 +373,7 @@ func (n *Node) Link(ctx context.Context, target fs.InodeEmbedder, name string, o
return
}
inode = n.newChild(ctx, st, out)
- n.translateSize(dirfd, cName, &out.Attr)
+ out.Size = n.translateSize(dirfd, cName, out.Mode, out.Size)
return inode, 0
}
@@ -514,22 +516,3 @@ func (n *Node) Rename(ctx context.Context, name string, newParent fs.InodeEmbedd
}
return 0
}
-
-// Fsync: handles FUSE opcodes FSYNC & FDIRSYNC
-//
-// Note: f is always set to nil by go-fuse
-func (n *Node) Fsync(ctx context.Context, f fs.FileHandle, flags uint32) syscall.Errno {
- dirfd, cName, errno := n.prepareAtSyscallMyself()
- if errno != 0 {
- return errno
- }
- defer syscall.Close(dirfd)
-
- fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NOFOLLOW, 0)
- if err != nil {
- return fs.ToErrno(err)
- }
- defer syscall.Close(fd)
-
- return fs.ToErrno(syscall.Fsync(fd))
-}
diff --git a/internal/fusefrontend/node_helpers.go b/internal/fusefrontend/node_helpers.go
index e8fca80..3102275 100644
--- a/internal/fusefrontend/node_helpers.go
+++ b/internal/fusefrontend/node_helpers.go
@@ -53,18 +53,18 @@ func (n *Node) readlink(dirfd int, cName string) (out []byte, errno syscall.Errn
return []byte(target), 0
}
-// translateSize translates the ciphertext size in `out` into plaintext size.
+// translateSize translates the ciphertext size cSize into plaintext size.
// Handles regular files & symlinks (and finds out what is what by looking at
-// `out.Mode`).
-func (n *Node) translateSize(dirfd int, cName string, out *fuse.Attr) {
- if out.IsRegular() {
- rn := n.rootNode()
- out.Size = rn.contentEnc.CipherSizeToPlainSize(out.Size)
- } else if out.IsSymlink() {
- // read and decrypt target
+// mode).
+func (n *Node) translateSize(dirfd int, cName string, mode uint32, cSize uint64) (pSize uint64) {
+ switch mode & syscall.S_IFMT {
+ case syscall.S_IFREG:
+ return n.rootNode().contentEnc.CipherSizeToPlainSize(cSize)
+ case syscall.S_IFLNK:
target, _ := n.readlink(dirfd, cName)
- out.Size = uint64(len(target))
+ return uint64(len(target))
}
+ return cSize
}
// Path returns the relative plaintext path of this node
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_prepare_syscall.go b/internal/fusefrontend/node_prepare_syscall.go
index 9021350..03194df 100644
--- a/internal/fusefrontend/node_prepare_syscall.go
+++ b/internal/fusefrontend/node_prepare_syscall.go
@@ -3,6 +3,7 @@ package fusefrontend
import (
"syscall"
+ "github.com/rfjakob/gocryptfs/v2/internal/nametransform"
"github.com/rfjakob/gocryptfs/v2/internal/tlog"
"github.com/hanwen/go-fuse/v2/fs"
@@ -73,8 +74,9 @@ func (n *Node) prepareAtSyscall(child string) (dirfd int, cName string, errno sy
var err error
iv, err = rn.nameTransform.ReadDirIVAt(dirfd)
if err != nil {
+ tlog.Warn.Printf("prepareAtSyscall: could not read %s: %v", nametransform.DirIVFilename, err)
syscall.Close(dirfd)
- return -1, "", fs.ToErrno(err)
+ return -1, "", syscall.EIO
}
}
rn.dirCache.Store(n, dirfd, iv)
diff --git a/internal/fusefrontend/node_xattr_darwin.go b/internal/fusefrontend/node_xattr_darwin.go
index f8f224f..1d25f3d 100644
--- a/internal/fusefrontend/node_xattr_darwin.go
+++ b/internal/fusefrontend/node_xattr_darwin.go
@@ -29,8 +29,8 @@ func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) {
}
defer syscall.Close(dirfd)
- // O_NONBLOCK to not block on FIFOs.
- fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0)
+ // O_NONBLOCK to not block on FIFOs, O_SYMLINK to open the symlink itself (if it is one).
+ fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0)
if err != nil {
return nil, fs.ToErrno(err)
}
@@ -52,10 +52,10 @@ func (n *Node) setXAttr(context *fuse.Context, cAttr string, cData []byte, flags
defer syscall.Close(dirfd)
// O_NONBLOCK to not block on FIFOs.
- fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0)
+ fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0)
// Directories cannot be opened read-write. Retry.
if err == syscall.EISDIR {
- fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0)
+ fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0)
}
if err != nil {
fs.ToErrno(err)
@@ -74,10 +74,10 @@ func (n *Node) removeXAttr(cAttr string) (errno syscall.Errno) {
defer syscall.Close(dirfd)
// O_NONBLOCK to not block on FIFOs.
- fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0)
+ fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0)
// Directories cannot be opened read-write. Retry.
if err == syscall.EISDIR {
- fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0)
+ fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0)
}
if err != nil {
return fs.ToErrno(err)
@@ -96,7 +96,7 @@ func (n *Node) listXAttr() (out []string, errno syscall.Errno) {
defer syscall.Close(dirfd)
// O_NONBLOCK to not block on FIFOs.
- fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0)
+ fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0)
if err != nil {
return nil, fs.ToErrno(err)
}
diff --git a/internal/fusefrontend/node_xattr_freebsd.go b/internal/fusefrontend/node_xattr_freebsd.go
new file mode 100644
index 0000000..9698283
--- /dev/null
+++ b/internal/fusefrontend/node_xattr_freebsd.go
@@ -0,0 +1,33 @@
+package fusefrontend
+
+import (
+ "golang.org/x/sys/unix"
+
+ "github.com/hanwen/go-fuse/v2/fuse"
+)
+
+const noSuchAttributeError = unix.ENOATTR
+
+func filterXattrSetFlags(flags int) int {
+ return flags
+}
+
+func (n *Node) getXAttr(cAttr string) (out []byte, errno unix.Errno) {
+ // TODO
+ return nil, unix.EOPNOTSUPP
+}
+
+func (n *Node) setXAttr(context *fuse.Context, cAttr string, cData []byte, flags uint32) (errno unix.Errno) {
+ // TODO
+ return unix.EOPNOTSUPP
+}
+
+func (n *Node) removeXAttr(cAttr string) (errno unix.Errno) {
+ // TODO
+ return unix.EOPNOTSUPP
+}
+
+func (n *Node) listXAttr() (out []string, errno unix.Errno) {
+ // TODO
+ return nil, unix.EOPNOTSUPP
+}
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/statx_linux.go b/internal/fusefrontend/statx_linux.go
new file mode 100644
index 0000000..f9f87cf
--- /dev/null
+++ b/internal/fusefrontend/statx_linux.go
@@ -0,0 +1,78 @@
+package fusefrontend
+
+import (
+ "context"
+ "syscall"
+
+ "github.com/hanwen/go-fuse/v2/fs"
+ "github.com/hanwen/go-fuse/v2/fuse"
+ "golang.org/x/sys/unix"
+
+ "github.com/rfjakob/gocryptfs/v2/internal/inomap"
+ "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat"
+)
+
+var _ = (fs.NodeStatxer)((*Node)(nil))
+var _ = (fs.FileStatxer)((*File)(nil))
+
+// Statx is the Linux statx equivalent of Getattr.
+func (n *Node) Statx(ctx context.Context, f fs.FileHandle, flags uint32, mask uint32, out *fuse.StatxOut) (errno syscall.Errno) {
+ // If the kernel gives us a file handle, use it. Current Linux kernels do
+ // not send one with FUSE_STATX, but keep this for future compatibility.
+ if f != nil {
+ if fsx, ok := f.(fs.FileStatxer); ok {
+ return fsx.Statx(ctx, flags, mask, out)
+ }
+ }
+
+ dirfd, cName, errno := n.prepareAtSyscallMyself()
+ if errno != 0 {
+ return errno
+ }
+ defer syscall.Close(dirfd)
+
+ var st unix.Statx_t
+ err := syscallcompat.Statx(dirfd, cName, int(flags)|unix.AT_SYMLINK_NOFOLLOW, int(mask), &st)
+ if err != nil {
+ return fs.ToErrno(err)
+ }
+
+ // fix inode number, size, owner
+ rn := n.rootNode()
+ st.Ino = rn.inoMap.Translate(inomap.NewQIno(unix.Mkdev(st.Dev_major, st.Dev_minor), 0, st.Ino))
+ st.Size = rn.translateSize(dirfd, cName, uint32(st.Mode), st.Size)
+ if rn.args.ForceOwner != nil {
+ st.Uid = rn.args.ForceOwner.Uid
+ st.Gid = rn.args.ForceOwner.Gid
+ }
+
+ out.FromStatx(&st)
+ return 0
+}
+
+// Statx returns statx information for an open backing file. Current Linux
+// kernels do not send a file handle with FUSE_STATX, so this is not reached yet.
+func (f *File) Statx(_ context.Context, flags uint32, mask uint32, out *fuse.StatxOut) syscall.Errno {
+ f.fdLock.RLock()
+ defer f.fdLock.RUnlock()
+
+ var st unix.Statx_t
+ err := syscallcompat.Statx(f.intFd(), "", int(flags)|unix.AT_EMPTY_PATH, int(mask), &st)
+ if err != nil {
+ return fs.ToErrno(err)
+ }
+
+ // fix inode number, size, owner
+ rn := f.rootNode
+ st.Ino = rn.inoMap.Translate(inomap.NewQIno(unix.Mkdev(st.Dev_major, st.Dev_minor), 0, st.Ino))
+ if uint32(st.Mode)&syscall.S_IFMT == syscall.S_IFREG {
+ st.Size = rn.contentEnc.CipherSizeToPlainSize(st.Size)
+ }
+ if rn.args.ForceOwner != nil {
+ st.Uid = rn.args.ForceOwner.Uid
+ st.Gid = rn.args.ForceOwner.Gid
+ }
+
+ out.FromStatx(&st)
+ return 0
+}
diff --git a/internal/fusefrontend_reverse/node_helpers.go b/internal/fusefrontend_reverse/node_helpers.go
index 3165db6..dc8d928 100644
--- a/internal/fusefrontend_reverse/node_helpers.go
+++ b/internal/fusefrontend_reverse/node_helpers.go
@@ -17,13 +17,13 @@ import (
)
const (
- // File names are padded to 16-byte multiples, encrypted and
- // base64-encoded. We can encode at most 176 bytes to stay below the 255
- // bytes limit:
- // * base64(176 bytes) = 235 bytes
- // * base64(192 bytes) = 256 bytes (over 255!)
- // But the PKCS#7 padding is at least one byte. This means we can only use
- // 175 bytes for the file name.
+// File names are padded to 16-byte multiples, encrypted and
+// base64-encoded. We can encode at most 176 bytes to stay below the 255
+// bytes limit:
+// * base64(176 bytes) = 235 bytes
+// * base64(192 bytes) = 256 bytes (over 255!)
+// But the PKCS#7 padding is at least one byte. This means we can only use
+// 175 bytes for the file name.
)
// translateSize translates the ciphertext size in `out` into plaintext size.
@@ -134,7 +134,7 @@ func (n *Node) lookupLongnameName(ctx context.Context, nameFile string, out *fus
if errno != 0 {
return
}
- if rn.isExcludedPlain(filepath.Join(d.cPath, pName)) {
+ if rn.isExcludedPlain(filepath.Join(d.pPath, pName)) {
errno = syscall.EPERM
return
}
diff --git a/internal/fusefrontend_reverse/node_xattr.go b/internal/fusefrontend_reverse/node_xattr.go
index f22764a..b940339 100644
--- a/internal/fusefrontend_reverse/node_xattr.go
+++ b/internal/fusefrontend_reverse/node_xattr.go
@@ -40,7 +40,7 @@ func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32,
} else {
pAttr, err := rn.decryptXattrName(attr)
if err != nil {
- return 0, syscall.EINVAL
+ return 0, noSuchAttributeError
}
pData, errno := n.getXAttr(pAttr)
if errno != 0 {
diff --git a/internal/fusefrontend_reverse/node_xattr_freebsd.go b/internal/fusefrontend_reverse/node_xattr_freebsd.go
new file mode 100644
index 0000000..949cf15
--- /dev/null
+++ b/internal/fusefrontend_reverse/node_xattr_freebsd.go
@@ -0,0 +1,17 @@
+package fusefrontend_reverse
+
+import (
+ "golang.org/x/sys/unix"
+)
+
+const noSuchAttributeError = unix.ENOATTR
+
+func (n *Node) getXAttr(cAttr string) (out []byte, errno unix.Errno) {
+ // TODO
+ return nil, unix.EOPNOTSUPP
+}
+
+func (n *Node) listXAttr() (out []string, errno unix.Errno) {
+ // TODO
+ return nil, unix.EOPNOTSUPP
+}
diff --git a/internal/fusefrontend_reverse/root_node.go b/internal/fusefrontend_reverse/root_node.go
index 420ed22..461b25c 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"
@@ -35,6 +36,11 @@ type RootNode struct {
contentEnc *contentenc.ContentEnc
// Tests whether a path is excluded (hidden) from the user. Used by -exclude.
excluder ignore.IgnoreParser
+ // configPlainPath is the path of a custom config file (-config), relative to
+ // Cipherdir, set only when that file is located inside Cipherdir. Such a file
+ // is hidden from the encrypted view, otherwise it would leak in encrypted
+ // form (https://github.com/rfjakob/gocryptfs/issues/1009).
+ configPlainPath string
// inoMap translates inode numbers from different devices to unique inode
// numbers.
inoMap *inomap.InoMap
@@ -62,7 +68,7 @@ func NewRootNode(args fusefrontend.Args, c *contentenc.ContentEnc, n *nametransf
var rootDev uint64
var st syscall.Stat_t
var statErr error
- var shortNameMax int
+ var shortNameMax = syscall.NAME_MAX
if statErr = syscall.Stat(args.Cipherdir, &st); statErr != nil {
tlog.Warn.Printf("Could not stat backing directory %q: %v", args.Cipherdir, statErr)
if args.OneFileSystem {
@@ -73,8 +79,10 @@ func NewRootNode(args fusefrontend.Args, c *contentenc.ContentEnc, n *nametransf
rootDev = uint64(st.Dev)
}
- shortNameMax = n.GetLongNameMax() * 3 / 4
- shortNameMax = shortNameMax - shortNameMax%16 - 1
+ if !args.PlaintextNames {
+ shortNameMax = n.GetLongNameMax() * 3 / 4
+ shortNameMax = shortNameMax - shortNameMax%16 - 1
+ }
rn := &RootNode{
args: args,
@@ -91,6 +99,14 @@ func NewRootNode(args fusefrontend.Args, c *contentenc.ContentEnc, n *nametransf
if len(args.Exclude) > 0 || len(args.ExcludeWildcard) > 0 || len(args.ExcludeFrom) > 0 {
rn.excluder = prepareExcluder(args)
}
+ // A custom config file (-config) located inside Cipherdir must be hidden from
+ // the encrypted view, otherwise it leaks there in encrypted form (#1009).
+ if args.ConfigCustom && args.Config != "" {
+ if rel, err := filepath.Rel(args.Cipherdir, args.Config); err == nil &&
+ rel != "." && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
+ rn.configPlainPath = rel
+ }
+ }
return rn
}
@@ -136,9 +152,15 @@ 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
}
+ // A custom config file (-config) inside Cipherdir is hidden from the
+ // encrypted view (https://github.com/rfjakob/gocryptfs/issues/1009).
+ if rn.configPlainPath != "" && pPath == rn.configPlainPath {
+ return true
+ }
return rn.excluder != nil && rn.excluder.MatchesPath(pPath)
}
@@ -146,7 +168,7 @@ func (rn *RootNode) isExcludedPlain(pPath string) bool {
// pDir is the relative plaintext path to the directory these entries are
// from. The entries should be plaintext files.
func (rn *RootNode) excludeDirEntries(d *dirfdPlus, entries []fuse.DirEntry) (filtered []fuse.DirEntry) {
- if rn.excluder == nil {
+ if rn.excluder == nil && rn.configPlainPath == "" {
return entries
}
filtered = make([]fuse.DirEntry, 0, len(entries))
diff --git a/internal/nametransform/diriv.go b/internal/nametransform/diriv.go
index 5dd4940..aaa65ee 100644
--- a/internal/nametransform/diriv.go
+++ b/internal/nametransform/diriv.go
@@ -83,16 +83,22 @@ func WriteDirIVAt(dirfd int) error {
if !syscallcompat.IsENOSPC(err) {
tlog.Warn.Printf("WriteDirIV: Write: %v", err)
}
- // Delete incomplete gocryptfs.diriv file
- syscallcompat.Unlinkat(dirfd, DirIVFilename, 0)
- return err
+ goto delete
+ }
+ err = f.Sync()
+ if err != nil {
+ tlog.Warn.Printf("WriteDirIV: Sync: %v", err)
+ goto delete
}
err = f.Close()
if err != nil {
tlog.Warn.Printf("WriteDirIV: Close: %v", err)
- // Delete incomplete gocryptfs.diriv file
- syscallcompat.Unlinkat(dirfd, DirIVFilename, 0)
- return err
+ goto delete
}
return nil
+
+delete:
+ // Delete potentially incomplete gocryptfs.diriv file
+ syscallcompat.Unlinkat(dirfd, DirIVFilename, 0)
+ return err
}
diff --git a/internal/siv_aead/benchmark.bash b/internal/siv_aead/benchmark.bash
index 40b57b3..400c134 100755
--- a/internal/siv_aead/benchmark.bash
+++ b/internal/siv_aead/benchmark.bash
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
set -eu
diff --git a/internal/speed/benchmark.bash b/internal/speed/benchmark.bash
index d2678a7..699ceb8 100755
--- a/internal/speed/benchmark.bash
+++ b/internal/speed/benchmark.bash
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
set -eu
diff --git a/internal/stupidgcm/benchmark.bash b/internal/stupidgcm/benchmark.bash
index 8681495..8319659 100755
--- a/internal/stupidgcm/benchmark.bash
+++ b/internal/stupidgcm/benchmark.bash
@@ -1,3 +1,3 @@
-#!/bin/bash
+#!/usr/bin/env bash
exec ../speed/benchmark.bash
diff --git a/internal/syscallcompat/asuser_freebsd.go b/internal/syscallcompat/asuser_freebsd.go
new file mode 100644
index 0000000..dfa8e18
--- /dev/null
+++ b/internal/syscallcompat/asuser_freebsd.go
@@ -0,0 +1,24 @@
+package syscallcompat
+
+import (
+ "golang.org/x/sys/unix"
+
+ "github.com/hanwen/go-fuse/v2/fuse"
+
+ "github.com/rfjakob/gocryptfs/v2/internal/tlog"
+)
+
+// asUser runs `f()` under the effective uid, gid, groups specified
+// in `context`.
+//
+// If `context` is nil, `f()` is executed directly without switching user id.
+//
+// FreeBSD does not support changing uid/gid per thread. If context is not nil,
+// an error is returned.
+func asUser(f func() (int, error), context *fuse.Context) (int, error) {
+ if context == nil {
+ return f()
+ }
+ tlog.Warn.Printf("asUser: error, only nil context is supported\n")
+ return 0, unix.EOPNOTSUPP
+}
diff --git a/internal/syscallcompat/emulate.go b/internal/syscallcompat/emulate.go
index 91b592b..435c579 100644
--- a/internal/syscallcompat/emulate.go
+++ b/internal/syscallcompat/emulate.go
@@ -1,3 +1,5 @@
+//go:build !freebsd
+
package syscallcompat
import (
diff --git a/internal/syscallcompat/emulate_test.go b/internal/syscallcompat/emulate_test.go
index 16383f2..907ba3a 100644
--- a/internal/syscallcompat/emulate_test.go
+++ b/internal/syscallcompat/emulate_test.go
@@ -1,3 +1,5 @@
+//go:build !freebsd
+
package syscallcompat
import (
diff --git a/internal/syscallcompat/quirks.go b/internal/syscallcompat/quirks.go
index e30d605..36bcb9f 100644
--- a/internal/syscallcompat/quirks.go
+++ b/internal/syscallcompat/quirks.go
@@ -5,16 +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
)
-func logQuirk(s string) {
+// 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_freebsd.go b/internal/syscallcompat/quirks_freebsd.go
new file mode 100644
index 0000000..c340cea
--- /dev/null
+++ b/internal/syscallcompat/quirks_freebsd.go
@@ -0,0 +1,22 @@
+package syscallcompat
+
+import (
+ "golang.org/x/sys/unix"
+
+ "github.com/rfjakob/gocryptfs/v2/internal/tlog"
+)
+
+// DetectQuirks decides if there are known quirks on the backing filesystem
+// that need to be workarounded.
+//
+// Tested by tests/root_test.TestBtrfsQuirks
+func DetectQuirks(cipherdir string) (q uint64) {
+ var st unix.Statfs_t
+ err := unix.Statfs(cipherdir, &st)
+ if err != nil {
+ tlog.Warn.Printf("DetectQuirks: Statfs on %q failed: %v", cipherdir, err)
+ return 0
+ }
+
+ return q
+}
diff --git a/internal/syscallcompat/quirks_linux.go b/internal/syscallcompat/quirks_linux.go
index bcdcf07..35f754d 100644
--- a/internal/syscallcompat/quirks_linux.go
+++ b/internal/syscallcompat/quirks_linux.go
@@ -1,11 +1,38 @@
package syscallcompat
import (
+ "syscall"
+
"golang.org/x/sys/unix"
"github.com/rfjakob/gocryptfs/v2/internal/tlog"
)
+// FS_NOCOW_FL is the flag set by "chattr +C" to disable copy-on-write on
+// btrfs. Not exported by golang.org/x/sys/unix, value from linux/fs.h.
+const FS_NOCOW_FL = 0x00800000
+
+// dirHasNoCow checks whether the directory at the given path has the
+// NOCOW (No Copy-on-Write) attribute set (i.e. "chattr +C").
+// When a directory has this attribute, files created within it inherit
+// NOCOW, which makes fallocate work correctly on btrfs because writes
+// go in-place rather than through COW.
+func dirHasNoCow(path string) bool {
+ fd, err := syscall.Open(path, syscall.O_RDONLY|syscall.O_DIRECTORY, 0)
+ if err != nil {
+ tlog.Debug.Printf("dirHasNoCow: Open %q failed: %v", path, err)
+ return false
+ }
+ defer syscall.Close(fd)
+
+ flags, err := unix.IoctlGetInt(fd, unix.FS_IOC_GETFLAGS)
+ if err != nil {
+ tlog.Debug.Printf("dirHasNoCow: FS_IOC_GETFLAGS on %q failed: %v", path, err)
+ return false
+ }
+ return flags&FS_NOCOW_FL != 0
+}
+
// DetectQuirks decides if there are known quirks on the backing filesystem
// that need to be workarounded.
//
@@ -21,10 +48,19 @@ func DetectQuirks(cipherdir string) (q uint64) {
// Preallocation on Btrfs is broken ( https://github.com/rfjakob/gocryptfs/issues/395 )
// and slow ( https://github.com/rfjakob/gocryptfs/issues/63 ).
//
+ // The root cause is that btrfs COW allocates new blocks on write even for
+ // preallocated extents, defeating the purpose of fallocate. However, if the
+ // backing directory has the NOCOW attribute (chattr +C), writes go in-place
+ // and fallocate works correctly.
+ //
// Cast to uint32 avoids compile error on arm: "constant 2435016766 overflows int32"
if uint32(st.Type) == unix.BTRFS_SUPER_MAGIC {
- logQuirk("Btrfs detected, forcing -noprealloc. See https://github.com/rfjakob/gocryptfs/issues/395 for why.")
- q |= QuirkBrokenFalloc
+ 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..4f84d98 100644
--- a/internal/syscallcompat/sys_common.go
+++ b/internal/syscallcompat/sys_common.go
@@ -54,10 +54,10 @@ func Openat(dirfd int, path string, flags int, mode uint32) (fd int, err error)
flags |= syscall.O_EXCL
}
} else {
- // If O_CREAT is not used, we should use O_NOFOLLOW
- if flags&syscall.O_NOFOLLOW == 0 {
- tlog.Warn.Printf("Openat: O_NOFOLLOW missing: flags = %#x", flags)
- flags |= syscall.O_NOFOLLOW
+ // If O_CREAT is not used, we should use O_NOFOLLOW or O_SYMLINK
+ if flags&(unix.O_NOFOLLOW|OpenatFlagNofollowSymlink) == 0 {
+ tlog.Warn.Printf("Openat: O_NOFOLLOW/O_SYMLINK missing: flags = %#x", flags)
+ flags |= unix.O_NOFOLLOW
}
}
@@ -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,10 +135,10 @@ 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 {
+ if err == ENODATA {
return nil, err
}
// Underlying fs does not support security.capabilities (example: tmpfs)
@@ -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..07eb30a 100644
--- a/internal/syscallcompat/sys_darwin.go
+++ b/internal/syscallcompat/sys_darwin.go
@@ -24,9 +24,20 @@ const (
RENAME_NOREPLACE = unix.RENAME_EXCL
RENAME_EXCHANGE = unix.RENAME_SWAP
+ ENODATA = unix.ENODATA
+
// 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
+
+ // F_OFD_SETLKW only exists on Linux. On Darwin, fall back to F_SETLKW as a
+ // flawed replacement.
+ F_OFD_SETLKW = unix.F_SETLKW
+ F_OFD_SETLK = unix.F_SETLK
)
// Unfortunately fsetattrlist does not have a syscall wrapper yet.
diff --git a/internal/syscallcompat/sys_freebsd.go b/internal/syscallcompat/sys_freebsd.go
new file mode 100644
index 0000000..0d28f3e
--- /dev/null
+++ b/internal/syscallcompat/sys_freebsd.go
@@ -0,0 +1,165 @@
+// Package syscallcompat wraps FreeBSD-specific syscalls
+package syscallcompat
+
+import (
+ "time"
+
+ "golang.org/x/sys/unix"
+
+ "github.com/hanwen/go-fuse/v2/fuse"
+
+ "github.com/rfjakob/gocryptfs/v2/internal/tlog"
+)
+
+const (
+ O_DIRECT = unix.O_DIRECT
+
+ // O_PATH is supported on FreeBSD, but is missing from the sys/unix package
+ // FreeBSD-15.0 /usr/src/sys/sys/fcntl.h:135
+ O_PATH = 0x00400000
+
+ // Only defined on Linux, but we can emulate the functionality on FreeBSD
+ // in Renameat2() below
+ RENAME_NOREPLACE = 0x1
+ RENAME_EXCHANGE = 0x2
+ RENAME_WHITEOUT = 0x4
+
+ // ENODATA is only defined on Linux, but FreeBSD provides ENOATTR
+ ENODATA = unix.ENOATTR
+
+ // On FreeBSD, we only have O_NOFOLLOW.
+ OpenatFlagNofollowSymlink = unix.O_NOFOLLOW
+
+ // For the utimensat syscall on FreeBSD
+ AT_EMPTY_PATH = 0x4000
+
+ // F_OFD_SETLKW only exists on Linux. On Darwin, fall back to F_SETLKW as a
+ // flawed replacement.
+ F_OFD_SETLKW = unix.F_SETLKW
+ F_OFD_SETLK = unix.F_SETLK
+)
+
+// EnospcPrealloc is supposed to preallocate ciphertext space without
+// changing the file size. This guarantees that we don't run out of
+// space while writing a ciphertext block (that would corrupt the block).
+//
+// The fallocate syscall isn't supported on FreeBSD with the same semantics
+// as Linux, in particular the _FALLOC_FL_KEEP_SIZE mode isn't supported.
+func EnospcPrealloc(fd int, off int64, len int64) (err error) {
+ return nil
+}
+
+// Fallocate wraps the posix_fallocate() syscall.
+// Fallocate returns an error if mode is not 0
+func Fallocate(fd int, mode uint32, off int64, len int64) (err error) {
+ if mode != 0 {
+ tlog.Warn.Printf("Fallocate: unsupported mode\n")
+ return unix.EOPNOTSUPP
+ }
+ _, _, err = unix.Syscall(unix.SYS_POSIX_FALLOCATE, uintptr(fd), uintptr(off), uintptr(len))
+ return err
+}
+
+// Mknodat wraps the Mknodat syscall.
+func Mknodat(dirfd int, path string, mode uint32, dev int) (err error) {
+ return unix.Mknodat(dirfd, path, mode, uint64(dev))
+}
+
+// Dup3 wraps the Dup3 syscall. We want to use Dup3 rather than Dup2 because Dup2
+// is not implemented on arm64.
+func Dup3(oldfd int, newfd int, flags int) (err error) {
+ return unix.Dup3(oldfd, newfd, flags)
+}
+
+// FchmodatNofollow is like Fchmodat but never follows symlinks.
+func FchmodatNofollow(dirfd int, path string, mode uint32) (err error) {
+ return unix.Fchmodat(dirfd, path, mode, unix.AT_SYMLINK_NOFOLLOW)
+}
+
+// LsetxattrUser runs the Lsetxattr syscall in the context of a different user.
+// This is useful when setting ACLs, as the result depends on the user running
+// the operation (see fuse-xfstests generic/375).
+//
+// If `context` is nil, this function behaves like ordinary Lsetxattr.
+func LsetxattrUser(path string, attr string, data []byte, flags int, context *fuse.Context) (err error) {
+ f := func() (int, error) {
+ err := unix.Lsetxattr(path, attr, data, flags)
+ return -1, err
+ }
+ _, err = asUser(f, context)
+ return err
+}
+
+func timesToTimespec(a *time.Time, m *time.Time) []unix.Timespec {
+ ts := make([]unix.Timespec, 2)
+ if a == nil {
+ ts[0] = unix.Timespec{Nsec: unix.UTIME_OMIT}
+ } else {
+ ts[0], _ = unix.TimeToTimespec(*a)
+ }
+ if m == nil {
+ ts[1] = unix.Timespec{Nsec: unix.UTIME_OMIT}
+ } else {
+ ts[1], _ = unix.TimeToTimespec(*m)
+ }
+ return ts
+}
+
+// FutimesNano syscall.
+func FutimesNano(fd int, a *time.Time, m *time.Time) (err error) {
+ ts := timesToTimespec(a, m)
+ return unix.UtimesNanoAt(unix.AT_FDCWD, "", ts, AT_EMPTY_PATH)
+}
+
+// UtimesNanoAtNofollow is like UtimesNanoAt but never follows symlinks.
+// Retries on EINTR.
+func UtimesNanoAtNofollow(dirfd int, path string, a *time.Time, m *time.Time) (err error) {
+ ts := timesToTimespec(a, m)
+ err = retryEINTR(func() error {
+ return unix.UtimesNanoAt(dirfd, path, ts, unix.AT_SYMLINK_NOFOLLOW)
+ })
+ return err
+}
+
+// Getdents syscall with "." and ".." filtered out.
+func Getdents(fd int) ([]fuse.DirEntry, error) {
+ entries, _, err := emulateGetdents(fd)
+ return entries, err
+}
+
+// GetdentsSpecial calls the Getdents syscall,
+// with normal entries and "." / ".." split into two slices.
+func GetdentsSpecial(fd int) (entries []fuse.DirEntry, entriesSpecial []fuse.DirEntry, err error) {
+ return emulateGetdents(fd)
+}
+
+// Renameat2 does not exist on FreeBSD, so we have to wrap it here.
+// Retries on EINTR.
+// The RENAME_EXCHANGE and RENAME_WHITEOUT flags are not supported.
+func Renameat2(olddirfd int, oldpath string, newdirfd int, newpath string, flags uint) (err error) {
+ if flags&(RENAME_NOREPLACE|RENAME_EXCHANGE) == RENAME_NOREPLACE|RENAME_EXCHANGE {
+ return unix.EINVAL
+ }
+ if flags&(RENAME_NOREPLACE|RENAME_EXCHANGE) == RENAME_NOREPLACE|RENAME_EXCHANGE {
+ return unix.EINVAL
+ }
+
+ if flags&RENAME_NOREPLACE != 0 {
+ var st unix.Stat_t
+ err = unix.Fstatat(newdirfd, newpath, &st, 0)
+ if err == nil {
+ // Assume newpath is an existing file if we can stat() it.
+ // On Linux, RENAME_NOREPLACE fails with EEXIST if newpath
+ // already exists.
+ return unix.EEXIST
+ }
+ }
+ if flags&RENAME_EXCHANGE != 0 {
+ return unix.EINVAL
+ }
+ if flags&RENAME_WHITEOUT != 0 {
+ return unix.EINVAL
+ }
+
+ return unix.Renameat(olddirfd, oldpath, newdirfd, newpath)
+}
diff --git a/internal/syscallcompat/sys_linux.go b/internal/syscallcompat/sys_linux.go
index 19d2c56..ffa5a97 100644
--- a/internal/syscallcompat/sys_linux.go
+++ b/internal/syscallcompat/sys_linux.go
@@ -28,6 +28,18 @@ 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
+
+ // Only defined on Linux
+ ENODATA = unix.ENODATA
+
+ // F_OFD_SETLKW only exists on Linux. On Darwin, fall back to F_SETLKW as a
+ // flawed replacement.
+ F_OFD_SETLKW = unix.F_OFD_SETLKW
+ F_OFD_SETLK = unix.F_OFD_SETLK
)
var preallocWarn sync.Once
@@ -68,6 +80,15 @@ func Mknodat(dirfd int, path string, mode uint32, dev int) (err error) {
return syscall.Mknodat(dirfd, path, mode, dev)
}
+// Statx wraps the Statx syscall.
+// Retries on EINTR.
+func Statx(dirfd int, path string, flags int, mask int, st *unix.Statx_t) (err error) {
+ err = retryEINTR(func() error {
+ return unix.Statx(dirfd, path, flags, mask, st)
+ })
+ return err
+}
+
// Dup3 wraps the Dup3 syscall. We want to use Dup3 rather than Dup2 because Dup2
// is not implemented on arm64.
func Dup3(oldfd int, newfd int, flags int) (err error) {
diff --git a/internal/syscallcompat/unix2syscall_darwin.go b/internal/syscallcompat/unix2syscall.go
index 5767a27..fa2e8c4 100644
--- a/internal/syscallcompat/unix2syscall_darwin.go
+++ b/internal/syscallcompat/unix2syscall.go
@@ -1,3 +1,5 @@
+//go:build darwin || freebsd
+
package syscallcompat
import (