diff options
-rw-r--r-- | internal/fusefrontend/file.go | 482 | ||||
-rw-r--r-- | internal/fusefrontend/file2_allocate_truncate.go | 10 | ||||
-rw-r--r-- | internal/fusefrontend/file_allocate_truncate.go | 227 | ||||
-rw-r--r-- | internal/fusefrontend/file_holes.go | 92 | ||||
-rw-r--r-- | internal/fusefrontend/fs.go | 692 | ||||
-rw-r--r-- | internal/fusefrontend/fs_dir.go | 343 | ||||
-rw-r--r-- | internal/fusefrontend/node_dir_ops.go | 12 | ||||
-rw-r--r-- | internal/fusefrontend/openbackingdir.go | 84 | ||||
-rw-r--r-- | internal/fusefrontend/xattr.go | 140 | ||||
-rw-r--r-- | internal/fusefrontend/xattr_darwin.go | 90 | ||||
-rw-r--r-- | internal/fusefrontend/xattr_linux.go | 69 |
11 files changed, 22 insertions, 2219 deletions
diff --git a/internal/fusefrontend/file.go b/internal/fusefrontend/file.go deleted file mode 100644 index 2e03aa7..0000000 --- a/internal/fusefrontend/file.go +++ /dev/null @@ -1,482 +0,0 @@ -package fusefrontend - -// FUSE operations on file handles - -import ( - "bytes" - "encoding/hex" - "fmt" - "io" - "log" - "os" - "sync" - "syscall" - "time" - - "github.com/hanwen/go-fuse/v2/fuse" - "github.com/hanwen/go-fuse/v2/fuse/nodefs" - - "github.com/rfjakob/gocryptfs/internal/contentenc" - "github.com/rfjakob/gocryptfs/internal/inomap" - "github.com/rfjakob/gocryptfs/internal/openfiletable" - "github.com/rfjakob/gocryptfs/internal/serialize_reads" - "github.com/rfjakob/gocryptfs/internal/stupidgcm" - "github.com/rfjakob/gocryptfs/internal/syscallcompat" - "github.com/rfjakob/gocryptfs/internal/tlog" -) - -var _ nodefs.File = &File{} // Verify that interface is implemented. - -// File - based on loopbackFile in go-fuse/fuse/nodefs/files.go -type File struct { - fd *os.File - // Has Release() already been called on this file? This also means that the - // wlock entry has been freed, so let's not crash trying to access it. - // Due to concurrency, Release can overtake other operations. These will - // return EBADF in that case. - released bool - // fdLock prevents the fd to be closed while we are in the middle of - // an operation. - // 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 - fileTableEntry *openfiletable.Entry - // Store where the last byte was written - lastWrittenOffset int64 - // The opCount is used to judge whether "lastWrittenOffset" is still - // guaranteed to be correct. - lastOpCount uint64 - // Parent filesystem - fs *FS - // We embed a nodefs.NewDefaultFile() that returns ENOSYS for every operation we - // have not implemented. This prevents build breakage when the go-fuse library - // adds new methods to the nodefs.File interface. - nodefs.File -} - -// NewFile returns a new go-fuse File instance. -func NewFile(fd *os.File, fs *FS) (*File, fuse.Status) { - var st syscall.Stat_t - err := syscall.Fstat(int(fd.Fd()), &st) - if err != nil { - tlog.Warn.Printf("NewFile: Fstat on fd %d failed: %v\n", fd.Fd(), err) - return nil, fuse.ToStatus(err) - } - qi := inomap.QInoFromStat(&st) - e := openfiletable.Register(qi) - - return &File{ - fd: fd, - contentEnc: fs.contentEnc, - qIno: qi, - fileTableEntry: e, - fs: fs, - File: nodefs.NewDefaultFile(), - }, fuse.OK -} - -// intFd - return the backing file descriptor as an integer. -func (f *File) intFd() int { - return int(f.fd.Fd()) -} - -// readFileID loads the file header from disk and extracts the file ID. -// Returns io.EOF if the file is empty. -func (f *File) readFileID() ([]byte, error) { - // We read +1 byte to determine if the file has actual content - // and not only the header. A header-only file will be considered empty. - // This makes File ID poisoning more difficult. - readLen := contentenc.HeaderLen + 1 - buf := make([]byte, readLen) - n, err := f.fd.ReadAt(buf, 0) - if err != nil { - if err == io.EOF && n != 0 { - tlog.Warn.Printf("readFileID %d: incomplete file, got %d instead of %d bytes", - f.qIno.Ino, n, readLen) - f.fs.reportMitigatedCorruption(fmt.Sprint(f.qIno.Ino)) - } - return nil, err - } - buf = buf[:contentenc.HeaderLen] - h, err := contentenc.ParseHeader(buf) - if err != nil { - return nil, err - } - return h.ID, nil -} - -// createHeader creates a new random header and writes it to disk. -// Returns the new file ID. -// The caller must hold fileIDLock.Lock(). -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.fs.args.NoPrealloc { - err = syscallcompat.EnospcPrealloc(f.intFd(), 0, contentenc.HeaderLen) - if err != nil { - if !syscallcompat.IsENOSPC(err) { - tlog.Warn.Printf("ino%d: createHeader: prealloc failed: %s\n", f.qIno.Ino, err.Error()) - } - return nil, err - } - } - // Actually write header - _, err = f.fd.WriteAt(buf, 0) - if err != nil { - return nil, err - } - return h.ID, err -} - -// doRead - read "length" plaintext bytes from plaintext offset "off" and append -// to "dst". -// Arguments "length" and "off" do not have to be block-aligned. -// -// doRead reads the corresponding ciphertext blocks from disk, decrypts them and -// returns the requested part of the plaintext. -// -// Called by Read() for normal reading, -// by Write() and Truncate() via doWrite() for Read-Modify-Write. -func (f *File) doRead(dst []byte, off uint64, length uint64) ([]byte, fuse.Status) { - // Get the file ID, either from the open file table, or from disk. - var fileID []byte - f.fileTableEntry.IDLock.Lock() - if f.fileTableEntry.ID != nil { - // Use the cached value in the file table - fileID = f.fileTableEntry.ID - } else { - // Not cached, we have to read it from disk. - var err error - fileID, err = f.readFileID() - if err != nil { - f.fileTableEntry.IDLock.Unlock() - if err == io.EOF { - // Empty file - return nil, fuse.OK - } - buf := make([]byte, 100) - n, _ := f.fd.ReadAt(buf, 0) - buf = buf[:n] - hexdump := hex.EncodeToString(buf) - tlog.Warn.Printf("doRead %d: corrupt header: %v\nFile hexdump (%d bytes): %s", - f.qIno.Ino, err, n, hexdump) - return nil, fuse.EIO - } - // Save into the file table - f.fileTableEntry.ID = fileID - } - f.fileTableEntry.IDLock.Unlock() - if fileID == nil { - log.Panicf("fileID=%v", fileID) - } - // Read the backing ciphertext in one go - blocks := f.contentEnc.ExplodePlainRange(off, length) - alignedOffset, alignedLength := blocks[0].JointCiphertextRange(blocks) - skip := blocks[0].Skip - tlog.Debug.Printf("doRead: off=%d len=%d -> off=%d len=%d skip=%d\n", - off, length, alignedOffset, alignedLength, skip) - - ciphertext := f.fs.contentEnc.CReqPool.Get() - ciphertext = ciphertext[:int(alignedLength)] - n, err := f.fd.ReadAt(ciphertext, int64(alignedOffset)) - if err != nil && err != io.EOF { - tlog.Warn.Printf("read: ReadAt: %s", err.Error()) - return nil, fuse.ToStatus(err) - } - // The ReadAt came back empty. We can skip all the decryption and return early. - if n == 0 { - f.fs.contentEnc.CReqPool.Put(ciphertext) - return dst, fuse.OK - } - // Truncate ciphertext buffer down to actually read bytes - ciphertext = ciphertext[0:n] - - firstBlockNo := blocks[0].BlockNo - 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) - f.fs.contentEnc.CReqPool.Put(ciphertext) - if err != nil { - if f.fs.args.ForceDecode && err == stupidgcm.ErrAuth { - // We do not have the information which block was corrupt here anymore, - // but DecryptBlocks() has already logged it anyway. - tlog.Warn.Printf("doRead %d: off=%d len=%d: returning corrupt data due to forcedecode", - f.qIno.Ino, off, length) - } else { - curruptBlockNo := firstBlockNo + f.contentEnc.PlainOffToBlockNo(uint64(len(plaintext))) - tlog.Warn.Printf("doRead %d: corrupt block #%d: %v", f.qIno.Ino, curruptBlockNo, err) - return nil, fuse.EIO - } - } - - // Crop down to the relevant part - var out []byte - lenHave := len(plaintext) - lenWant := int(skip + length) - if lenHave > lenWant { - out = plaintext[skip:lenWant] - } else if lenHave > int(skip) { - out = plaintext[skip:lenHave] - } - // else: out stays empty, file was smaller than the requested offset - - out = append(dst, out...) - f.fs.contentEnc.PReqPool.Put(plaintext) - - return out, fuse.OK -} - -// Read - FUSE call -func (f *File) Read(buf []byte, off int64) (resultData fuse.ReadResult, code fuse.Status) { - if len(buf) > fuse.MAX_KERNEL_WRITE { - // This would crash us due to our fixed-size buffer pool - tlog.Warn.Printf("Read: rejecting oversized request with EMSGSIZE, len=%d", len(buf)) - return nil, fuse.Status(syscall.EMSGSIZE) - } - f.fdLock.RLock() - defer f.fdLock.RUnlock() - - f.fileTableEntry.ContentLock.RLock() - defer f.fileTableEntry.ContentLock.RUnlock() - - tlog.Debug.Printf("ino%d: FUSE Read: offset=%d length=%d", f.qIno.Ino, off, len(buf)) - if f.fs.args.SerializeReads { - serialize_reads.Wait(off, len(buf)) - } - out, status := f.doRead(buf[:0], uint64(off), uint64(len(buf))) - if f.fs.args.SerializeReads { - serialize_reads.Done() - } - if status != fuse.OK { - return nil, status - } - tlog.Debug.Printf("ino%d: Read: status %v, returning %d bytes", f.qIno.Ino, status, len(out)) - return fuse.ReadResultData(out), status -} - -// doWrite - encrypt "data" and write it to plaintext offset "off" -// -// Arguments do not have to be block-aligned, read-modify-write is -// performed internally as necessary -// -// Called by Write() for normal writing, -// and by Truncate() to rewrite the last file block. -// -// Empty writes do nothing and are allowed. -func (f *File) doWrite(data []byte, off int64) (uint32, fuse.Status) { - fileWasEmpty := false - // Get the file ID, create a new one if it does not exist yet. - var fileID []byte - // The caller has exclusively locked ContentLock, which blocks all other - // readers and writers. No need to take IDLock. - if f.fileTableEntry.ID != nil { - fileID = f.fileTableEntry.ID - } else { - // If the file ID is not cached, read it from disk - var err error - fileID, err = f.readFileID() - // Write a new file header if the file is empty - if err == io.EOF { - fileID, err = f.createHeader() - fileWasEmpty = true - } - if err != nil { - return 0, fuse.ToStatus(err) - } - f.fileTableEntry.ID = fileID - } - // Handle payload data - dataBuf := bytes.NewBuffer(data) - blocks := f.contentEnc.ExplodePlainRange(uint64(off), uint64(len(data))) - toEncrypt := make([][]byte, len(blocks)) - for i, b := range blocks { - blockData := dataBuf.Next(int(b.Length)) - // Incomplete block -> Read-Modify-Write - if b.IsPartial() { - // Read - oldData, status := f.doRead(nil, b.BlockPlainOff(), f.contentEnc.PlainBS()) - if status != fuse.OK { - tlog.Warn.Printf("ino%d fh%d: RMW read failed: %s", f.qIno.Ino, f.intFd(), status.String()) - return 0, status - } - // Modify - blockData = f.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", - f.qIno.Ino, len(blockData), b.BlockNo) - // Write into the to-encrypt list - toEncrypt[i] = blockData - } - // Encrypt all blocks - ciphertext := f.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 := int64(blocks[0].BlockCipherOff()) - if !f.fs.args.NoPrealloc { - err = syscallcompat.EnospcPrealloc(f.intFd(), cOff, int64(len(ciphertext))) - if err != nil { - if !syscallcompat.IsENOSPC(err) { - tlog.Warn.Printf("ino%d fh%d: doWrite: prealloc failed: %v", f.qIno.Ino, f.intFd(), err) - } - if fileWasEmpty { - // Kill the file header again - f.fileTableEntry.ID = nil - err2 := syscall.Ftruncate(f.intFd(), 0) - if err2 != nil { - tlog.Warn.Printf("ino%d fh%d: doWrite: rollback failed: %v", f.qIno.Ino, f.intFd(), err2) - } - } - return 0, fuse.ToStatus(err) - } - } - // Write - _, err = f.fd.WriteAt(ciphertext, cOff) - // Return memory to CReqPool - f.fs.contentEnc.CReqPool.Put(ciphertext) - if err != nil { - tlog.Warn.Printf("ino%d fh%d: doWrite: WriteAt off=%d len=%d failed: %v", - f.qIno.Ino, f.intFd(), cOff, len(ciphertext), err) - return 0, fuse.ToStatus(err) - } - return uint32(len(data)), fuse.OK -} - -// isConsecutiveWrite returns true if the current write -// directly (in time and space) follows the last write. -// This is an optimisation for streaming writes on NFS where a -// Stat() call is very expensive. -// The caller must "wlock.lock(f.devIno.ino)" otherwise this check would be racy. -func (f *File) isConsecutiveWrite(off int64) bool { - opCount := openfiletable.WriteOpCount() - return opCount == f.lastOpCount+1 && off == f.lastWrittenOffset+1 -} - -// Write - FUSE call -// -// If the write creates a hole, pads the file to the next block boundary. -func (f *File) Write(data []byte, off int64) (uint32, fuse.Status) { - if len(data) > fuse.MAX_KERNEL_WRITE { - // This would crash us due to our fixed-size buffer pool - tlog.Warn.Printf("Write: rejecting oversized request with EMSGSIZE, len=%d", len(data)) - return 0, fuse.Status(syscall.EMSGSIZE) - } - f.fdLock.RLock() - defer f.fdLock.RUnlock() - if f.released { - // The file descriptor has been closed concurrently - tlog.Warn.Printf("ino%d fh%d: Write on released file", f.qIno.Ino, f.intFd()) - return 0, fuse.EBADF - } - f.fileTableEntry.ContentLock.Lock() - defer f.fileTableEntry.ContentLock.Unlock() - tlog.Debug.Printf("ino%d: FUSE Write: offset=%d length=%d", f.qIno.Ino, off, len(data)) - // If the write creates a file hole, we have to zero-pad the last block. - // But if the write directly follows an earlier write, it cannot create a - // hole, and we can save one Stat() call. - if !f.isConsecutiveWrite(off) { - status := f.writePadHole(off) - if !status.Ok() { - return 0, status - } - } - n, status := f.doWrite(data, off) - if status.Ok() { - f.lastOpCount = openfiletable.WriteOpCount() - f.lastWrittenOffset = off + int64(len(data)) - 1 - } - return n, status -} - -// Release - FUSE call, close file -func (f *File) Release() { - f.fdLock.Lock() - if f.released { - log.Panicf("ino%d fh%d: double release", f.qIno.Ino, f.intFd()) - } - f.released = true - openfiletable.Unregister(f.qIno) - f.fd.Close() - f.fdLock.Unlock() -} - -// Flush - FUSE call -func (f *File) Flush() fuse.Status { - f.fdLock.RLock() - defer f.fdLock.RUnlock() - - // Since Flush() may be called for each dup'd fd, we don't - // want to really close the file, we just want to flush. This - // is achieved by closing a dup'd fd. - newFd, err := syscall.Dup(f.intFd()) - - if err != nil { - return fuse.ToStatus(err) - } - err = syscall.Close(newFd) - return fuse.ToStatus(err) -} - -// Fsync FUSE call -func (f *File) Fsync(flags int) (code fuse.Status) { - f.fdLock.RLock() - defer f.fdLock.RUnlock() - - return fuse.ToStatus(syscall.Fsync(f.intFd())) -} - -// Chmod FUSE call -func (f *File) Chmod(mode uint32) fuse.Status { - f.fdLock.RLock() - defer f.fdLock.RUnlock() - - // os.File.Chmod goes through the "syscallMode" translation function that messes - // up the suid and sgid bits. So use syscall.Fchmod directly. - err := syscall.Fchmod(f.intFd(), mode) - return fuse.ToStatus(err) -} - -// Chown FUSE call -func (f *File) Chown(uid uint32, gid uint32) fuse.Status { - f.fdLock.RLock() - defer f.fdLock.RUnlock() - - return fuse.ToStatus(f.fd.Chown(int(uid), int(gid))) -} - -// GetAttr FUSE call (like stat) -func (f *File) GetAttr(a *fuse.Attr) fuse.Status { - f.fdLock.RLock() - defer f.fdLock.RUnlock() - - tlog.Debug.Printf("file.GetAttr()") - st := syscall.Stat_t{} - err := syscall.Fstat(f.intFd(), &st) - if err != nil { - return fuse.ToStatus(err) - } - f.fs.inoMap.TranslateStat(&st) - a.FromStat(&st) - a.Size = f.contentEnc.CipherSizeToPlainSize(a.Size) - if f.fs.args.ForceOwner != nil { - a.Owner = *f.fs.args.ForceOwner - } - - return fuse.OK -} - -// Utimens FUSE call -func (f *File) Utimens(a *time.Time, m *time.Time) fuse.Status { - f.fdLock.RLock() - defer f.fdLock.RUnlock() - err := syscallcompat.FutimesNano(f.intFd(), a, m) - return fuse.ToStatus(err) -} diff --git a/internal/fusefrontend/file2_allocate_truncate.go b/internal/fusefrontend/file2_allocate_truncate.go index b504c50..cdda974 100644 --- a/internal/fusefrontend/file2_allocate_truncate.go +++ b/internal/fusefrontend/file2_allocate_truncate.go @@ -6,6 +6,7 @@ package fusefrontend import ( "context" "log" + "sync" "syscall" "github.com/hanwen/go-fuse/v2/fs" @@ -14,6 +15,15 @@ import ( "github.com/rfjakob/gocryptfs/internal/tlog" ) +// FALLOC_DEFAULT is a "normal" fallocate operation +const FALLOC_DEFAULT = 0x00 + +// FALLOC_FL_KEEP_SIZE allocates disk space while not modifying the file size +const FALLOC_FL_KEEP_SIZE = 0x01 + +// Only warn once +var allocateWarnOnce sync.Once + // Allocate - FUSE call for fallocate(2) // // mode=FALLOC_FL_KEEP_SIZE is implemented directly. diff --git a/internal/fusefrontend/file_allocate_truncate.go b/internal/fusefrontend/file_allocate_truncate.go deleted file mode 100644 index b6e9150..0000000 --- a/internal/fusefrontend/file_allocate_truncate.go +++ /dev/null @@ -1,227 +0,0 @@ -package fusefrontend - -// FUSE operations Truncate and Allocate on file handles -// i.e. ftruncate and fallocate - -import ( - "log" - "sync" - "syscall" - - "github.com/hanwen/go-fuse/v2/fuse" - - "github.com/rfjakob/gocryptfs/internal/syscallcompat" - "github.com/rfjakob/gocryptfs/internal/tlog" -) - -// FALLOC_DEFAULT is a "normal" fallocate operation -const FALLOC_DEFAULT = 0x00 - -// FALLOC_FL_KEEP_SIZE allocates disk space while not modifying the file size -const FALLOC_FL_KEEP_SIZE = 0x01 - -// Only warn once -var allocateWarnOnce sync.Once - -// Allocate - FUSE call for fallocate(2) -// -// mode=FALLOC_FL_KEEP_SIZE is implemented directly. -// -// mode=FALLOC_DEFAULT is implemented as a two-step process: -// -// (1) Allocate the space using FALLOC_FL_KEEP_SIZE -// (2) Set the file size using ftruncate (via truncateGrowFile) -// -// This allows us to reuse the file grow mechanics from Truncate as they are -// complicated and hard to get right. -// -// Other modes (hole punching, zeroing) are not supported. -func (f *File) Allocate(off uint64, sz uint64, mode uint32) fuse.Status { - if mode != FALLOC_DEFAULT && mode != FALLOC_FL_KEEP_SIZE { - f := func() { - tlog.Info.Printf("fallocate: only mode 0 (default) and 1 (keep size) are supported") - } - allocateWarnOnce.Do(f) - return fuse.Status(syscall.EOPNOTSUPP) - } - - f.fdLock.RLock() - defer f.fdLock.RUnlock() - if f.released { - return fuse.EBADF - } - f.fileTableEntry.ContentLock.Lock() - defer f.fileTableEntry.ContentLock.Unlock() - - blocks := f.contentEnc.ExplodePlainRange(off, sz) - firstBlock := blocks[0] - lastBlock := blocks[len(blocks)-1] - - // Step (1): Allocate the space the user wants using FALLOC_FL_KEEP_SIZE. - // This will fill file holes and/or allocate additional space past the end of - // the file. - cipherOff := firstBlock.BlockCipherOff() - cipherSz := lastBlock.BlockCipherOff() - cipherOff + - f.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) - if err != nil { - return fuse.ToStatus(err) - } - if mode == FALLOC_FL_KEEP_SIZE { - // The user did not want to change the apparent size. We are done. - return fuse.OK - } - // Step (2): Grow the apparent file size - // We need the old file size to determine if we are growing the file at all. - newPlainSz := off + sz - oldPlainSz, err := f.statPlainSize() - if err != nil { - return fuse.ToStatus(err) - } - if newPlainSz <= oldPlainSz { - // The new size is smaller (or equal). Fallocate with mode = 0 never - // truncates a file, so we are done. - return fuse.OK - } - // The file grows. The space has already been allocated in (1), so what is - // left to do is to pad the first and last block and call truncate. - // truncateGrowFile does just that. - return f.truncateGrowFile(oldPlainSz, newPlainSz) -} - -// Truncate - FUSE call -func (f *File) Truncate(newSize uint64) fuse.Status { - f.fdLock.RLock() - defer f.fdLock.RUnlock() - if f.released { - // The file descriptor has been closed concurrently. - tlog.Warn.Printf("ino%d fh%d: Truncate on released file", f.qIno.Ino, f.intFd()) - return fuse.EBADF - } - f.fileTableEntry.ContentLock.Lock() - defer f.fileTableEntry.ContentLock.Unlock() - 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 fuse.ToStatus(err) - } - // Truncate to zero kills the file header - f.fileTableEntry.ID = nil - return fuse.OK - } - // We need the old file size to determine if we are growing or shrinking - // the file - oldSize, err := f.statPlainSize() - if err != nil { - return fuse.ToStatus(err) - } - - oldB := float32(oldSize) / float32(f.contentEnc.PlainBS()) - newB := float32(newSize) / float32(f.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 - if newSize == oldSize { - return fuse.OK - } - // File grows - if newSize > oldSize { - return f.truncateGrowFile(oldSize, newSize) - } - - // File shrinks - blockNo := f.contentEnc.PlainOffToBlockNo(newSize) - cipherOff := f.contentEnc.BlockNoToCipherOff(blockNo) - plainOff := f.contentEnc.BlockNoToPlainOff(blockNo) - lastBlockLen := newSize - plainOff - var data []byte - if lastBlockLen > 0 { - var status fuse.Status - data, status = f.doRead(nil, plainOff, lastBlockLen) - if status != fuse.OK { - tlog.Warn.Printf("Truncate: shrink doRead returned error: %v", err) - return status - } - } - // Truncate down to the last complete block - err = syscall.Ftruncate(int(f.fd.Fd()), int64(cipherOff)) - if err != nil { - tlog.Warn.Printf("Truncate: shrink Ftruncate returned error: %v", err) - return fuse.ToStatus(err) - } - // Append partial block - if lastBlockLen > 0 { - _, status := f.doWrite(data, int64(plainOff)) - return status - } - return fuse.OK -} - -// statPlainSize stats the file and returns the plaintext size -func (f *File) statPlainSize() (uint64, error) { - fi, err := f.fd.Stat() - if err != nil { - tlog.Warn.Printf("ino%d fh%d: statPlainSize: %v", f.qIno.Ino, f.intFd(), err) - return 0, err - } - cipherSz := uint64(fi.Size()) - plainSz := uint64(f.contentEnc.CipherSizeToPlainSize(cipherSz)) - return plainSz, nil -} - -// truncateGrowFile extends a file using seeking or ftruncate performing RMW on -// the first and last block as necessary. New blocks in the middle become -// file holes unless they have been fallocate()'d beforehand. -func (f *File) truncateGrowFile(oldPlainSz uint64, newPlainSz uint64) fuse.Status { - if newPlainSz <= oldPlainSz { - log.Panicf("BUG: newSize=%d <= oldSize=%d", newPlainSz, oldPlainSz) - } - newEOFOffset := newPlainSz - 1 - if oldPlainSz > 0 { - n1 := f.contentEnc.PlainOffToBlockNo(oldPlainSz - 1) - n2 := f.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 { - buf := make([]byte, 1) - _, status := f.doWrite(buf, int64(newEOFOffset)) - return status - } - } - // The truncate creates at least one new block. - // - // Make sure the old last block is padded to the block boundary. This call - // is a no-op if it is already block-aligned. - status := f.zeroPad(oldPlainSz) - if !status.Ok() { - return status - } - // 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 { - // The file was empty, so it did not have a header. Create one. - if oldPlainSz == 0 { - id, err := f.createHeader() - if err != nil { - return fuse.ToStatus(err) - } - f.fileTableEntry.ID = id - } - cSz := int64(f.contentEnc.PlainSizeToCipherSize(newPlainSz)) - err := syscall.Ftruncate(f.intFd(), cSz) - if err != nil { - tlog.Warn.Printf("Truncate: grow Ftruncate returned error: %v", err) - } - return fuse.ToStatus(err) - } - // The new size is NOT aligned, so we need to write a partial block. - // Write a single zero to the last byte and let doWrite figure it out. - buf := make([]byte, 1) - _, status = f.doWrite(buf, int64(newEOFOffset)) - return status -} diff --git a/internal/fusefrontend/file_holes.go b/internal/fusefrontend/file_holes.go deleted file mode 100644 index 2b7564e..0000000 --- a/internal/fusefrontend/file_holes.go +++ /dev/null @@ -1,92 +0,0 @@ -package fusefrontend - -// Helper functions for sparse files (files with holes) - -import ( - "runtime" - "syscall" - - "github.com/hanwen/go-fuse/v2/fuse" - - "github.com/rfjakob/gocryptfs/internal/tlog" -) - -// Will a write to plaintext offset "targetOff" create a file hole in the -// ciphertext? If yes, zero-pad the last ciphertext block. -func (f *File) writePadHole(targetOff int64) fuse.Status { - // Get the current file size. - fi, err := f.fd.Stat() - if err != nil { - tlog.Warn.Printf("checkAndPadHole: Fstat failed: %v", err) - return fuse.ToStatus(err) - } - plainSize := f.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) - // targetBlock is the block the user wants to write to. - targetBlock := f.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 { - return fuse.OK - } - // The write goes past the next block. nextBlock has - // to be zero-padded to the block boundary and (at least) nextBlock+1 - // will contain a file hole in the ciphertext. - status := f.zeroPad(plainSize) - if status != fuse.OK { - return status - } - return fuse.OK -} - -// 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) fuse.Status { - lastBlockLen := plainSize % f.contentEnc.PlainBS() - if lastBlockLen == 0 { - // Already block-aligned - return fuse.OK - } - missing := f.contentEnc.PlainBS() - lastBlockLen - pad := make([]byte, missing) - tlog.Debug.Printf("zeroPad: Writing %d bytes\n", missing) - _, status := f.doWrite(pad, int64(plainSize)) - return status -} - -// SeekData calls the lseek syscall with SEEK_DATA. It returns the offset of the -// next data bytes, skipping over file holes. -func (f *File) SeekData(oldOffset int64) (int64, error) { - if runtime.GOOS != "linux" { - // Does MacOS support something like this? - return 0, syscall.EOPNOTSUPP - } - const SEEK_DATA = 3 - - // Convert plaintext offset to ciphertext offset and round down to the - // start of the current block. File holes smaller than a full block will - // be ignored. - blockNo := f.contentEnc.PlainOffToBlockNo(uint64(oldOffset)) - oldCipherOff := int64(f.contentEnc.BlockNoToCipherOff(blockNo)) - - // Determine the next data offset. If the old offset points to (or beyond) - // the end of the file, the Seek syscall fails with syscall.ENXIO. - newCipherOff, err := syscall.Seek(f.intFd(), oldCipherOff, SEEK_DATA) - if err != nil { - return 0, err - } - - // Convert ciphertext offset back to plaintext offset. At this point, - // newCipherOff should always be >= contentenc.HeaderLen. Round down, - // but ensure that the result is never smaller than the initial offset - // (to avoid endless loops). - blockNo = f.contentEnc.CipherOffToBlockNo(uint64(newCipherOff)) - newOffset := int64(f.contentEnc.BlockNoToPlainOff(blockNo)) - if newOffset < oldOffset { - newOffset = oldOffset - } - - return newOffset, nil -} diff --git a/internal/fusefrontend/fs.go b/internal/fusefrontend/fs.go deleted file mode 100644 index e8dae9f..0000000 --- a/internal/fusefrontend/fs.go +++ /dev/null @@ -1,692 +0,0 @@ -// Package fusefrontend interfaces directly with the go-fuse library. -package fusefrontend - -// FUSE operations on paths - -import ( - "os" - "sync" - "sync/atomic" - "syscall" - "time" - - "golang.org/x/sys/unix" - - "github.com/hanwen/go-fuse/v2/fuse" - "github.com/hanwen/go-fuse/v2/fuse/nodefs" - "github.com/hanwen/go-fuse/v2/fuse/pathfs" - - "github.com/rfjakob/gocryptfs/internal/configfile" - "github.com/rfjakob/gocryptfs/internal/contentenc" - "github.com/rfjakob/gocryptfs/internal/inomap" - "github.com/rfjakob/gocryptfs/internal/nametransform" - "github.com/rfjakob/gocryptfs/internal/serialize_reads" - "github.com/rfjakob/gocryptfs/internal/syscallcompat" - "github.com/rfjakob/gocryptfs/internal/tlog" -) - -// FS implements the go-fuse virtual filesystem interface. -type FS struct { - // Embed pathfs.defaultFileSystem to avoid compile failure when the - // pathfs.FileSystem interface gets new functions. defaultFileSystem - // provides a no-op implementation for all functions. - pathfs.FileSystem - args Args // Stores configuration arguments - // dirIVLock: Lock()ed if any "gocryptfs.diriv" file is modified - // Readers must RLock() it to prevent them from seeing intermediate - // states - dirIVLock sync.RWMutex - // Filename encryption helper - nameTransform nametransform.NameTransformer - // Content encryption helper - contentEnc *contentenc.ContentEnc - // This lock is used by openWriteOnlyFile() to block concurrent opens while - // it relaxes the permissions on a file. - openWriteOnlyLock sync.RWMutex - // MitigatedCorruptions is used to report data corruption that is internally - // mitigated by ignoring the corrupt item. For example, when OpenDir() finds - // a corrupt filename, we still return the other valid filenames. - // The corruption is logged to syslog to inform the user, and in addition, - // the corrupt filename is logged to this channel via - // reportMitigatedCorruption(). - // "gocryptfs -fsck" reads from the channel to also catch these transparently- - // mitigated corruptions. - MitigatedCorruptions chan string - // This flag is set to zero each time fs.isFiltered() is called - // (uint32 so that it can be reset with CompareAndSwapUint32). - // When -idle was used when mounting, idleMonitor() sets it to 1 - // periodically. - IsIdle uint32 - // dirCache caches directory fds - dirCache dirCacheStruct - // inoMap translates inode numbers from different devices to unique inode - // numbers. - inoMap *inomap.InoMap -} - -//var _ pathfs.FileSystem = &FS{} // Verify that interface is implemented. - -// NewFS returns a new encrypted FUSE overlay filesystem. -func NewFS(args Args, c *contentenc.ContentEnc, n nametransform.NameTransformer) *FS { - if args.SerializeReads { - serialize_reads.InitSerializer() - } - if len(args.Exclude) > 0 { - tlog.Warn.Printf("Forward mode does not support -exclude") - } - var st syscall.Stat_t - err := syscall.Stat(args.Cipherdir, &st) - if err != nil { - tlog.Warn.Printf("NewFS: could not stat cipherdir: %v", err) - st.Dev = 0 - } - return &FS{ - FileSystem: pathfs.NewDefaultFileSystem(), - args: args, - nameTransform: n, - contentEnc: c, - inoMap: inomap.New(), - } -} - -// GetAttr implements pathfs.Filesystem. -// -// GetAttr is symlink-safe through use of openBackingDir() and Fstatat(). -func (fs *FS) GetAttr(relPath string, context *fuse.Context) (*fuse.Attr, fuse.Status) { - tlog.Debug.Printf("FS.GetAttr(%q)", relPath) - if fs.isFiltered(relPath) { - return nil, fuse.EPERM - } - dirfd, cName, err := fs.openBackingDir(relPath) - if err != nil { - return nil, fuse.ToStatus(err) - } - var st unix.Stat_t - err = syscallcompat.Fstatat(dirfd, cName, &st, unix.AT_SYMLINK_NOFOLLOW) - syscall.Close(dirfd) - if err != nil { - return nil, fuse.ToStatus(err) - } - a := &fuse.Attr{} - st2 := syscallcompat.Unix2syscall(st) - fs.inoMap.TranslateStat(&st2) - a.FromStat(&st2) - if a.IsRegular() { - a.Size = fs.contentEnc.CipherSizeToPlainSize(a.Size) - } else if a.IsSymlink() { - target, _ := fs.Readlink(relPath, context) - a.Size = uint64(len(target)) - } - if fs.args.ForceOwner != nil { - a.Owner = *fs.args.ForceOwner - } - return a, fuse.OK -} - -// 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 (fs *FS) 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 -} - -// Open - FUSE call. Open already-existing file. -// -// Symlink-safe through Openat(). -func (fs *FS) Open(path string, flags uint32, context *fuse.Context) (fuseFile nodefs.File, status fuse.Status) { - if fs.isFiltered(path) { - return nil, fuse.EPERM - } - newFlags := fs.mangleOpenFlags(flags) - // Taking this lock makes sure we don't race openWriteOnlyFile() - fs.openWriteOnlyLock.RLock() - defer fs.openWriteOnlyLock.RUnlock() - // Symlink-safe open - dirfd, cName, err := fs.openBackingDir(path) - if err != nil { - return nil, fuse.ToStatus(err) - } - defer syscall.Close(dirfd) - fd, err := syscallcompat.Openat(dirfd, cName, newFlags, 0) - // Handle a few specific errors - if err != nil { - if err == syscall.EMFILE { - var lim syscall.Rlimit - syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim) - tlog.Warn.Printf("Open %q: too many open files. Current \"ulimit -n\": %d", cName, lim.Cur) - } - if err == syscall.EACCES && (int(flags)&syscall.O_ACCMODE) == syscall.O_WRONLY { - return fs.openWriteOnlyFile(dirfd, cName, newFlags) - } - return nil, fuse.ToStatus(err) - } - f := os.NewFile(uintptr(fd), cName) - return NewFile(f, fs) -} - -// openBackingFile opens the ciphertext file that backs relative plaintext -// path "relPath". Always adds O_NOFOLLOW to the flags. -func (fs *FS) openBackingFile(relPath string, flags int) (fd int, err error) { - dirfd, cName, err := fs.openBackingDir(relPath) - if err != nil { - return -1, err - } - defer syscall.Close(dirfd) - return syscallcompat.Openat(dirfd, cName, flags|syscall.O_NOFOLLOW, 0) -} - -// Due to RMW, we always need read permissions on the backing file. This is a -// problem if the file permissions do not allow reading (i.e. 0200 permissions). -// This function works around that problem by chmod'ing the file, obtaining a fd, -// and chmod'ing it back. -func (fs *FS) openWriteOnlyFile(dirfd int, cName string, newFlags int) (*File, fuse.Status) { - woFd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NOFOLLOW, 0) - if err != nil { - return nil, fuse.ToStatus(err) - } - defer syscall.Close(woFd) - var st syscall.Stat_t - err = syscall.Fstat(woFd, &st) - if err != nil { - return nil, fuse.ToStatus(err) - } - // The cast to uint32 fixes a build failure on Darwin, where st.Mode is uint16. - perms := uint32(st.Mode) - // Verify that we don't have read permissions - if perms&0400 != 0 { - tlog.Warn.Printf("openWriteOnlyFile: unexpected permissions %#o, returning EPERM", perms) - return nil, fuse.ToStatus(syscall.EPERM) - } - // Upgrade the lock to block other Open()s and downgrade again on return - fs.openWriteOnlyLock.RUnlock() - fs.openWriteOnlyLock.Lock() - defer func() { - fs.openWriteOnlyLock.Unlock() - fs.openWriteOnlyLock.RLock() - }() - // Relax permissions and revert on return - err = syscall.Fchmod(woFd, perms|0400) - if err != nil { - tlog.Warn.Printf("openWriteOnlyFile: changing permissions failed: %v", err) - return nil, fuse.ToStatus(err) - } - defer func() { - err2 := syscall.Fchmod(woFd, perms) - if err2 != nil { - tlog.Warn.Printf("openWriteOnlyFile: reverting permissions failed: %v", err2) - } - }() - rwFd, err := syscallcompat.Openat(dirfd, cName, newFlags, 0) - if err != nil { - return nil, fuse.ToStatus(err) - } - f := os.NewFile(uintptr(rwFd), cName) - return NewFile(f, fs) -} - -// Create - FUSE call. Creates a new file. -// -// Symlink-safe through the use of Openat(). -func (fs *FS) Create(path string, flags uint32, mode uint32, context *fuse.Context) (nodefs.File, fuse.Status) { - if fs.isFiltered(path) { - return nil, fuse.EPERM - } - newFlags := fs.mangleOpenFlags(flags) - dirfd, cName, err := fs.openBackingDir(path) - if err != nil { - return nil, fuse.ToStatus(err) - } - defer syscall.Close(dirfd) - fd := -1 - // Make sure context is nil if we don't want to preserve the owner - if !fs.args.PreserveOwner { - context = nil - } - // Handle long file name - if !fs.args.PlaintextNames && nametransform.IsLongContent(cName) { - // Create ".name" - err = fs.nameTransform.WriteLongNameAt(dirfd, cName, path) - if err != nil { - return nil, fuse.ToStatus(err) - } - // Create content - fd, err = syscallcompat.OpenatUser(dirfd, cName, newFlags|syscall.O_CREAT|syscall.O_EXCL, mode, context) - if err != nil { - nametransform.DeleteLongNameAt(dirfd, cName) - } - } else { - // Create content, normal (short) file name - fd, err = syscallcompat.OpenatUser(dirfd, cName, newFlags|syscall.O_CREAT|syscall.O_EXCL, mode, context) - } - if err != nil { - // xfstests generic/488 triggers this - if err == syscall.EMFILE { - var lim syscall.Rlimit - syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim) - tlog.Warn.Printf("Create %q: too many open files. Current \"ulimit -n\": %d", cName, lim.Cur) - } - return nil, fuse.ToStatus(err) - } - f := os.NewFile(uintptr(fd), cName) - return NewFile(f, fs) -} - -// Chmod - FUSE call. Change permissions on "path". -// -// Symlink-safe through use of Fchmodat(). -func (fs *FS) Chmod(path string, mode uint32, context *fuse.Context) (code fuse.Status) { - if fs.isFiltered(path) { - return fuse.EPERM - } - dirfd, cName, err := fs.openBackingDir(path) - if err != nil { - return fuse.ToStatus(err) - } - defer syscall.Close(dirfd) - // os.Chmod goes through the "syscallMode" translation function that messes - // up the suid and sgid bits. So use a syscall directly. - err = syscallcompat.FchmodatNofollow(dirfd, cName, mode) - return fuse.ToStatus(err) -} - -// Chown - FUSE call. Change the owner of "path". -// -// Symlink-safe through use of Fchownat(). -func (fs *FS) Chown(path string, uid uint32, gid uint32, context *fuse.Context) (code fuse.Status) { - if fs.isFiltered(path) { - return fuse.EPERM - } - dirfd, cName, err := fs.openBackingDir(path) - if err != nil { - return fuse.ToStatus(err) - } - defer syscall.Close(dirfd) - err = syscallcompat.Fchownat(dirfd, cName, int(uid), int(gid), unix.AT_SYMLINK_NOFOLLOW) - return fuse.ToStatus(err) -} - -// Mknod - FUSE call. Create a device file. -// -// Symlink-safe through use of Mknodat(). -func (fs *FS) Mknod(path string, mode uint32, dev uint32, context *fuse.Context) (code fuse.Status) { - if fs.isFiltered(path) { - return fuse.EPERM - } - dirfd, cName, err := fs.openBackingDir(path) - if err != nil { - return fuse.ToStatus(err) - } - defer syscall.Close(dirfd) - // Make sure context is nil if we don't want to preserve the owner - if !fs.args.PreserveOwner { - context = nil - } - // Create ".name" file to store long file name (except in PlaintextNames mode) - if !fs.args.PlaintextNames && nametransform.IsLongContent(cName) { - err = fs.nameTransform.WriteLongNameAt(dirfd, cName, path) - if err != nil { - return fuse.ToStatus(err) - } - // Create "gocryptfs.longfile." device node - err = syscallcompat.MknodatUser(dirfd, cName, mode, int(dev), context) - if err != nil { - nametransform.DeleteLongNameAt(dirfd, cName) - } - } else { - // Create regular device node - err = syscallcompat.MknodatUser(dirfd, cName, mode, int(dev), context) - } - return fuse.ToStatus(err) -} - -// Truncate - FUSE call. Truncates a file. -// -// Support truncate(2) by opening the file and calling ftruncate(2) -// While the glibc "truncate" wrapper seems to always use ftruncate, fsstress from -// xfstests uses this a lot by calling "truncate64" directly. -// -// Symlink-safe by letting file.Truncate() do all the work. -func (fs *FS) Truncate(path string, offset uint64, context *fuse.Context) (code fuse.Status) { - file, code := fs.Open(path, uint32(os.O_RDWR), context) - if code != fuse.OK { - return code - } - code = file.Truncate(offset) - file.Release() - return code -} - -// Utimens - FUSE call. Set the timestamps on file "path". -// -// Symlink-safe through UtimesNanoAt. -func (fs *FS) Utimens(path string, a *time.Time, m *time.Time, context *fuse.Context) (code fuse.Status) { - if fs.isFiltered(path) { - return fuse.EPERM - } - dirfd, cName, err := fs.openBackingDir(path) - if err != nil { - return fuse.ToStatus(err) - } - defer syscall.Close(dirfd) - err = syscallcompat.UtimesNanoAtNofollow(dirfd, cName, a, m) - return fuse.ToStatus(err) -} - -// StatFs - FUSE call. Returns information about the filesystem. -// -// Symlink-safe because the passed path is ignored. -func (fs *FS) StatFs(path string) *fuse.StatfsOut { - var st syscall.Statfs_t - err := syscall.Statfs(fs.args.Cipherdir, &st) - if err == nil { - var out fuse.StatfsOut - out.FromStatfsT(&st) - return &out - } - return nil -} - -// decryptSymlinkTarget: "cData64" is base64-decoded and decrypted -// like file contents (GCM). -// The empty string decrypts to the empty string. -// -// This function does not do any I/O and is hence symlink-safe. -func (fs *FS) decryptSymlinkTarget(cData64 string) (string, error) { - if cData64 == "" { - return "", nil - } - cData, err := fs.nameTransform.B64DecodeString(cData64) - if err != nil { - return "", err - } - data, err := fs.contentEnc.DecryptBlock([]byte(cData), 0, nil) - if err != nil { - return "", err - } - return string(data), nil -} - -// Readlink - FUSE call. -// -// Symlink-safe through openBackingDir() + Readlinkat(). -func (fs *FS) Readlink(relPath string, context *fuse.Context) (out string, status fuse.Status) { - dirfd, cName, err := fs.openBackingDir(relPath) - if err != nil { - return "", fuse.ToStatus(err) - } - defer syscall.Close(dirfd) - cTarget, err := syscallcompat.Readlinkat(dirfd, cName) - if err != nil { - return "", fuse.ToStatus(err) - } - if fs.args.PlaintextNames { - return cTarget, fuse.OK - } - // Symlinks are encrypted like file contents (GCM) and base64-encoded - target, err := fs.decryptSymlinkTarget(cTarget) - if err != nil { - tlog.Warn.Printf("Readlink %q: decrypting target failed: %v", cName, err) - return "", fuse.EIO - } - return string(target), fuse.OK -} - -// Unlink - FUSE call. Delete a file. -// -// Symlink-safe through use of Unlinkat(). -func (fs *FS) Unlink(path string, context *fuse.Context) (code fuse.Status) { - if fs.isFiltered(path) { - return fuse.EPERM - } - dirfd, cName, err := fs.openBackingDir(path) - if err != nil { - return fuse.ToStatus(err) - } - defer syscall.Close(dirfd) - // Delete content - err = syscallcompat.Unlinkat(dirfd, cName, 0) - if err != nil { - return fuse.ToStatus(err) - } - // Delete ".name" file - if !fs.args.PlaintextNames && nametransform.IsLongContent(cName) { - err = nametransform.DeleteLongNameAt(dirfd, cName) - if err != nil { - tlog.Warn.Printf("Unlink: could not delete .name file: %v", err) - } - } - return fuse.ToStatus(err) -} - -// encryptSymlinkTarget: "data" is encrypted like file contents (GCM) -// and base64-encoded. -// The empty string encrypts to the empty string. -// -// Symlink-safe because it does not do any I/O. -func (fs *FS) encryptSymlinkTarget(data string) (cData64 string) { - if data == "" { - return "" - } - cData := fs.contentEnc.EncryptBlock([]byte(data), 0, nil) - cData64 = fs.nameTransform.B64EncodeToString(cData) - return cData64 -} - -// Symlink - FUSE call. Create a symlink. -// -// Symlink-safe through use of Symlinkat. -func (fs *FS) Symlink(target string, linkName string, context *fuse.Context) (code fuse.Status) { - tlog.Debug.Printf("Symlink(\"%s\", \"%s\")", target, linkName) - if fs.isFiltered(linkName) { - return fuse.EPERM - } - dirfd, cName, err := fs.openBackingDir(linkName) - if err != nil { - return fuse.ToStatus(err) - } - defer syscall.Close(dirfd) - // Make sure context is nil if we don't want to preserve the owner - if !fs.args.PreserveOwner { - context = nil - } - cTarget := target - if !fs.args.PlaintextNames { - // Symlinks are encrypted like file contents (GCM) and base64-encoded - cTarget = fs.encryptSymlinkTarget(target) - } - // Create ".name" file to store long file name (except in PlaintextNames mode) - if !fs.args.PlaintextNames && nametransform.IsLongContent(cName) { - err = fs.nameTransform.WriteLongNameAt(dirfd, cName, linkName) - if err != nil { - return fuse.ToStatus(err) - } - // Create "gocryptfs.longfile." symlink - err = syscallcompat.SymlinkatUser(cTarget, dirfd, cName, context) - if err != nil { - nametransform.DeleteLongNameAt(dirfd, cName) - } - } else { - // Create symlink - err = syscallcompat.SymlinkatUser(cTarget, dirfd, cName, context) - } - return fuse.ToStatus(err) -} - -// Rename - FUSE call. -// -// Symlink-safe through Renameat(). -func (fs *FS) Rename(oldPath string, newPath string, context *fuse.Context) (code fuse.Status) { - defer fs.dirCache.Clear() - if fs.isFiltered(newPath) { - return fuse.EPERM - } - oldDirfd, oldCName, err := fs.openBackingDir(oldPath) - if err != nil { - return fuse.ToStatus(err) - } - defer syscall.Close(oldDirfd) - newDirfd, newCName, err := fs.openBackingDir(newPath) - if err != nil { - return fuse.ToStatus(err) - } - defer syscall.Close(newDirfd) - // Easy case. - if fs.args.PlaintextNames { - return fuse.ToStatus(syscallcompat.Renameat(oldDirfd, oldCName, newDirfd, newCName)) - } - // Long destination file name: create .name file - nameFileAlreadyThere := false - if nametransform.IsLongContent(newCName) { - err = fs.nameTransform.WriteLongNameAt(newDirfd, newCName, newPath) - // Failure to write the .name file is expected when the target path already - // exists. Since hashes are pretty unique, there is no need to modify the - // .name file in this case, and we ignore the error. - if err == syscall.EEXIST { - nameFileAlreadyThere = true - } else if err != nil { - return fuse.ToStatus(err) - } - } - // Actual rename - tlog.Debug.Printf("Renameat %d/%s -> %d/%s\n", oldDirfd, oldCName, newDirfd, newCName) - err = syscallcompat.Renameat(oldDirfd, oldCName, newDirfd, newCName) - if err == syscall.ENOTEMPTY || err == syscall.EEXIST { - // If an empty directory is overwritten we will always get an error as - // the "empty" directory will still contain gocryptfs.diriv. - // Interestingly, ext4 returns ENOTEMPTY while xfs returns EEXIST. - // We handle that by trying to fs.Rmdir() the target directory and trying - // again. - tlog.Debug.Printf("Rename: Handling ENOTEMPTY") - if fs.Rmdir(newPath, context) == fuse.OK { - err = syscallcompat.Renameat(oldDirfd, oldCName, newDirfd, newCName) - } - } - if err != nil { - if nametransform.IsLongContent(newCName) && nameFileAlreadyThere == false { - // Roll back .name creation unless the .name file was already there - nametransform.DeleteLongNameAt(newDirfd, newCName) - } - return fuse.ToStatus(err) - } - if nametransform.IsLongContent(oldCName) { - nametransform.DeleteLongNameAt(oldDirfd, oldCName) - } - return fuse.OK -} - -// Link - FUSE call. Creates a hard link at "newPath" pointing to file -// "oldPath". -// -// Symlink-safe through use of Linkat(). -func (fs *FS) Link(oldPath string, newPath string, context *fuse.Context) (code fuse.Status) { - if fs.isFiltered(newPath) { - return fuse.EPERM - } - oldDirFd, cOldName, err := fs.openBackingDir(oldPath) - if err != nil { - return fuse.ToStatus(err) - } - defer syscall.Close(oldDirFd) - newDirFd, cNewName, err := fs.openBackingDir(newPath) - if err != nil { - return fuse.ToStatus(err) - } - defer syscall.Close(newDirFd) - // Handle long file name (except in PlaintextNames mode) - if !fs.args.PlaintextNames && nametransform.IsLongContent(cNewName) { - err = fs.nameTransform.WriteLongNameAt(newDirFd, cNewName, newPath) - if err != nil { - return fuse.ToStatus(err) - } - // Create "gocryptfs.longfile." link - err = syscallcompat.Linkat(oldDirFd, cOldName, newDirFd, cNewName, 0) - if err != nil { - nametransform.DeleteLongNameAt(newDirFd, cNewName) - } - } else { - // Create regular link - err = syscallcompat.Linkat(oldDirFd, cOldName, newDirFd, cNewName, 0) - } - return fuse.ToStatus(err) -} - -// Access - FUSE call. Check if a file can be accessed in the specified mode(s) -// (read, write, execute). -// -// From https://github.com/libfuse/libfuse/blob/master/include/fuse.h : -// -// > Check file access permissions -// > -// > If the 'default_permissions' mount option is given, this method is not -// > called. -// -// We always enable default_permissions when -allow_other is passed, so there -// is no need for this function to check the uid in fuse.Context. -// -// Symlink-safe through use of faccessat. -func (fs *FS) Access(relPath string, mode uint32, context *fuse.Context) (code fuse.Status) { - if fs.isFiltered(relPath) { - return fuse.EPERM - } - dirfd, cName, err := fs.openBackingDir(relPath) - if err != nil { - return fuse.ToStatus(err) - } - err = syscallcompat.Faccessat(dirfd, cName, mode) - syscall.Close(dirfd) - return fuse.ToStatus(err) -} - -// 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). -// See the MitigatedCorruptions channel for more info. -func (fs *FS) reportMitigatedCorruption(item string) { - if fs.MitigatedCorruptions == nil { - return - } - select { - case fs.MitigatedCorruptions <- item: - case <-time.After(1 * time.Second): - tlog.Warn.Printf("BUG: reportCorruptItem: timeout") - //debug.PrintStack() - return - } -} - -// isFiltered - check if plaintext "path" should be forbidden -// -// Prevents name clashes with internal files when file names are not encrypted -func (fs *FS) isFiltered(path string) bool { - atomic.StoreUint32(&fs.IsIdle, 0) - - if !fs.args.PlaintextNames { - return false - } - // gocryptfs.conf in the root directory is forbidden - if path == configfile.ConfDefaultName { - tlog.Info.Printf("The name /%s is reserved when -plaintextnames is used\n", - configfile.ConfDefaultName) - return true - } - // Note: gocryptfs.diriv is NOT forbidden because diriv and plaintextnames - // are exclusive - return false -} diff --git a/internal/fusefrontend/fs_dir.go b/internal/fusefrontend/fs_dir.go deleted file mode 100644 index 0d8adae..0000000 --- a/internal/fusefrontend/fs_dir.go +++ /dev/null @@ -1,343 +0,0 @@ -package fusefrontend - -// Mkdir and Rmdir - -import ( - "fmt" - "io" - "runtime" - "syscall" - - "golang.org/x/sys/unix" - - "github.com/hanwen/go-fuse/v2/fuse" - - "github.com/rfjakob/gocryptfs/internal/configfile" - "github.com/rfjakob/gocryptfs/internal/cryptocore" - "github.com/rfjakob/gocryptfs/internal/nametransform" - "github.com/rfjakob/gocryptfs/internal/syscallcompat" - "github.com/rfjakob/gocryptfs/internal/tlog" -) - -const dsStoreName = ".DS_Store" - -// mkdirWithIv - create a new directory and corresponding diriv file. dirfd -// should be a handle to the parent directory, cName is the name of the new -// directory and mode specifies the access permissions to use. -func (fs *FS) mkdirWithIv(dirfd int, cName string, mode uint32, context *fuse.Context) error { - // Between the creation of the directory and the creation of gocryptfs.diriv - // the directory is inconsistent. Take the lock to prevent other readers - // from seeing it. - fs.dirIVLock.Lock() - defer fs.dirIVLock.Unlock() - err := syscallcompat.MkdiratUser(dirfd, cName, mode, &context.Caller) - if err != nil { - return err - } - dirfd2, err := syscallcompat.Openat(dirfd, cName, syscall.O_DIRECTORY|syscall.O_NOFOLLOW|syscallcompat.O_PATH, 0) - if err == nil { - // Create gocryptfs.diriv - err = nametransform.WriteDirIVAt(dirfd2) - syscall.Close(dirfd2) - } - if err != nil { - // Delete inconsistent directory (missing gocryptfs.diriv!) - err2 := syscallcompat.Unlinkat(dirfd, cName, unix.AT_REMOVEDIR) - if err2 != nil { - tlog.Warn.Printf("mkdirWithIv: rollback failed: %v", err2) - } - } - return err -} - -// Mkdir - FUSE call. Create a directory at "newPath" with permissions "mode". -// -// Symlink-safe through use of Mkdirat(). -func (fs *FS) Mkdir(newPath string, mode uint32, context *fuse.Context) (code fuse.Status) { - if fs.isFiltered(newPath) { - return fuse.EPERM - } - dirfd, cName, err := fs.openBackingDir(newPath) - if err != nil { - return fuse.ToStatus(err) - } - defer syscall.Close(dirfd) - // Make sure context is nil if we don't want to preserve the owner - if !fs.args.PreserveOwner { - context = nil - } - if fs.args.PlaintextNames { - err = syscallcompat.MkdiratUser(dirfd, cName, mode, &context.Caller) - return fuse.ToStatus(err) - } - - // We need write and execute permissions to create gocryptfs.diriv. - // Also, we need read permissions to open the directory (to avoid - // race-conditions between getting and setting the mode). - origMode := mode - mode = mode | 0700 - - // Handle long file name - if nametransform.IsLongContent(cName) { - // Create ".name" - err = fs.nameTransform.WriteLongNameAt(dirfd, cName, newPath) - if err != nil { - return fuse.ToStatus(err) - } - - // Create directory - err = fs.mkdirWithIv(dirfd, cName, mode, context) - if err != nil { - nametransform.DeleteLongNameAt(dirfd, cName) - return fuse.ToStatus(err) - } - } else { - err = fs.mkdirWithIv(dirfd, cName, mode, context) - if err != nil { - return fuse.ToStatus(err) - } - } - // Set mode - if origMode != mode { - dirfd2, err := syscallcompat.Openat(dirfd, cName, - syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0) - if err != nil { - tlog.Warn.Printf("Mkdir %q: Openat failed: %v", cName, err) - return fuse.ToStatus(err) - } - defer syscall.Close(dirfd2) - - var st syscall.Stat_t - err = syscall.Fstat(dirfd2, &st) - if err != nil { - tlog.Warn.Printf("Mkdir %q: Fstat failed: %v", cName, err) - return fuse.ToStatus(err) - } - - // Preserve SGID bit if it was set due to inheritance. - origMode = uint32(st.Mode&^0777) | origMode - err = syscall.Fchmod(dirfd2, origMode) - if err != nil { - tlog.Warn.Printf("Mkdir %q: Fchmod %#o -> %#o failed: %v", cName, mode, origMode, err) - return fuse.ToStatus(err) - } - } - return fuse.OK -} - -// haveDsstore return true if one of the entries in "names" is ".DS_Store". -func haveDsstore(entries []fuse.DirEntry) bool { - for _, e := range entries { - if e.Name == dsStoreName { - return true - } - } - return false -} - -// Rmdir - FUSE call. -// -// Symlink-safe through Unlinkat() + AT_REMOVEDIR. -func (fs *FS) Rmdir(relPath string, context *fuse.Context) (code fuse.Status) { - defer fs.dirCache.Clear() - parentDirFd, cName, err := fs.openBackingDir(relPath) - if err != nil { - return fuse.ToStatus(err) - } - defer syscall.Close(parentDirFd) - if fs.args.PlaintextNames { - // Unlinkat with AT_REMOVEDIR is equivalent to Rmdir - err = unix.Unlinkat(parentDirFd, cName, unix.AT_REMOVEDIR) - return fuse.ToStatus(err) - } - // Unless we are running as root, we need read, write and execute permissions - // to handle gocryptfs.diriv. - permWorkaround := false - var origMode uint32 - if !fs.args.PreserveOwner { - var st unix.Stat_t - err = syscallcompat.Fstatat(parentDirFd, cName, &st, unix.AT_SYMLINK_NOFOLLOW) - if err != nil { - return fuse.ToStatus(err) - } - if st.Mode&0700 != 0700 { - tlog.Debug.Printf("Rmdir: permWorkaround") - permWorkaround = true - // This cast is needed on Darwin, where st.Mode is uint16. - origMode = uint32(st.Mode) - err = syscallcompat.FchmodatNofollow(parentDirFd, cName, origMode|0700) - if err != nil { - tlog.Debug.Printf("Rmdir: permWorkaround: chmod failed: %v", err) - return fuse.ToStatus(err) - } - } - } - dirfd, err := syscallcompat.Openat(parentDirFd, cName, - syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0) - if err != nil { - tlog.Debug.Printf("Rmdir: Open: %v", err) - return fuse.ToStatus(err) - } - defer syscall.Close(dirfd) - // Undo the chmod if removing the directory failed. This must run before - // closing dirfd, so defer it after (defer is LIFO). - if permWorkaround { - defer func() { - if code != fuse.OK { - err = unix.Fchmod(dirfd, origMode) - if err != nil { - tlog.Warn.Printf("Rmdir: permWorkaround: rollback failed: %v", err) - } - } - }() - } -retry: - // Check directory contents - children, err := syscallcompat.Getdents(dirfd) - if err == io.EOF { - // The directory is empty - tlog.Warn.Printf("Rmdir: %q: %s is missing", cName, nametransform.DirIVFilename) - err = unix.Unlinkat(parentDirFd, cName, unix.AT_REMOVEDIR) - return fuse.ToStatus(err) - } - if err != nil { - tlog.Warn.Printf("Rmdir: Readdirnames: %v", err) - return fuse.ToStatus(err) - } - // MacOS sprinkles .DS_Store files everywhere. This is hard to avoid for - // users, so handle it transparently here. - if runtime.GOOS == "darwin" && len(children) <= 2 && haveDsstore(children) { - err = unix.Unlinkat(dirfd, dsStoreName, 0) - if err != nil { - tlog.Warn.Printf("Rmdir: failed to delete blocking file %q: %v", dsStoreName, err) - return fuse.ToStatus(err) - } - tlog.Warn.Printf("Rmdir: had to delete blocking file %q", dsStoreName) - goto retry - } - // If the directory is not empty besides gocryptfs.diriv, do not even - // attempt the dance around gocryptfs.diriv. - if len(children) > 1 { - return fuse.ToStatus(syscall.ENOTEMPTY) - } - // Move "gocryptfs.diriv" to the parent dir as "gocryptfs.diriv.rmdir.XYZ" - tmpName := fmt.Sprintf("%s.rmdir.%d", nametransform.DirIVFilename, cryptocore.RandUint64()) - tlog.Debug.Printf("Rmdir: Renaming %s to %s", nametransform.DirIVFilename, tmpName) - // The directory is in an inconsistent state between rename and rmdir. - // Protect against concurrent readers. - fs.dirIVLock.Lock() - defer fs.dirIVLock.Unlock() - err = syscallcompat.Renameat(dirfd, nametransform.DirIVFilename, - parentDirFd, tmpName) - if err != nil { - tlog.Warn.Printf("Rmdir: Renaming %s to %s failed: %v", - nametransform.DirIVFilename, tmpName, err) - return fuse.ToStatus(err) - } - // Actual Rmdir - err = syscallcompat.Unlinkat(parentDirFd, cName, unix.AT_REMOVEDIR) - if err != nil { - // This can happen if another file in the directory was created in the - // meantime, undo the rename - err2 := syscallcompat.Renameat(parentDirFd, tmpName, - dirfd, nametransform.DirIVFilename) - if err2 != nil { - tlog.Warn.Printf("Rmdir: Rename rollback failed: %v", err2) - } - return fuse.ToStatus(err) - } - // Delete "gocryptfs.diriv.rmdir.XYZ" - err = syscallcompat.Unlinkat(parentDirFd, tmpName, 0) - if err != nil { - tlog.Warn.Printf("Rmdir: Could not clean up %s: %v", tmpName, err) - } - // Delete .name file - if nametransform.IsLongContent(cName) { - nametransform.DeleteLongNameAt(parentDirFd, cName) - } - return fuse.OK -} - -// OpenDir - FUSE call -// -// This function is symlink-safe through use of openBackingDir() and -// ReadDirIVAt(). -func (fs *FS) OpenDir(dirName string, context *fuse.Context) ([]fuse.DirEntry, fuse.Status) { - tlog.Debug.Printf("OpenDir(%s)", dirName) - parentDirFd, cDirName, err := fs.openBackingDir(dirName) - if err != nil { - return nil, fuse.ToStatus(err) - } - defer syscall.Close(parentDirFd) - // Read ciphertext directory - var cipherEntries []fuse.DirEntry - var status fuse.Status - fd, err := syscallcompat.Openat(parentDirFd, cDirName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0) - if err != nil { - return nil, fuse.ToStatus(err) - } - defer syscall.Close(fd) - cipherEntries, err = syscallcompat.Getdents(fd) - if err != nil { - return nil, fuse.ToStatus(err) - } - // Get DirIV (stays nil if PlaintextNames is used) - var cachedIV []byte - if !fs.args.PlaintextNames { - // Read the DirIV from disk - cachedIV, err = nametransform.ReadDirIVAt(fd) - if err != nil { - tlog.Warn.Printf("OpenDir %q: could not read %s: %v", cDirName, nametransform.DirIVFilename, err) - return nil, fuse.EIO - } - } - // Decrypted directory entries - var plain []fuse.DirEntry - // Filter and decrypt filenames - for i := range cipherEntries { - cName := cipherEntries[i].Name - if dirName == "" && cName == configfile.ConfDefaultName { - // silently ignore "gocryptfs.conf" in the top level dir - continue - } - if fs.args.PlaintextNames { - plain = append(plain, cipherEntries[i]) - continue - } - if cName == nametransform.DirIVFilename { - // silently ignore "gocryptfs.diriv" everywhere if dirIV is enabled - continue - } - // Handle long file name - isLong := nametransform.LongNameNone - if fs.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) - fs.reportMitigatedCorruption(cName) - continue - } - cName = cNameLong - } else if isLong == nametransform.LongNameFilename { - // ignore "gocryptfs.longname.*.name" - continue - } - name, err := fs.nameTransform.DecryptName(cName, cachedIV) - if err != nil { - tlog.Warn.Printf("OpenDir %q: invalid entry %q: %v", - cDirName, cName, err) - fs.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 plain, status -} diff --git a/internal/fusefrontend/node_dir_ops.go b/internal/fusefrontend/node_dir_ops.go index 066e791..a93271d 100644 --- a/internal/fusefrontend/node_dir_ops.go +++ b/internal/fusefrontend/node_dir_ops.go @@ -20,6 +20,18 @@ import ( "github.com/rfjakob/gocryptfs/internal/tlog" ) +const dsStoreName = ".DS_Store" + +// haveDsstore return true if one of the entries in "names" is ".DS_Store". +func haveDsstore(entries []fuse.DirEntry) bool { + for _, e := range entries { + if e.Name == dsStoreName { + return true + } + } + return false +} + // mkdirWithIv - create a new directory and corresponding diriv file. dirfd // should be a handle to the parent directory, cName is the name of the new // directory and mode specifies the access permissions to use. diff --git a/internal/fusefrontend/openbackingdir.go b/internal/fusefrontend/openbackingdir.go deleted file mode 100644 index e9e10db..0000000 --- a/internal/fusefrontend/openbackingdir.go +++ /dev/null @@ -1,84 +0,0 @@ -package fusefrontend - -import ( - "path/filepath" - "strings" - "syscall" - - "github.com/rfjakob/gocryptfs/internal/nametransform" - "github.com/rfjakob/gocryptfs/internal/syscallcompat" -) - -// openBackingDir opens the parent ciphertext directory of plaintext path -// "relPath". It returns the dirfd (opened with O_PATH) and the encrypted -// basename. -// -// The caller should then use Openat(dirfd, cName, ...) and friends. -// For convenience, if relPath is "", cName is going to be ".". -// -// openBackingDir is secure against symlink races by using Openat and -// ReadDirIVAt. -func (fs *FS) openBackingDir(relPath string) (dirfd int, cName string, err error) { - dirRelPath := nametransform.Dir(relPath) - // With PlaintextNames, we don't need to read DirIVs. Easy. - if fs.args.PlaintextNames { - dirfd, err = syscallcompat.OpenDirNofollow(fs.args.Cipherdir, dirRelPath) - if err != nil { - return -1, "", err - } - // If relPath is empty, cName is ".". - cName = filepath.Base(relPath) - return dirfd, cName, nil - } - // Cache lookup - dirfd, iv := fs.dirCache.Lookup(dirRelPath) - if dirfd > 0 { - // If relPath is empty, cName is ".". - if relPath == "" { - return dirfd, ".", nil - } - name := filepath.Base(relPath) - cName, err = fs.nameTransform.EncryptAndHashName(name, iv) - if err != nil { - syscall.Close(dirfd) - return -1, "", err - } - return dirfd, cName, nil - } - // Open cipherdir (following symlinks) - dirfd, err = syscall.Open(fs.args.Cipherdir, syscall.O_DIRECTORY|syscallcompat.O_PATH, 0) - if err != nil { - return -1, "", err - } - // If relPath is empty, cName is ".". - if relPath == "" { - return dirfd, ".", nil - } - // Walk the directory tree - parts := strings.Split(relPath, "/") - for i, name := range parts { - iv, err := nametransform.ReadDirIVAt(dirfd) - if err != nil { - syscall.Close(dirfd) - return -1, "", err - } - cName, err = fs.nameTransform.EncryptAndHashName(name, iv) - if err != nil { - syscall.Close(dirfd) - return -1, "", err - } - // Last part? We are done. - if i == len(parts)-1 { - fs.dirCache.Store(dirRelPath, dirfd, iv) - break - } - // Not the last part? Descend into next directory. - dirfd2, err := syscallcompat.Openat(dirfd, cName, syscall.O_NOFOLLOW|syscall.O_DIRECTORY|syscallcompat.O_PATH, 0) - syscall.Close(dirfd) - if err != nil { - return -1, "", err - } - dirfd = dirfd2 - } - return dirfd, cName, nil -} diff --git a/internal/fusefrontend/xattr.go b/internal/fusefrontend/xattr.go deleted file mode 100644 index 6638d83..0000000 --- a/internal/fusefrontend/xattr.go +++ /dev/null @@ -1,140 +0,0 @@ -// Package fusefrontend interfaces directly with the go-fuse library. -package fusefrontend - -import ( - "strings" - "syscall" - - "github.com/hanwen/go-fuse/v2/fuse" - - "github.com/rfjakob/gocryptfs/internal/tlog" -) - -const _EOPNOTSUPP = fuse.Status(syscall.EOPNOTSUPP) - -// GetXAttr - FUSE call. Reads the value of extended attribute "attr". -// -// This function is symlink-safe through Fgetxattr. -func (fs *FS) GetXAttr(relPath string, attr string, context *fuse.Context) ([]byte, fuse.Status) { - if fs.isFiltered(relPath) { - return nil, fuse.EPERM - } - cAttr := fs.encryptXattrName(attr) - - cData, status := fs.getXAttr(relPath, cAttr, context) - if !status.Ok() { - return nil, status - } - - data, err := fs.decryptXattrValue(cData) - if err != nil { - tlog.Warn.Printf("GetXAttr: %v", err) - return nil, fuse.EIO - } - return data, fuse.OK -} - -// SetXAttr - FUSE call. Set extended attribute. -// -// This function is symlink-safe through Fsetxattr. -func (fs *FS) SetXAttr(relPath string, attr string, data []byte, flags int, context *fuse.Context) fuse.Status { - if fs.isFiltered(relPath) { - return fuse.EPERM - } - flags = filterXattrSetFlags(flags) - cAttr := fs.encryptXattrName(attr) - cData := fs.encryptXattrValue(data) - return fs.setXAttr(relPath, cAttr, cData, flags, context) -} - -// RemoveXAttr - FUSE call. -// -// This function is symlink-safe through Fremovexattr. -func (fs *FS) RemoveXAttr(relPath string, attr string, context *fuse.Context) fuse.Status { - if fs.isFiltered(relPath) { - return fuse.EPERM - } - cAttr := fs.encryptXattrName(attr) - return fs.removeXAttr(relPath, cAttr, context) -} - -// ListXAttr - FUSE call. Lists extended attributes on the file at "relPath". -// -// This function is symlink-safe through Flistxattr. -func (fs *FS) ListXAttr(relPath string, context *fuse.Context) ([]string, fuse.Status) { - if fs.isFiltered(relPath) { - return nil, fuse.EPERM - } - - cNames, status := fs.listXAttr(relPath, context) - if !status.Ok() { - return nil, status - } - - names := make([]string, 0, len(cNames)) - for _, curName := range cNames { - if !strings.HasPrefix(curName, xattrStorePrefix) { - continue - } - name, err := fs.decryptXattrName(curName) - if err != nil { - tlog.Warn.Printf("ListXAttr: invalid xattr name %q: %v", curName, err) - fs.reportMitigatedCorruption(curName) - continue - } - names = append(names, name) - } - return names, fuse.OK -} - -// encryptXattrName transforms "user.foo" to "user.gocryptfs.a5sAd4XAa47f5as6dAf" -func (fs *FS) encryptXattrName(attr string) (cAttr string) { - // xattr names are encrypted like file names, but with a fixed IV. - cAttr = xattrStorePrefix + fs.nameTransform.EncryptName(attr, xattrNameIV) - return cAttr -} - -func (fs *FS) decryptXattrName(cAttr string) (attr string, err error) { - // Reject anything that does not start with "user.gocryptfs." - if !strings.HasPrefix(cAttr, xattrStorePrefix) { - return "", syscall.EINVAL - } - // Strip "user.gocryptfs." prefix - cAttr = cAttr[len(xattrStorePrefix):] - attr, err = fs.nameTransform.DecryptName(cAttr, xattrNameIV) - if err != nil { - return "", err - } - return attr, nil -} - -// encryptXattrValue encrypts the xattr value "data". -// The data is encrypted like a file content block, but without binding it to -// a file location (block number and file id are set to zero). -// Special case: an empty value is encrypted to an empty value. -func (fs *FS) encryptXattrValue(data []byte) (cData []byte) { - if len(data) == 0 { - return []byte{} - } - return fs.contentEnc.EncryptBlock(data, 0, nil) -} - -// decryptXattrValue decrypts the xattr value "cData". -func (fs *FS) decryptXattrValue(cData []byte) (data []byte, err error) { - if len(cData) == 0 { - return []byte{}, nil - } - data, err1 := fs.contentEnc.DecryptBlock([]byte(cData), 0, nil) - if err1 == nil { - return data, nil - } - // This backward compatibility is needed to support old - // file systems having xattr values base64-encoded. - cData, err2 := fs.nameTransform.B64DecodeString(string(cData)) - if err2 != nil { - // Looks like the value was not base64-encoded, but just corrupt. - // Return the original decryption error: err1 - return nil, err1 - } - return fs.contentEnc.DecryptBlock([]byte(cData), 0, nil) -} diff --git a/internal/fusefrontend/xattr_darwin.go b/internal/fusefrontend/xattr_darwin.go deleted file mode 100644 index 1d4ffcd..0000000 --- a/internal/fusefrontend/xattr_darwin.go +++ /dev/null @@ -1,90 +0,0 @@ -// +build darwin - -// Package fusefrontend interfaces directly with the go-fuse library. -package fusefrontend - -import ( - "syscall" - - "golang.org/x/sys/unix" - - "github.com/hanwen/go-fuse/v2/fuse" - - "github.com/rfjakob/gocryptfs/internal/syscallcompat" -) - -// On Darwin it is needed 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 - const XATTR_NOSECURITY = 0x0008 - - return flags &^ XATTR_NOSECURITY -} - -func (fs *FS) getXAttr(relPath string, cAttr string, context *fuse.Context) ([]byte, fuse.Status) { - // O_NONBLOCK to not block on FIFOs. - fd, err := fs.openBackingFile(relPath, syscall.O_RDONLY|syscall.O_NONBLOCK) - if err != nil { - return nil, fuse.ToStatus(err) - } - defer syscall.Close(fd) - - cData, err := syscallcompat.Fgetxattr(fd, cAttr) - if err != nil { - return nil, fuse.ToStatus(err) - } - - return cData, fuse.OK -} - -func (fs *FS) setXAttr(relPath string, cAttr string, cData []byte, flags int, context *fuse.Context) fuse.Status { - // O_NONBLOCK to not block on FIFOs. - fd, err := fs.openBackingFile(relPath, syscall.O_WRONLY|syscall.O_NONBLOCK) - // Directories cannot be opened read-write. Retry. - if err == syscall.EISDIR { - fd, err = fs.openBackingFile(relPath, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK) - } - if err != nil { - return fuse.ToStatus(err) - } - defer syscall.Close(fd) - - err = unix.Fsetxattr(fd, cAttr, cData, flags) - return fuse.ToStatus(err) -} - -func (fs *FS) removeXAttr(relPath string, cAttr string, context *fuse.Context) fuse.Status { - // O_NONBLOCK to not block on FIFOs. - fd, err := fs.openBackingFile(relPath, syscall.O_WRONLY|syscall.O_NONBLOCK) - // Directories cannot be opened read-write. Retry. - if err == syscall.EISDIR { - fd, err = fs.openBackingFile(relPath, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK) - } - if err != nil { - return fuse.ToStatus(err) - } - defer syscall.Close(fd) - - err = unix.Fremovexattr(fd, cAttr) - return fuse.ToStatus(err) -} - -func (fs *FS) listXAttr(relPath string, context *fuse.Context) ([]string, fuse.Status) { - // O_NONBLOCK to not block on FIFOs. - fd, err := fs.openBackingFile(relPath, syscall.O_RDONLY|syscall.O_NONBLOCK) - // On a symlink, openBackingFile fails with ELOOP. Let's pretend there - // can be no xattrs on symlinks, and always return an empty result. - if err == syscall.ELOOP { - return nil, fuse.OK - } - if err != nil { - return nil, fuse.ToStatus(err) - } - defer syscall.Close(fd) - - cNames, err := syscallcompat.Flistxattr(fd) - if err != nil { - return nil, fuse.ToStatus(err) - } - return cNames, fuse.OK -} diff --git a/internal/fusefrontend/xattr_linux.go b/internal/fusefrontend/xattr_linux.go deleted file mode 100644 index 5df0617..0000000 --- a/internal/fusefrontend/xattr_linux.go +++ /dev/null @@ -1,69 +0,0 @@ -// +build linux - -// Package fusefrontend interfaces directly with the go-fuse library. -package fusefrontend - -import ( - "fmt" - "syscall" - - "golang.org/x/sys/unix" - - "github.com/hanwen/go-fuse/v2/fuse" - - "github.com/rfjakob/gocryptfs/internal/syscallcompat" -) - -func (fs *FS) getXAttr(relPath string, cAttr string, context *fuse.Context) ([]byte, fuse.Status) { - dirfd, cName, err := fs.openBackingDir(relPath) - if err != nil { - return nil, fuse.ToStatus(err) - } - defer syscall.Close(dirfd) - - procPath := fmt.Sprintf("/proc/self/fd/%d/%s", dirfd, cName) - cData, err := syscallcompat.Lgetxattr(procPath, cAttr) - if err != nil { - return nil, fuse.ToStatus(err) - } - return cData, fuse.OK -} - -func (fs *FS) setXAttr(relPath string, cAttr string, cData []byte, flags int, context *fuse.Context) fuse.Status { - dirfd, cName, err := fs.openBackingDir(relPath) - if err != nil { - return fuse.ToStatus(err) - } - defer syscall.Close(dirfd) - - procPath := fmt.Sprintf("/proc/self/fd/%d/%s", dirfd, cName) - err = unix.Lsetxattr(procPath, cAttr, cData, flags) - return fuse.ToStatus(err) -} - -func (fs *FS) removeXAttr(relPath string, cAttr string, context *fuse.Context) fuse.Status { - dirfd, cName, err := fs.openBackingDir(relPath) - if err != nil { - return fuse.ToStatus(err) - } - defer syscall.Close(dirfd) - - procPath := fmt.Sprintf("/proc/self/fd/%d/%s", dirfd, cName) - err = unix.Lremovexattr(procPath, cAttr) - return fuse.ToStatus(err) -} - -func (fs *FS) listXAttr(relPath string, context *fuse.Context) ([]string, fuse.Status) { - dirfd, cName, err := fs.openBackingDir(relPath) - if err != nil { - return nil, fuse.ToStatus(err) - } - defer syscall.Close(dirfd) - - procPath := fmt.Sprintf("/proc/self/fd/%d/%s", dirfd, cName) - cNames, err := syscallcompat.Llistxattr(procPath) - if err != nil { - return nil, fuse.ToStatus(err) - } - return cNames, fuse.OK -} |