aboutsummaryrefslogtreecommitdiff
path: root/internal/fusefrontend
diff options
context:
space:
mode:
Diffstat (limited to 'internal/fusefrontend')
-rw-r--r--internal/fusefrontend/args.go4
-rw-r--r--internal/fusefrontend/file.go49
-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_prepare_syscall.go4
-rw-r--r--internal/fusefrontend/statx_linux.go78
8 files changed, 248 insertions, 45 deletions
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 64c6ca0..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 {
@@ -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,7 +353,6 @@ 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
@@ -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_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/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
+}