diff options
Diffstat (limited to 'internal/fusefrontend')
| -rw-r--r-- | internal/fusefrontend/args.go | 4 | ||||
| -rw-r--r-- | internal/fusefrontend/file.go | 49 | ||||
| -rw-r--r-- | internal/fusefrontend/file_allocate_truncate.go | 62 | ||||
| -rw-r--r-- | internal/fusefrontend/file_lock.go | 51 | ||||
| -rw-r--r-- | internal/fusefrontend/node.go | 27 | ||||
| -rw-r--r-- | internal/fusefrontend/node_helpers.go | 18 | ||||
| -rw-r--r-- | internal/fusefrontend/node_prepare_syscall.go | 4 | ||||
| -rw-r--r-- | internal/fusefrontend/statx_linux.go | 78 |
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 +} |
