diff options
Diffstat (limited to 'internal')
28 files changed, 600 insertions, 79 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 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/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/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_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_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/sys_common.go b/internal/syscallcompat/sys_common.go index 3cb9ffa..4f84d98 100644 --- a/internal/syscallcompat/sys_common.go +++ b/internal/syscallcompat/sys_common.go @@ -138,7 +138,7 @@ func getxattrSmartBuf(fn func(buf []byte) (int, error)) ([]byte, error) { 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) diff --git a/internal/syscallcompat/sys_darwin.go b/internal/syscallcompat/sys_darwin.go index ef19f24..07eb30a 100644 --- a/internal/syscallcompat/sys_darwin.go +++ b/internal/syscallcompat/sys_darwin.go @@ -24,6 +24,8 @@ 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 @@ -31,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 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 71478af..ffa5a97 100644 --- a/internal/syscallcompat/sys_linux.go +++ b/internal/syscallcompat/sys_linux.go @@ -32,6 +32,14 @@ const ( // 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 @@ -72,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 ( |
