diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/fusefrontend/file.go | 46 | ||||
| -rw-r--r-- | internal/fusefrontend/file_allocate_truncate.go | 60 | ||||
| -rw-r--r-- | internal/fusefrontend/file_lock.go | 51 | ||||
| -rw-r--r-- | internal/syscallcompat/sys_darwin.go | 5 | ||||
| -rw-r--r-- | internal/syscallcompat/sys_freebsd.go | 5 | ||||
| -rw-r--r-- | internal/syscallcompat/sys_linux.go | 5 |
6 files changed, 162 insertions, 10 deletions
diff --git a/internal/fusefrontend/file.go b/internal/fusefrontend/file.go index 64c6ca0..9195f00 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 diff --git a/internal/fusefrontend/file_allocate_truncate.go b/internal/fusefrontend/file_allocate_truncate.go index a3decf9..bfd11e1 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,60 @@ 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. 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/syscallcompat/sys_darwin.go b/internal/syscallcompat/sys_darwin.go index 52d4e0f..07eb30a 100644 --- a/internal/syscallcompat/sys_darwin.go +++ b/internal/syscallcompat/sys_darwin.go @@ -33,6 +33,11 @@ const ( // 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 index d88b1cf..0d28f3e 100644 --- a/internal/syscallcompat/sys_freebsd.go +++ b/internal/syscallcompat/sys_freebsd.go @@ -32,6 +32,11 @@ const ( // 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 diff --git a/internal/syscallcompat/sys_linux.go b/internal/syscallcompat/sys_linux.go index a850ba1..2b8a6f7 100644 --- a/internal/syscallcompat/sys_linux.go +++ b/internal/syscallcompat/sys_linux.go @@ -35,6 +35,11 @@ const ( // 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 |
