From ee5ab1cc29c6f5ff6061a88b742d5ff88de40802 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sun, 16 Aug 2020 12:48:16 +0200 Subject: v2api: rename "File2" to just "File" Rename the symbols and the files. --- internal/fusefrontend/file.go | 443 +++++++++++++++++++++++ internal/fusefrontend/file2.go | 443 ----------------------- internal/fusefrontend/file2_allocate_truncate.go | 218 ----------- internal/fusefrontend/file2_api_check.go | 23 -- internal/fusefrontend/file2_holes.go | 68 ---- internal/fusefrontend/file2_setattr.go | 85 ----- internal/fusefrontend/file_allocate_truncate.go | 218 +++++++++++ internal/fusefrontend/file_api_check.go | 23 ++ internal/fusefrontend/file_holes.go | 68 ++++ internal/fusefrontend/file_setattr.go | 85 +++++ internal/fusefrontend/node.go | 8 +- 11 files changed, 841 insertions(+), 841 deletions(-) create mode 100644 internal/fusefrontend/file.go delete mode 100644 internal/fusefrontend/file2.go delete mode 100644 internal/fusefrontend/file2_allocate_truncate.go delete mode 100644 internal/fusefrontend/file2_api_check.go delete mode 100644 internal/fusefrontend/file2_holes.go delete mode 100644 internal/fusefrontend/file2_setattr.go create mode 100644 internal/fusefrontend/file_allocate_truncate.go create mode 100644 internal/fusefrontend/file_api_check.go create mode 100644 internal/fusefrontend/file_holes.go create mode 100644 internal/fusefrontend/file_setattr.go diff --git a/internal/fusefrontend/file.go b/internal/fusefrontend/file.go new file mode 100644 index 0000000..66b3e24 --- /dev/null +++ b/internal/fusefrontend/file.go @@ -0,0 +1,443 @@ +package fusefrontend + +// FUSE operations on file handles + +import ( + "bytes" + "context" + "encoding/hex" + "fmt" + "io" + "log" + "os" + "sync" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + + "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" +) + +// File implements the go-fuse v2 API (github.com/hanwen/go-fuse/v2/fs) +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 + rootNode *RootNode +} + +// NewFile returns a new go-fuse File instance. +func NewFile(fd *os.File, rn *RootNode, st *syscall.Stat_t) *File { + qi := inomap.QInoFromStat(st) + e := openfiletable.Register(qi) + + return &File{ + fd: fd, + contentEnc: rn.contentEnc, + qIno: qi, + fileTableEntry: e, + rootNode: rn, + } +} + +// 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.rootNode.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.rootNode.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, syscall.Errno) { + // 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, 0 + } + 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, syscall.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.rootNode.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, fs.ToErrno(err) + } + // The ReadAt came back empty. We can skip all the decryption and return early. + if n == 0 { + f.rootNode.contentEnc.CReqPool.Put(ciphertext) + return dst, 0 + } + // 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.rootNode.contentEnc.CReqPool.Put(ciphertext) + if err != nil { + if f.rootNode.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, syscall.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.rootNode.contentEnc.PReqPool.Put(plaintext) + + return out, 0 +} + +// Read - FUSE call +func (f *File) Read(ctx context.Context, buf []byte, off int64) (resultData fuse.ReadResult, errno syscall.Errno) { + 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, 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.rootNode.args.SerializeReads { + serialize_reads.Wait(off, len(buf)) + } + out, errno := f.doRead(buf[:0], uint64(off), uint64(len(buf))) + if f.rootNode.args.SerializeReads { + serialize_reads.Done() + } + 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 +} + +// 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, syscall.Errno) { + 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, fs.ToErrno(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, errno := f.doRead(nil, b.BlockPlainOff(), f.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)) + 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.rootNode.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, fs.ToErrno(err) + } + } + // Write + _, err = f.fd.WriteAt(ciphertext, cOff) + // Return memory to CReqPool + f.rootNode.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, fs.ToErrno(err) + } + return uint32(len(data)), 0 +} + +// 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(ctx context.Context, data []byte, off int64) (uint32, syscall.Errno) { + 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, 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, syscall.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) { + errno := f.writePadHole(off) + if errno != 0 { + return 0, errno + } + } + n, errno := f.doWrite(data, off) + if errno != 0 { + f.lastOpCount = openfiletable.WriteOpCount() + f.lastWrittenOffset = off + int64(len(data)) - 1 + } + return n, errno +} + +// Release - FUSE call, close file +func (f *File) Release(ctx context.Context) syscall.Errno { + 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() + return 0 +} + +// Flush - FUSE call +func (f *File) Flush(ctx context.Context) syscall.Errno { + 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 fs.ToErrno(err) + } + err = syscall.Close(newFd) + return fs.ToErrno(err) +} + +// Fsync FUSE call +func (f *File) Fsync(ctx context.Context, flags uint32) (errno syscall.Errno) { + f.fdLock.RLock() + defer f.fdLock.RUnlock() + + return fs.ToErrno(syscall.Fsync(f.intFd())) +} + +// Getattr FUSE call (like stat) +func (f *File) Getattr(ctx context.Context, a *fuse.AttrOut) syscall.Errno { + 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 fs.ToErrno(err) + } + f.rootNode.inoMap.TranslateStat(&st) + a.FromStat(&st) + a.Size = f.contentEnc.CipherSizeToPlainSize(a.Size) + if f.rootNode.args.ForceOwner != nil { + a.Owner = *f.rootNode.args.ForceOwner + } + + return 0 +} diff --git a/internal/fusefrontend/file2.go b/internal/fusefrontend/file2.go deleted file mode 100644 index 0de325c..0000000 --- a/internal/fusefrontend/file2.go +++ /dev/null @@ -1,443 +0,0 @@ -package fusefrontend - -// FUSE operations on file handles - -import ( - "bytes" - "context" - "encoding/hex" - "fmt" - "io" - "log" - "os" - "sync" - "syscall" - - "github.com/hanwen/go-fuse/v2/fs" - "github.com/hanwen/go-fuse/v2/fuse" - - "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" -) - -// File2 implements the go-fuse v2 API (github.com/hanwen/go-fuse/v2/fs) -type File2 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 - rootNode *RootNode -} - -// NewFile returns a new go-fuse File instance. -func NewFile2(fd *os.File, rn *RootNode, st *syscall.Stat_t) *File2 { - qi := inomap.QInoFromStat(st) - e := openfiletable.Register(qi) - - return &File2{ - fd: fd, - contentEnc: rn.contentEnc, - qIno: qi, - fileTableEntry: e, - rootNode: rn, - } -} - -// intFd - return the backing file descriptor as an integer. -func (f *File2) 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 *File2) 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.rootNode.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 *File2) 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 { - 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 *File2) doRead(dst []byte, off uint64, length uint64) ([]byte, syscall.Errno) { - // 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, 0 - } - 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, syscall.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.rootNode.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, fs.ToErrno(err) - } - // The ReadAt came back empty. We can skip all the decryption and return early. - if n == 0 { - f.rootNode.contentEnc.CReqPool.Put(ciphertext) - return dst, 0 - } - // 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.rootNode.contentEnc.CReqPool.Put(ciphertext) - if err != nil { - if f.rootNode.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, syscall.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.rootNode.contentEnc.PReqPool.Put(plaintext) - - return out, 0 -} - -// Read - FUSE call -func (f *File2) Read(ctx context.Context, buf []byte, off int64) (resultData fuse.ReadResult, errno syscall.Errno) { - 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, 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.rootNode.args.SerializeReads { - serialize_reads.Wait(off, len(buf)) - } - out, errno := f.doRead(buf[:0], uint64(off), uint64(len(buf))) - if f.rootNode.args.SerializeReads { - serialize_reads.Done() - } - 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 -} - -// 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 *File2) doWrite(data []byte, off int64) (uint32, syscall.Errno) { - 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, fs.ToErrno(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, errno := f.doRead(nil, b.BlockPlainOff(), f.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)) - 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.rootNode.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, fs.ToErrno(err) - } - } - // Write - _, err = f.fd.WriteAt(ciphertext, cOff) - // Return memory to CReqPool - f.rootNode.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, fs.ToErrno(err) - } - return uint32(len(data)), 0 -} - -// 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 *File2) 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 *File2) Write(ctx context.Context, data []byte, off int64) (uint32, syscall.Errno) { - 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, 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, syscall.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) { - errno := f.writePadHole(off) - if errno != 0 { - return 0, errno - } - } - n, errno := f.doWrite(data, off) - if errno != 0 { - f.lastOpCount = openfiletable.WriteOpCount() - f.lastWrittenOffset = off + int64(len(data)) - 1 - } - return n, errno -} - -// Release - FUSE call, close file -func (f *File2) Release(ctx context.Context) syscall.Errno { - 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() - return 0 -} - -// Flush - FUSE call -func (f *File2) Flush(ctx context.Context) syscall.Errno { - 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 fs.ToErrno(err) - } - err = syscall.Close(newFd) - return fs.ToErrno(err) -} - -// Fsync FUSE call -func (f *File2) Fsync(ctx context.Context, flags uint32) (errno syscall.Errno) { - f.fdLock.RLock() - defer f.fdLock.RUnlock() - - return fs.ToErrno(syscall.Fsync(f.intFd())) -} - -// Getattr FUSE call (like stat) -func (f *File2) Getattr(ctx context.Context, a *fuse.AttrOut) syscall.Errno { - 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 fs.ToErrno(err) - } - f.rootNode.inoMap.TranslateStat(&st) - a.FromStat(&st) - a.Size = f.contentEnc.CipherSizeToPlainSize(a.Size) - if f.rootNode.args.ForceOwner != nil { - a.Owner = *f.rootNode.args.ForceOwner - } - - return 0 -} diff --git a/internal/fusefrontend/file2_allocate_truncate.go b/internal/fusefrontend/file2_allocate_truncate.go deleted file mode 100644 index cdda974..0000000 --- a/internal/fusefrontend/file2_allocate_truncate.go +++ /dev/null @@ -1,218 +0,0 @@ -package fusefrontend - -// FUSE operations Truncate and Allocate on file handles -// i.e. ftruncate and fallocate - -import ( - "context" - "log" - "sync" - "syscall" - - "github.com/hanwen/go-fuse/v2/fs" - - "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 *File2) Allocate(ctx context.Context, off uint64, sz uint64, mode uint32) syscall.Errno { - 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 syscall.EOPNOTSUPP - } - - f.fdLock.RLock() - defer f.fdLock.RUnlock() - if f.released { - return syscall.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 fs.ToErrno(err) - } - if mode == FALLOC_FL_KEEP_SIZE { - // The user did not want to change the apparent size. We are done. - return 0 - } - // 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 fs.ToErrno(err) - } - if newPlainSz <= oldPlainSz { - // The new size is smaller (or equal). Fallocate with mode = 0 never - // truncates a file, so we are done. - return 0 - } - // 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 - called from Setattr. -func (f *File2) 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) - } - // 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() - if err != nil { - return fs.ToErrno(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 0 - } - // 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 { - data, errno = f.doRead(nil, plainOff, lastBlockLen) - if errno != 0 { - tlog.Warn.Printf("Truncate: shrink doRead returned error: %v", err) - return errno - } - } - // 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 fs.ToErrno(err) - } - // Append partial block - if lastBlockLen > 0 { - _, status := f.doWrite(data, int64(plainOff)) - return status - } - return 0 -} - -// statPlainSize stats the file and returns the plaintext size -func (f *File2) 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 *File2) truncateGrowFile(oldPlainSz uint64, newPlainSz uint64) syscall.Errno { - 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) - _, errno := f.doWrite(buf, int64(newEOFOffset)) - return errno - } - } - // 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. - errno := f.zeroPad(oldPlainSz) - if errno != 0 { - return errno - } - // 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 fs.ToErrno(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 fs.ToErrno(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) - _, errno = f.doWrite(buf, int64(newEOFOffset)) - return errno -} diff --git a/internal/fusefrontend/file2_api_check.go b/internal/fusefrontend/file2_api_check.go deleted file mode 100644 index 01f9a46..0000000 --- a/internal/fusefrontend/file2_api_check.go +++ /dev/null @@ -1,23 +0,0 @@ -package fusefrontend - -import ( - "github.com/hanwen/go-fuse/v2/fs" -) - -// Check that we have implemented the fs.File* interfaces -var _ = (fs.FileGetattrer)((*File2)(nil)) -var _ = (fs.FileSetattrer)((*File2)(nil)) -var _ = (fs.FileReleaser)((*File2)(nil)) -var _ = (fs.FileReader)((*File2)(nil)) -var _ = (fs.FileWriter)((*File2)(nil)) -var _ = (fs.FileFsyncer)((*File2)(nil)) -var _ = (fs.FileFlusher)((*File2)(nil)) -var _ = (fs.FileAllocater)((*File2)(nil)) -var _ = (fs.FileLseeker)((*File2)(nil)) - -/* TODO -var _ = (fs.FileHandle)((*File2)(nil)) -var _ = (fs.FileGetlker)((*File2)(nil)) -var _ = (fs.FileSetlker)((*File2)(nil)) -var _ = (fs.FileSetlkwer)((*File2)(nil)) -*/ diff --git a/internal/fusefrontend/file2_holes.go b/internal/fusefrontend/file2_holes.go deleted file mode 100644 index 5c314d3..0000000 --- a/internal/fusefrontend/file2_holes.go +++ /dev/null @@ -1,68 +0,0 @@ -package fusefrontend - -// Helper functions for sparse files (files with holes) - -import ( - "context" - "syscall" - - "github.com/hanwen/go-fuse/v2/fs" - - "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 *File2) writePadHole(targetOff int64) syscall.Errno { - // Get the current file size. - fi, err := f.fd.Stat() - if err != nil { - tlog.Warn.Printf("checkAndPadHole: Fstat failed: %v", err) - return fs.ToErrno(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 0 - } - // 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. - errno := f.zeroPad(plainSize) - if errno != 0 { - return errno - } - return 0 -} - -// 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 *File2) zeroPad(plainSize uint64) syscall.Errno { - lastBlockLen := plainSize % f.contentEnc.PlainBS() - if lastBlockLen == 0 { - // Already block-aligned - return 0 - } - missing := f.contentEnc.PlainBS() - lastBlockLen - pad := make([]byte, missing) - tlog.Debug.Printf("zeroPad: Writing %d bytes\n", missing) - _, errno := f.doWrite(pad, int64(plainSize)) - return errno -} - -// Lseek - FUSE call. -func (f *File2) Lseek(ctx context.Context, off uint64, whence uint32) (uint64, syscall.Errno) { - cipherOff := f.rootNode.contentEnc.PlainSizeToCipherSize(off) - newCipherOff, err := syscall.Seek(f.intFd(), int64(cipherOff), int(whence)) - if err != nil { - return uint64(newCipherOff), fs.ToErrno(err) - } - newOff := f.contentEnc.CipherSizeToPlainSize(uint64(newCipherOff)) - return newOff, 0 -} diff --git a/internal/fusefrontend/file2_setattr.go b/internal/fusefrontend/file2_setattr.go deleted file mode 100644 index 1385f3f..0000000 --- a/internal/fusefrontend/file2_setattr.go +++ /dev/null @@ -1,85 +0,0 @@ -package fusefrontend - -import ( - "context" - "syscall" - - "github.com/hanwen/go-fuse/v2/fs" - "github.com/hanwen/go-fuse/v2/fuse" - - "github.com/rfjakob/gocryptfs/internal/syscallcompat" - "github.com/rfjakob/gocryptfs/internal/tlog" -) - -func (f *File2) Setattr(ctx context.Context, in *fuse.SetAttrIn, out *fuse.AttrOut) (errno syscall.Errno) { - errno = f.setAttr(ctx, in) - if errno != 0 { - return errno - } - return f.Getattr(ctx, out) -} - -func (f *File2) setAttr(ctx context.Context, in *fuse.SetAttrIn) (errno syscall.Errno) { - f.fdLock.RLock() - defer f.fdLock.RUnlock() - if f.released { - tlog.Warn.Printf("ino%d fh%d: Truncate on released file", f.qIno.Ino, f.intFd()) - return syscall.EBADF - } - f.fileTableEntry.ContentLock.Lock() - defer f.fileTableEntry.ContentLock.Unlock() - - // fchmod(2) - if mode, ok := in.GetMode(); ok { - errno = fs.ToErrno(syscall.Fchmod(f.intFd(), mode)) - if errno != 0 { - return errno - } - } - - // fchown(2) - uid32, uOk := in.GetUID() - gid32, gOk := in.GetGID() - if uOk || gOk { - uid := -1 - gid := -1 - - if uOk { - uid = int(uid32) - } - if gOk { - gid = int(gid32) - } - errno = fs.ToErrno(syscall.Fchown(f.intFd(), uid, gid)) - if errno != 0 { - return errno - } - } - - // utimens(2) - mtime, mok := in.GetMTime() - atime, aok := in.GetATime() - if mok || aok { - ap := &atime - mp := &mtime - if !aok { - ap = nil - } - if !mok { - mp = nil - } - errno = fs.ToErrno(syscallcompat.FutimesNano(f.intFd(), ap, mp)) - if errno != 0 { - return errno - } - } - - // truncate(2) - if sz, ok := in.GetSize(); ok { - errno = syscall.Errno(f.truncate(sz)) - if errno != 0 { - return errno - } - } - return 0 -} diff --git a/internal/fusefrontend/file_allocate_truncate.go b/internal/fusefrontend/file_allocate_truncate.go new file mode 100644 index 0000000..f4e6099 --- /dev/null +++ b/internal/fusefrontend/file_allocate_truncate.go @@ -0,0 +1,218 @@ +package fusefrontend + +// FUSE operations Truncate and Allocate on file handles +// i.e. ftruncate and fallocate + +import ( + "context" + "log" + "sync" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + + "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(ctx context.Context, off uint64, sz uint64, mode uint32) syscall.Errno { + 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 syscall.EOPNOTSUPP + } + + f.fdLock.RLock() + defer f.fdLock.RUnlock() + if f.released { + return syscall.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 fs.ToErrno(err) + } + if mode == FALLOC_FL_KEEP_SIZE { + // The user did not want to change the apparent size. We are done. + return 0 + } + // 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 fs.ToErrno(err) + } + if newPlainSz <= oldPlainSz { + // The new size is smaller (or equal). Fallocate with mode = 0 never + // truncates a file, so we are done. + return 0 + } + // 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 - called from Setattr. +func (f *File) truncate(newSize uint64) (errno syscall.Errno) { + var err error + // Common case first: Truncate to zero + if newSize == 0 { + err = syscall.Ftruncate(int(f.fd.Fd()), 0) + if err != nil { + tlog.Warn.Printf("ino%d fh%d: Ftruncate(fd, 0) returned error: %v", f.qIno.Ino, f.intFd(), err) + return fs.ToErrno(err) + } + // 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() + if err != nil { + return fs.ToErrno(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 0 + } + // 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 { + data, errno = f.doRead(nil, plainOff, lastBlockLen) + if errno != 0 { + tlog.Warn.Printf("Truncate: shrink doRead returned error: %v", err) + return errno + } + } + // 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 fs.ToErrno(err) + } + // Append partial block + if lastBlockLen > 0 { + _, status := f.doWrite(data, int64(plainOff)) + return status + } + return 0 +} + +// 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) syscall.Errno { + 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) + _, errno := f.doWrite(buf, int64(newEOFOffset)) + return errno + } + } + // 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. + errno := f.zeroPad(oldPlainSz) + if errno != 0 { + return errno + } + // 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 fs.ToErrno(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 fs.ToErrno(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) + _, errno = f.doWrite(buf, int64(newEOFOffset)) + return errno +} diff --git a/internal/fusefrontend/file_api_check.go b/internal/fusefrontend/file_api_check.go new file mode 100644 index 0000000..4761a4c --- /dev/null +++ b/internal/fusefrontend/file_api_check.go @@ -0,0 +1,23 @@ +package fusefrontend + +import ( + "github.com/hanwen/go-fuse/v2/fs" +) + +// Check that we have implemented the fs.File* interfaces +var _ = (fs.FileGetattrer)((*File)(nil)) +var _ = (fs.FileSetattrer)((*File)(nil)) +var _ = (fs.FileReleaser)((*File)(nil)) +var _ = (fs.FileReader)((*File)(nil)) +var _ = (fs.FileWriter)((*File)(nil)) +var _ = (fs.FileFsyncer)((*File)(nil)) +var _ = (fs.FileFlusher)((*File)(nil)) +var _ = (fs.FileAllocater)((*File)(nil)) +var _ = (fs.FileLseeker)((*File)(nil)) + +/* TODO +var _ = (fs.FileHandle)((*File)(nil)) +var _ = (fs.FileGetlker)((*File)(nil)) +var _ = (fs.FileSetlker)((*File)(nil)) +var _ = (fs.FileSetlkwer)((*File)(nil)) +*/ diff --git a/internal/fusefrontend/file_holes.go b/internal/fusefrontend/file_holes.go new file mode 100644 index 0000000..85792f9 --- /dev/null +++ b/internal/fusefrontend/file_holes.go @@ -0,0 +1,68 @@ +package fusefrontend + +// Helper functions for sparse files (files with holes) + +import ( + "context" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + + "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) syscall.Errno { + // Get the current file size. + fi, err := f.fd.Stat() + if err != nil { + tlog.Warn.Printf("checkAndPadHole: Fstat failed: %v", err) + return fs.ToErrno(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 0 + } + // 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. + errno := f.zeroPad(plainSize) + if errno != 0 { + return errno + } + return 0 +} + +// 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() + if lastBlockLen == 0 { + // Already block-aligned + return 0 + } + missing := f.contentEnc.PlainBS() - lastBlockLen + pad := make([]byte, missing) + tlog.Debug.Printf("zeroPad: Writing %d bytes\n", missing) + _, errno := f.doWrite(pad, int64(plainSize)) + return errno +} + +// Lseek - FUSE call. +func (f *File) Lseek(ctx context.Context, off uint64, whence uint32) (uint64, syscall.Errno) { + cipherOff := f.rootNode.contentEnc.PlainSizeToCipherSize(off) + newCipherOff, err := syscall.Seek(f.intFd(), int64(cipherOff), int(whence)) + if err != nil { + return uint64(newCipherOff), fs.ToErrno(err) + } + newOff := f.contentEnc.CipherSizeToPlainSize(uint64(newCipherOff)) + return newOff, 0 +} diff --git a/internal/fusefrontend/file_setattr.go b/internal/fusefrontend/file_setattr.go new file mode 100644 index 0000000..0d6dc48 --- /dev/null +++ b/internal/fusefrontend/file_setattr.go @@ -0,0 +1,85 @@ +package fusefrontend + +import ( + "context" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + + "github.com/rfjakob/gocryptfs/internal/syscallcompat" + "github.com/rfjakob/gocryptfs/internal/tlog" +) + +func (f *File) Setattr(ctx context.Context, in *fuse.SetAttrIn, out *fuse.AttrOut) (errno syscall.Errno) { + errno = f.setAttr(ctx, in) + if errno != 0 { + return errno + } + return f.Getattr(ctx, out) +} + +func (f *File) setAttr(ctx context.Context, in *fuse.SetAttrIn) (errno syscall.Errno) { + f.fdLock.RLock() + defer f.fdLock.RUnlock() + if f.released { + tlog.Warn.Printf("ino%d fh%d: Truncate on released file", f.qIno.Ino, f.intFd()) + return syscall.EBADF + } + f.fileTableEntry.ContentLock.Lock() + defer f.fileTableEntry.ContentLock.Unlock() + + // fchmod(2) + if mode, ok := in.GetMode(); ok { + errno = fs.ToErrno(syscall.Fchmod(f.intFd(), mode)) + if errno != 0 { + return errno + } + } + + // fchown(2) + uid32, uOk := in.GetUID() + gid32, gOk := in.GetGID() + if uOk || gOk { + uid := -1 + gid := -1 + + if uOk { + uid = int(uid32) + } + if gOk { + gid = int(gid32) + } + errno = fs.ToErrno(syscall.Fchown(f.intFd(), uid, gid)) + if errno != 0 { + return errno + } + } + + // utimens(2) + mtime, mok := in.GetMTime() + atime, aok := in.GetATime() + if mok || aok { + ap := &atime + mp := &mtime + if !aok { + ap = nil + } + if !mok { + mp = nil + } + errno = fs.ToErrno(syscallcompat.FutimesNano(f.intFd(), ap, mp)) + if errno != 0 { + return errno + } + } + + // truncate(2) + if sz, ok := in.GetSize(); ok { + errno = syscall.Errno(f.truncate(sz)) + if errno != 0 { + return errno + } + } + return 0 +} diff --git a/internal/fusefrontend/node.go b/internal/fusefrontend/node.go index fb38362..00d06f5 100644 --- a/internal/fusefrontend/node.go +++ b/internal/fusefrontend/node.go @@ -133,7 +133,7 @@ func (n *Node) Create(ctx context.Context, name string, flags uint32, mode uint3 ch := n.newChild(ctx, &st, out) f := os.NewFile(uintptr(fd), cName) - return ch, NewFile2(f, rn, &st), 0, 0 + return ch, NewFile(f, rn, &st), 0, 0 } // Unlink - FUSE call. Delete a file. @@ -216,7 +216,7 @@ func (n *Node) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFl return } f := os.NewFile(uintptr(fd), cName) - fh = NewFile2(f, rn, &st) + fh = NewFile(f, rn, &st) return } @@ -224,7 +224,7 @@ func (n *Node) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFl func (n *Node) Setattr(ctx context.Context, f fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) (errno syscall.Errno) { // Use the fd if the kernel gave us one if f != nil { - f2 := f.(*File2) + f2 := f.(*File) return f2.Setattr(ctx, in, out) } @@ -286,7 +286,7 @@ func (n *Node) Setattr(ctx context.Context, f fs.FileHandle, in *fuse.SetAttrIn, if errno != 0 { return errno } - f2 := f.(*File2) + f2 := f.(*File) defer f2.Release(ctx) errno = syscall.Errno(f2.truncate(sz)) if errno != 0 { -- cgit v1.2.3