diff options
Diffstat (limited to 'internal/fusefrontend')
20 files changed, 610 insertions, 238 deletions
diff --git a/internal/fusefrontend/args.go b/internal/fusefrontend/args.go index 64a5923..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 @@ -51,4 +55,6 @@ type Args struct { OneFileSystem bool // DeterministicNames disables gocryptfs.diriv files DeterministicNames bool + // NoXattr disables extended attribute operations + NoXattr bool } diff --git a/internal/fusefrontend/file.go b/internal/fusefrontend/file.go index 0e25de3..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" @@ -37,8 +39,6 @@ type File struct { // Every FUSE entrypoint should RLock(). The only user of Lock() is // Release(), which closes the fd and sets "released" to true. fdLock sync.RWMutex - // Content encryption helper - contentEnc *contentenc.ContentEnc // Device and inode number uniquely identify the backing file qIno inomap.QIno // Entry in the open file table @@ -50,6 +50,8 @@ type File struct { lastOpCount uint64 // Parent filesystem rootNode *RootNode + // If this open file is a directory, dirHandle will be set, otherwise it's nil. + dirHandle *DirHandle } // NewFile returns a new go-fuse File instance based on an already-open file @@ -71,7 +73,6 @@ func NewFile(fd int, cName string, rn *RootNode) (f *File, st *syscall.Stat_t, e f = &File{ fd: osFile, - contentEnc: rn.contentEnc, qIno: qi, fileTableEntry: e, rootNode: rn, @@ -91,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 { @@ -116,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) { @@ -175,7 +183,7 @@ func (f *File) doRead(dst []byte, off uint64, length uint64) ([]byte, syscall.Er log.Panicf("fileID=%v", fileID) } // Read the backing ciphertext in one go - blocks := f.contentEnc.ExplodePlainRange(off, length) + blocks := f.rootNode.contentEnc.ExplodePlainRange(off, length) alignedOffset, alignedLength := blocks[0].JointCiphertextRange(blocks) // f.fd.ReadAt takes an int64! if alignedOffset > math.MaxInt64 { @@ -204,10 +212,10 @@ func (f *File) doRead(dst []byte, off uint64, length uint64) ([]byte, syscall.Er tlog.Debug.Printf("ReadAt offset=%d bytes (%d blocks), want=%d, got=%d", alignedOffset, firstBlockNo, alignedLength, n) // Decrypt it - plaintext, err := f.contentEnc.DecryptBlocks(ciphertext, firstBlockNo, fileID) + plaintext, err := f.rootNode.contentEnc.DecryptBlocks(ciphertext, firstBlockNo, fileID) f.rootNode.contentEnc.CReqPool.Put(ciphertext) if err != nil { - corruptBlockNo := firstBlockNo + f.contentEnc.PlainOffToBlockNo(uint64(len(plaintext))) + corruptBlockNo := firstBlockNo + f.rootNode.contentEnc.PlainOffToBlockNo(uint64(len(plaintext))) tlog.Warn.Printf("doRead %d: corrupt block #%d: %v", f.qIno.Ino, corruptBlockNo, err) return nil, syscall.EIO } @@ -245,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 @@ -267,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,20 +315,32 @@ func (f *File) doWrite(data []byte, off int64) (uint32, syscall.Errno) { } // Handle payload data dataBuf := bytes.NewBuffer(data) - blocks := f.contentEnc.ExplodePlainRange(uint64(off), uint64(len(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 if b.IsPartial() { // Read - oldData, errno := f.doRead(nil, b.BlockPlainOff(), f.contentEnc.PlainBS()) + oldData, errno := f.doRead(nil, b.BlockPlainOff(), f.rootNode.contentEnc.PlainBS()) if errno != 0 { tlog.Warn.Printf("ino%d fh%d: RMW read failed: errno=%d", f.qIno.Ino, f.intFd(), errno) return 0, errno } // Modify - blockData = f.contentEnc.MergeBlocks(oldData, blockData, int(b.Skip)) + blockData = f.rootNode.contentEnc.MergeBlocks(oldData, blockData, int(b.Skip)) tlog.Debug.Printf("len(oldData)=%d len(blockData)=%d", len(oldData), len(blockData)) } tlog.Debug.Printf("ino%d: Writing %d bytes to block #%d", @@ -307,16 +349,15 @@ func (f *File) doWrite(data []byte, off int64) (uint32, syscall.Errno) { toEncrypt[i] = blockData } // Encrypt all blocks - ciphertext := f.contentEnc.EncryptBlocks(toEncrypt, blocks[0].BlockNo, f.fileTableEntry.ID) + ciphertext := f.rootNode.contentEnc.EncryptBlocks(toEncrypt, blocks[0].BlockNo, f.fileTableEntry.ID) // 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) { @@ -414,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() @@ -437,7 +475,10 @@ func (f *File) Getattr(ctx context.Context, a *fuse.AttrOut) syscall.Errno { } f.rootNode.inoMap.TranslateStat(&st) a.FromStat(&st) - a.Size = f.contentEnc.CipherSizeToPlainSize(a.Size) + if a.IsRegular() { + a.Size = f.rootNode.contentEnc.CipherSizeToPlainSize(a.Size) + } + // TODO: Handle symlink size similar to node.translateSize() if f.rootNode.args.ForceOwner != nil { a.Owner = *f.rootNode.args.ForceOwner } diff --git a/internal/fusefrontend/file_allocate_truncate.go b/internal/fusefrontend/file_allocate_truncate.go index cae796e..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" @@ -54,7 +58,7 @@ func (f *File) Allocate(ctx context.Context, off uint64, sz uint64, mode uint32) f.fileTableEntry.ContentLock.Lock() defer f.fileTableEntry.ContentLock.Unlock() - blocks := f.contentEnc.ExplodePlainRange(off, sz) + blocks := f.rootNode.contentEnc.ExplodePlainRange(off, sz) firstBlock := blocks[0] lastBlock := blocks[len(blocks)-1] @@ -63,7 +67,7 @@ func (f *File) Allocate(ctx context.Context, off uint64, sz uint64, mode uint32) // the file. cipherOff := firstBlock.BlockCipherOff() cipherSz := lastBlock.BlockCipherOff() - cipherOff + - f.contentEnc.BlockOverhead() + lastBlock.Skip + lastBlock.Length + f.rootNode.contentEnc.BlockOverhead() + lastBlock.Skip + lastBlock.Length err := syscallcompat.Fallocate(f.intFd(), FALLOC_FL_KEEP_SIZE, int64(cipherOff), int64(cipherSz)) tlog.Debug.Printf("Allocate off=%d sz=%d mode=%x cipherOff=%d cipherSz=%d\n", off, sz, mode, cipherOff, cipherSz) @@ -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() @@ -113,8 +159,8 @@ func (f *File) truncate(newSize uint64) (errno syscall.Errno) { return fs.ToErrno(err) } - oldB := float32(oldSize) / float32(f.contentEnc.PlainBS()) - newB := float32(newSize) / float32(f.contentEnc.PlainBS()) + oldB := float32(oldSize) / float32(f.rootNode.contentEnc.PlainBS()) + newB := float32(newSize) / float32(f.rootNode.contentEnc.PlainBS()) tlog.Debug.Printf("ino%d: FUSE Truncate from %.2f to %.2f blocks (%d to %d bytes)", f.qIno.Ino, oldB, newB, oldSize, newSize) // File size stays the same - nothing to do @@ -127,9 +173,9 @@ func (f *File) truncate(newSize uint64) (errno syscall.Errno) { } // File shrinks - blockNo := f.contentEnc.PlainOffToBlockNo(newSize) - cipherOff := f.contentEnc.BlockNoToCipherOff(blockNo) - plainOff := f.contentEnc.BlockNoToPlainOff(blockNo) + blockNo := f.rootNode.contentEnc.PlainOffToBlockNo(newSize) + cipherOff := f.rootNode.contentEnc.BlockNoToCipherOff(blockNo) + plainOff := f.rootNode.contentEnc.BlockNoToPlainOff(blockNo) lastBlockLen := newSize - plainOff var data []byte if lastBlockLen > 0 { @@ -161,7 +207,7 @@ func (f *File) statPlainSize() (uint64, error) { return 0, err } cipherSz := uint64(fi.Size()) - plainSz := uint64(f.contentEnc.CipherSizeToPlainSize(cipherSz)) + plainSz := uint64(f.rootNode.contentEnc.CipherSizeToPlainSize(cipherSz)) return plainSz, nil } @@ -174,8 +220,8 @@ func (f *File) truncateGrowFile(oldPlainSz uint64, newPlainSz uint64) syscall.Er } newEOFOffset := newPlainSz - 1 if oldPlainSz > 0 { - n1 := f.contentEnc.PlainOffToBlockNo(oldPlainSz - 1) - n2 := f.contentEnc.PlainOffToBlockNo(newEOFOffset) + n1 := f.rootNode.contentEnc.PlainOffToBlockNo(oldPlainSz - 1) + n2 := f.rootNode.contentEnc.PlainOffToBlockNo(newEOFOffset) // The file is grown within one block, no need to pad anything. // Write a single zero to the last byte and let doWrite figure out the RMW. if n1 == n2 { @@ -194,7 +240,7 @@ func (f *File) truncateGrowFile(oldPlainSz uint64, newPlainSz uint64) syscall.Er } // The new size is block-aligned. In this case we can do everything ourselves // and avoid the call to doWrite. - if newPlainSz%f.contentEnc.PlainBS() == 0 { + if newPlainSz%f.rootNode.contentEnc.PlainBS() == 0 { // The file was empty, so it did not have a header. Create one. if oldPlainSz == 0 { id, err := f.createHeader() @@ -203,7 +249,7 @@ func (f *File) truncateGrowFile(oldPlainSz uint64, newPlainSz uint64) syscall.Er } f.fileTableEntry.ID = id } - cSz := int64(f.contentEnc.PlainSizeToCipherSize(newPlainSz)) + cSz := int64(f.rootNode.contentEnc.PlainSizeToCipherSize(newPlainSz)) err := syscall.Ftruncate(f.intFd(), cSz) if err != nil { tlog.Warn.Printf("Truncate: grow Ftruncate returned error: %v", err) diff --git a/internal/fusefrontend/file_dir_ops.go b/internal/fusefrontend/file_dir_ops.go new file mode 100644 index 0000000..b69e7bc --- /dev/null +++ b/internal/fusefrontend/file_dir_ops.go @@ -0,0 +1,177 @@ +package fusefrontend + +import ( + "context" + "syscall" + + "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/nametransform" + "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" + "github.com/rfjakob/gocryptfs/v2/internal/tlog" +) + +func (n *Node) OpendirHandle(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { + var fd int = -1 + var fdDup int = -1 + var file *File + var dirIV []byte + var ds fs.DirStream + rn := n.rootNode() + + dirfd, cName, errno := n.prepareAtSyscallMyself() + if errno != 0 { + return + } + defer syscall.Close(dirfd) + + // Open backing directory + fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0) + if err != nil { + errno = fs.ToErrno(err) + return + } + + // NewLoopbackDirStreamFd gets its own fd to untangle Release vs Releasedir + fdDup, err = syscall.Dup(fd) + + if err != nil { + errno = fs.ToErrno(err) + goto err_out + } + + ds, errno = fs.NewLoopbackDirStreamFd(fdDup) + if errno != 0 { + goto err_out + } + + if !rn.args.PlaintextNames { + // Read the DirIV from disk + dirIV, err = rn.nameTransform.ReadDirIVAt(fd) + if err != nil { + tlog.Warn.Printf("OpendirHandle: could not read %s: %v", nametransform.DirIVFilename, err) + errno = syscall.EIO + goto err_out + } + } + + file, _, errno = NewFile(fd, cName, rn) + if errno != 0 { + goto err_out + } + + file.dirHandle = &DirHandle{ + ds: ds, + dirIV: dirIV, + isRootDir: n.IsRoot(), + } + + return file, fuseFlags, errno + +err_out: + if fd >= 0 { + syscall.Close(fd) + } + if fdDup >= 0 { + syscall.Close(fdDup) + } + if errno == 0 { + tlog.Warn.Printf("BUG: OpendirHandle: err_out called with errno == 0") + errno = syscall.EIO + } + return nil, 0, errno +} + +type DirHandle struct { + // Content of gocryptfs.diriv. nil if plaintextnames is used. + dirIV []byte + + isRootDir bool + + // fs.loopbackDirStream with a private dup of the file descriptor + ds fs.FileHandle +} + +var _ = (fs.FileReleasedirer)((*File)(nil)) + +func (f *File) Releasedir(ctx context.Context, flags uint32) { + // Does its own locking + f.dirHandle.ds.(fs.FileReleasedirer).Releasedir(ctx, flags) + // Does its own locking + f.Release(ctx) +} + +var _ = (fs.FileSeekdirer)((*File)(nil)) + +func (f *File) Seekdir(ctx context.Context, off uint64) syscall.Errno { + return f.dirHandle.ds.(fs.FileSeekdirer).Seekdir(ctx, off) +} + +var _ = (fs.FileFsyncdirer)((*File)(nil)) + +func (f *File) Fsyncdir(ctx context.Context, flags uint32) syscall.Errno { + return f.dirHandle.ds.(fs.FileFsyncdirer).Fsyncdir(ctx, flags) +} + +var _ = (fs.FileReaddirenter)((*File)(nil)) + +// This function is symlink-safe through use of openBackingDir() and +// ReadDirIVAt(). +func (f *File) Readdirent(ctx context.Context) (entry *fuse.DirEntry, errno syscall.Errno) { + f.fdLock.RLock() + defer f.fdLock.RUnlock() + + for { + entry, errno = f.dirHandle.ds.(fs.FileReaddirenter).Readdirent(ctx) + if errno != 0 || entry == nil { + return + } + + cName := entry.Name + if cName == "." || cName == ".." { + // We want these as-is + return + } + if f.dirHandle.isRootDir && cName == configfile.ConfDefaultName { + // silently ignore "gocryptfs.conf" in the top level dir + continue + } + if f.rootNode.args.PlaintextNames { + return + } + if !f.rootNode.args.DeterministicNames && cName == nametransform.DirIVFilename { + // silently ignore "gocryptfs.diriv" everywhere if dirIV is enabled + continue + } + // Handle long file name + isLong := nametransform.LongNameNone + if f.rootNode.args.LongNames { + isLong = nametransform.NameType(cName) + } + if isLong == nametransform.LongNameContent { + cNameLong, err := nametransform.ReadLongNameAt(f.intFd(), cName) + if err != nil { + tlog.Warn.Printf("Readdirent: incomplete entry %q: Could not read .name: %v", + cName, err) + f.rootNode.reportMitigatedCorruption(cName) + continue + } + cName = cNameLong + } else if isLong == nametransform.LongNameFilename { + // ignore "gocryptfs.longname.*.name" + continue + } + name, err := f.rootNode.nameTransform.DecryptName(cName, f.dirHandle.dirIV) + if err != nil { + tlog.Warn.Printf("Readdirent: could not decrypt entry %q: %v", + cName, err) + f.rootNode.reportMitigatedCorruption(cName) + continue + } + // Override the ciphertext name with the plaintext name but reuse the rest + // of the structure + entry.Name = name + return + } +} diff --git a/internal/fusefrontend/file_holes.go b/internal/fusefrontend/file_holes.go index f35fa70..fc58898 100644 --- a/internal/fusefrontend/file_holes.go +++ b/internal/fusefrontend/file_holes.go @@ -21,12 +21,12 @@ func (f *File) writePadHole(targetOff int64) syscall.Errno { tlog.Warn.Printf("checkAndPadHole: Fstat failed: %v", err) return fs.ToErrno(err) } - plainSize := f.contentEnc.CipherSizeToPlainSize(uint64(fi.Size())) + plainSize := f.rootNode.contentEnc.CipherSizeToPlainSize(uint64(fi.Size())) // Appending a single byte to the file (equivalent to writing to // offset=plainSize) would write to "nextBlock". - nextBlock := f.contentEnc.PlainOffToBlockNo(plainSize) + nextBlock := f.rootNode.contentEnc.PlainOffToBlockNo(plainSize) // targetBlock is the block the user wants to write to. - targetBlock := f.contentEnc.PlainOffToBlockNo(uint64(targetOff)) + targetBlock := f.rootNode.contentEnc.PlainOffToBlockNo(uint64(targetOff)) // The write goes into an existing block or (if the last block was full) // starts a new one directly after the last block. Nothing to do. if targetBlock <= nextBlock { @@ -45,12 +45,12 @@ func (f *File) writePadHole(targetOff int64) syscall.Errno { // Zero-pad the file of size plainSize to the next block boundary. This is a no-op // if the file is already block-aligned. func (f *File) zeroPad(plainSize uint64) syscall.Errno { - lastBlockLen := plainSize % f.contentEnc.PlainBS() + lastBlockLen := plainSize % f.rootNode.contentEnc.PlainBS() if lastBlockLen == 0 { // Already block-aligned return 0 } - missing := f.contentEnc.PlainBS() - lastBlockLen + missing := f.rootNode.contentEnc.PlainBS() - lastBlockLen pad := make([]byte, missing) tlog.Debug.Printf("zeroPad: Writing %d bytes\n", missing) _, errno := f.doWrite(pad, int64(plainSize)) 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 687a386..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 { @@ -71,28 +71,54 @@ func (n *Node) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (ch func (n *Node) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) (errno syscall.Errno) { // If the kernel gives us a file handle, use it. if f != nil { - return f.(fs.FileGetattrer).Getattr(ctx, out) + if fga, ok := f.(fs.FileGetattrer); ok { + return fga.Getattr(ctx, out) + } } + rn := n.rootNode() + var st *syscall.Stat_t + var err error dirfd, cName, errno := n.prepareAtSyscallMyself() + // Hack for deleted fifos. As OPEN on a fifo does not reach + // the filesystem, we have no fd to access it. To make "cat" and git's + // t9300-fast-import.sh happy, we fake it as best as we can. + // https://github.com/rfjakob/gocryptfs/issues/929 + if errno == syscall.ENOENT && n.StableAttr().Mode == syscall.S_IFIFO { + out.Mode = syscall.S_IFIFO + out.Ino = n.StableAttr().Ino + // cat looks at this to determine the optimal io size. Seems to be always 4096 for fifos. + out.Blksize = 4096 + // We don't know what the owner was. Set it to nobody which seems safer + // than leaving it at 0 (root). + out.Owner.Gid = 65534 + out.Owner.Uid = 65534 + // All the other fields stay 0. This is what cat sees (strace -v log): + // + // fstat(1, {st_dev=makedev(0, 0x4d), st_ino=3838227, st_mode=S_IFIFO|000, st_nlink=0, + // st_uid=65534, st_gid=65534, st_blksize=4096, st_blocks=0, st_size=0, st_atime=0, + // st_atime_nsec=0, st_mtime=0, st_mtime_nsec=0, st_ctime=0, st_ctime_nsec=0}) = 0 + goto out + } if errno != 0 { return } + defer syscall.Close(dirfd) - st, err := syscallcompat.Fstatat2(dirfd, cName, unix.AT_SYMLINK_NOFOLLOW) + st, err = syscallcompat.Fstatat2(dirfd, cName, unix.AT_SYMLINK_NOFOLLOW) if err != nil { return fs.ToErrno(err) } // Fix inode number - rn := n.rootNode() rn.inoMap.TranslateStat(st) 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 { out.Owner = *rn.args.ForceOwner } @@ -220,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 @@ -345,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 } @@ -488,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_api_check.go b/internal/fusefrontend/node_api_check.go index 0f60c74..37d4293 100644 --- a/internal/fusefrontend/node_api_check.go +++ b/internal/fusefrontend/node_api_check.go @@ -7,7 +7,6 @@ import ( // Check that we have implemented the fs.Node* interfaces var _ = (fs.NodeGetattrer)((*Node)(nil)) var _ = (fs.NodeLookuper)((*Node)(nil)) -var _ = (fs.NodeReaddirer)((*Node)(nil)) var _ = (fs.NodeCreater)((*Node)(nil)) var _ = (fs.NodeMkdirer)((*Node)(nil)) var _ = (fs.NodeRmdirer)((*Node)(nil)) diff --git a/internal/fusefrontend/node_dir_ops.go b/internal/fusefrontend/node_dir_ops.go index 97e4caa..97327ce 100644 --- a/internal/fusefrontend/node_dir_ops.go +++ b/internal/fusefrontend/node_dir_ops.go @@ -12,7 +12,6 @@ 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/cryptocore" "github.com/rfjakob/gocryptfs/v2/internal/nametransform" "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" @@ -137,12 +136,6 @@ func (n *Node) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.En } defer syscall.Close(fd) - err = syscall.Fstat(fd, &st) - if err != nil { - tlog.Warn.Printf("Mkdir %q: Fstat failed: %v", cName, err) - return nil, fs.ToErrno(err) - } - // Fix permissions if origMode != mode { // Preserve SGID bit if it was set due to inheritance. @@ -151,97 +144,17 @@ func (n *Node) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.En if err != nil { tlog.Warn.Printf("Mkdir %q: Fchmod %#o -> %#o failed: %v", cName, mode, origMode, err) } - } - // Create child node & return - ch := n.newChild(ctx, &st, out) - return ch, 0 -} - -// Readdir - FUSE call. -// -// This function is symlink-safe through use of openBackingDir() and -// ReadDirIVAt(). -func (n *Node) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { - parentDirFd, cDirName, errno := n.prepareAtSyscallMyself() - if errno != 0 { - return nil, errno - } - defer syscall.Close(parentDirFd) - - // Read ciphertext directory - fd, err := syscallcompat.Openat(parentDirFd, cDirName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0) - if err != nil { - return nil, fs.ToErrno(err) - } - defer syscall.Close(fd) - cipherEntries, specialEntries, err := syscallcompat.GetdentsSpecial(fd) + err = syscall.Fstat(fd, &st) if err != nil { + tlog.Warn.Printf("Mkdir %q: Fstat failed: %v", cName, err) return nil, fs.ToErrno(err) } - // Get DirIV (stays nil if PlaintextNames is used) - var cachedIV []byte - rn := n.rootNode() - if !rn.args.PlaintextNames { - // Read the DirIV from disk - cachedIV, err = rn.nameTransform.ReadDirIVAt(fd) - if err != nil { - tlog.Warn.Printf("OpenDir %q: could not read %s: %v", cDirName, nametransform.DirIVFilename, err) - return nil, syscall.EIO - } - } - // Decrypted directory entries - var plain []fuse.DirEntry - // Add "." and ".." - plain = append(plain, specialEntries...) - // Filter and decrypt filenames - for i := range cipherEntries { - cName := cipherEntries[i].Name - if n.IsRoot() && cName == configfile.ConfDefaultName { - // silently ignore "gocryptfs.conf" in the top level dir - continue - } - if rn.args.PlaintextNames { - plain = append(plain, cipherEntries[i]) - continue - } - if !rn.args.DeterministicNames && cName == nametransform.DirIVFilename { - // silently ignore "gocryptfs.diriv" everywhere if dirIV is enabled - continue - } - // Handle long file name - isLong := nametransform.LongNameNone - if rn.args.LongNames { - isLong = nametransform.NameType(cName) - } - if isLong == nametransform.LongNameContent { - cNameLong, err := nametransform.ReadLongNameAt(fd, cName) - if err != nil { - tlog.Warn.Printf("OpenDir %q: invalid entry %q: Could not read .name: %v", - cDirName, cName, err) - rn.reportMitigatedCorruption(cName) - continue - } - cName = cNameLong - } else if isLong == nametransform.LongNameFilename { - // ignore "gocryptfs.longname.*.name" - continue - } - name, err := rn.nameTransform.DecryptName(cName, cachedIV) - if err != nil { - tlog.Warn.Printf("OpenDir %q: invalid entry %q: %v", - cDirName, cName, err) - rn.reportMitigatedCorruption(cName) - continue - } - // Override the ciphertext name with the plaintext name but reuse the rest - // of the structure - cipherEntries[i].Name = name - plain = append(plain, cipherEntries[i]) - } - return fs.NewListDirStream(plain), 0 + // Create child node & return + ch := n.newChild(ctx, &st, out) + return ch, 0 } // Rmdir - FUSE call. diff --git a/internal/fusefrontend/node_helpers.go b/internal/fusefrontend/node_helpers.go index f5dfeb6..3102275 100644 --- a/internal/fusefrontend/node_helpers.go +++ b/internal/fusefrontend/node_helpers.go @@ -2,7 +2,6 @@ package fusefrontend import ( "context" - "sync/atomic" "syscall" "github.com/hanwen/go-fuse/v2/fs" @@ -54,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 @@ -91,12 +90,12 @@ func (n *Node) newChild(ctx context.Context, st *syscall.Stat_t, out *fuse.Entry if rn.args.SharedStorage || rn.quirks&syscallcompat.QuirkDuplicateIno1 != 0 { // Make each directory entry a unique node by using a unique generation // value - see the comment at RootNode.gen for details. - gen = atomic.AddUint64(&rn.gen, 1) + gen = rn.gen.Add(1) } // Create child node id := fs.StableAttr{ - Mode: uint32(st.Mode), + Mode: uint32(st.Mode), // go-fuse masks this with syscall.S_IFMT Gen: gen, Ino: st.Ino, } 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 2a4d6ab..03194df 100644 --- a/internal/fusefrontend/node_prepare_syscall.go +++ b/internal/fusefrontend/node_prepare_syscall.go @@ -1,9 +1,9 @@ package fusefrontend import ( - "sync/atomic" "syscall" + "github.com/rfjakob/gocryptfs/v2/internal/nametransform" "github.com/rfjakob/gocryptfs/v2/internal/tlog" "github.com/hanwen/go-fuse/v2/fs" @@ -24,7 +24,7 @@ func (n *Node) prepareAtSyscall(child string) (dirfd int, cName string, errno sy // All filesystem operations go through here, so this is a good place // to reset the idle marker. - atomic.StoreUint32(&rn.IsIdle, 0) + rn.IsIdle.Store(false) if n.IsRoot() && rn.isFiltered(child) { return -1, "", syscall.EPERM @@ -74,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.go b/internal/fusefrontend/node_xattr.go index 44bc502..1470a2a 100644 --- a/internal/fusefrontend/node_xattr.go +++ b/internal/fusefrontend/node_xattr.go @@ -12,9 +12,6 @@ import ( "github.com/rfjakob/gocryptfs/v2/internal/tlog" ) -// -1 as uint32 -const minus1 = ^uint32(0) - // We store encrypted xattrs under this prefix plus the base64-encoded // encrypted original name. var xattrStorePrefix = "user.gocryptfs." @@ -35,6 +32,10 @@ func isAcl(attr string) bool { // This function is symlink-safe through Fgetxattr. func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, syscall.Errno) { rn := n.rootNode() + // If -noxattr is enabled, return ENOATTR for all getxattr calls + if rn.args.NoXattr { + return 0, noSuchAttributeError + } // If we are not mounted with -suid, reading the capability xattr does not // make a lot of sense, so reject the request and gain a massive speedup. // See https://github.com/rfjakob/gocryptfs/issues/515 . @@ -50,13 +51,13 @@ func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, var errno syscall.Errno data, errno = n.getXAttr(attr) if errno != 0 { - return minus1, errno + return 0, errno } } else { // encrypted user xattr cAttr, err := rn.encryptXattrName(attr) if err != nil { - return minus1, syscall.EIO + return 0, syscall.EIO } cData, errno := n.getXAttr(cAttr) if errno != 0 { @@ -65,15 +66,11 @@ func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, data, err = rn.decryptXattrValue(cData) if err != nil { tlog.Warn.Printf("GetXAttr: %v", err) - return minus1, syscall.EIO + return 0, syscall.EIO } } - // Caller passes size zero to find out how large their buffer should be - if len(dest) == 0 { - return uint32(len(data)), 0 - } if len(dest) < len(data) { - return minus1, syscall.ERANGE + return uint32(len(data)), syscall.ERANGE } l := copy(dest, data) return uint32(l), 0 @@ -84,6 +81,10 @@ func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, // This function is symlink-safe through Fsetxattr. func (n *Node) Setxattr(ctx context.Context, attr string, data []byte, flags uint32) syscall.Errno { rn := n.rootNode() + // If -noxattr is enabled, fail all setxattr calls + if rn.args.NoXattr { + return syscall.EOPNOTSUPP + } flags = uint32(filterXattrSetFlags(int(flags))) // ACLs are passed through without encryption @@ -109,6 +110,10 @@ func (n *Node) Setxattr(ctx context.Context, attr string, data []byte, flags uin // This function is symlink-safe through Fremovexattr. func (n *Node) Removexattr(ctx context.Context, attr string) syscall.Errno { rn := n.rootNode() + // If -noxattr is enabled, fail all removexattr calls + if rn.args.NoXattr { + return syscall.EOPNOTSUPP + } // ACLs are passed through without encryption if isAcl(attr) { @@ -126,11 +131,15 @@ func (n *Node) Removexattr(ctx context.Context, attr string) syscall.Errno { // // This function is symlink-safe through Flistxattr. func (n *Node) Listxattr(ctx context.Context, dest []byte) (uint32, syscall.Errno) { + rn := n.rootNode() + // If -noxattr is enabled, return zero results for listxattr + if rn.args.NoXattr { + return 0, 0 + } cNames, errno := n.listXAttr() if errno != 0 { return 0, errno } - rn := n.rootNode() var buf bytes.Buffer for _, curName := range cNames { // ACLs are passed through without encryption @@ -155,12 +164,8 @@ func (n *Node) Listxattr(ctx context.Context, dest []byte) (uint32, syscall.Errn } buf.WriteString(name + "\000") } - // Caller passes size zero to find out how large their buffer should be - if len(dest) == 0 { - return uint32(buf.Len()), 0 - } if buf.Len() > len(dest) { - return minus1, syscall.ERANGE + return uint32(buf.Len()), syscall.ERANGE } return uint32(copy(dest, buf.Bytes())), 0 } diff --git a/internal/fusefrontend/node_xattr_darwin.go b/internal/fusefrontend/node_xattr_darwin.go index a539847..1d25f3d 100644 --- a/internal/fusefrontend/node_xattr_darwin.go +++ b/internal/fusefrontend/node_xattr_darwin.go @@ -11,6 +11,9 @@ import ( "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" ) +// On Darwin, ENOATTR is returned when an attribute is not found. +const noSuchAttributeError = syscall.ENOATTR + // On Darwin we have to unset XATTR_NOSECURITY 0x0008 func filterXattrSetFlags(flags int) int { // See https://opensource.apple.com/source/xnu/xnu-1504.15.3/bsd/sys/xattr.h.auto.html @@ -26,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) } @@ -49,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) @@ -71,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) @@ -93,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/node_xattr_linux.go b/internal/fusefrontend/node_xattr_linux.go index 4a356a5..9964212 100644 --- a/internal/fusefrontend/node_xattr_linux.go +++ b/internal/fusefrontend/node_xattr_linux.go @@ -12,6 +12,9 @@ import ( "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" ) +// On Linux, ENODATA is returned when an attribute is not found. +const noSuchAttributeError = syscall.ENODATA + func filterXattrSetFlags(flags int) int { return flags } diff --git a/internal/fusefrontend/prepare_syscall_test.go b/internal/fusefrontend/prepare_syscall_test.go index acddaf3..e2c7d08 100644 --- a/internal/fusefrontend/prepare_syscall_test.go +++ b/internal/fusefrontend/prepare_syscall_test.go @@ -1,6 +1,7 @@ package fusefrontend import ( + "context" "strings" "syscall" "testing" @@ -22,13 +23,13 @@ func TestPrepareAtSyscall(t *testing.T) { rn := newTestFS(args) out := &fuse.EntryOut{} - child, errno := rn.Mkdir(nil, "dir1", 0700, out) + child, errno := rn.Mkdir(context.TODO(), "dir1", 0700, out) if errno != 0 { t.Fatal(errno) } rn.AddChild("dir1", child, false) dir1 := toNode(child.Operations()) - _, errno = dir1.Mkdir(nil, "dir2", 0700, out) + _, errno = dir1.Mkdir(context.TODO(), "dir2", 0700, out) if errno != 0 { t.Fatal(errno) } @@ -43,7 +44,7 @@ func TestPrepareAtSyscall(t *testing.T) { syscall.Close(dirfd) // Again, but populate the cache for "" by looking up a non-existing file - rn.Lookup(nil, "xyz1234", &fuse.EntryOut{}) + rn.Lookup(context.TODO(), "xyz1234", &fuse.EntryOut{}) dirfd, cName, errno = rn.prepareAtSyscallMyself() if errno != 0 { t.Fatal(errno) @@ -89,7 +90,7 @@ func TestPrepareAtSyscall(t *testing.T) { syscall.Close(dirfd) n255 := strings.Repeat("n", 255) - dir1.Mkdir(nil, n255, 0700, out) + dir1.Mkdir(context.TODO(), n255, 0700, out) dirfd, cName, errno = dir1.prepareAtSyscall(n255) if errno != 0 { t.Fatal(errno) @@ -116,13 +117,13 @@ func TestPrepareAtSyscallPlaintextnames(t *testing.T) { rn := newTestFS(args) out := &fuse.EntryOut{} - child, errno := rn.Mkdir(nil, "dir1", 0700, out) + child, errno := rn.Mkdir(context.TODO(), "dir1", 0700, out) if errno != 0 { t.Fatal(errno) } rn.AddChild("dir1", child, false) dir1 := toNode(child.Operations()) - _, errno = dir1.Mkdir(nil, "dir2", 0700, out) + _, errno = dir1.Mkdir(context.TODO(), "dir2", 0700, out) if errno != 0 { t.Fatal(errno) } diff --git a/internal/fusefrontend/root_node.go b/internal/fusefrontend/root_node.go index ac814ad..38d070d 100644 --- a/internal/fusefrontend/root_node.go +++ b/internal/fusefrontend/root_node.go @@ -1,9 +1,9 @@ package fusefrontend import ( - "os" "strings" "sync" + "sync/atomic" "syscall" "time" @@ -44,7 +44,7 @@ type RootNode struct { // (uint32 so that it can be reset with CompareAndSwapUint32). // When -idle was used when mounting, idleMonitor() sets it to 1 // periodically. - IsIdle uint32 + IsIdle atomic.Bool // dirCache caches directory fds dirCache dirCache // inoMap translates inode numbers from different devices to unique inode @@ -55,7 +55,7 @@ type RootNode struct { // This makes each directory entry unique (even hard links), // makes go-fuse hand out separate FUSE Node IDs for each, and prevents // bizarre problems when inode numbers are reused behind our back. - gen uint64 + gen atomic.Uint64 // quirks is a bitmap that enables workaround for quirks in the filesystem // backing the cipherdir quirks uint64 @@ -90,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 @@ -103,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/xattr_unit_test.go b/internal/fusefrontend/xattr_unit_test.go index 86c87a7..7d8e32e 100644 --- a/internal/fusefrontend/xattr_unit_test.go +++ b/internal/fusefrontend/xattr_unit_test.go @@ -21,10 +21,10 @@ func newTestFS(args Args) *RootNode { cEnc := contentenc.New(cCore, contentenc.DefaultBS) n := nametransform.New(cCore.EMECipher, true, 0, true, nil, false) rn := NewRootNode(args, cEnc, n) - oneSec := time.Second + oneSecond := time.Second options := &fs.Options{ - EntryTimeout: &oneSec, - AttrTimeout: &oneSec, + EntryTimeout: &oneSecond, + AttrTimeout: &oneSecond, } fs.NewNodeFS(rn, options) return rn |
