diff options
| author | Jakob Unterwurzacher | 2020-07-26 18:35:12 +0200 | 
|---|---|---|
| committer | Jakob Unterwurzacher | 2020-07-26 18:35:12 +0200 | 
| commit | 777b95f82ffea8a25b95089343b07b29378110da (patch) | |
| tree | 641adf67d3969a2b87c52ef62b384f022b7362d7 | |
| parent | 81fb42b9124e0d8e59d67ff2072d4388ce42ff77 (diff) | |
v2api: delete (most) fusefrontend v1 files
All the functionality in these files has been reimplemented
for the v2 api. Drop the old files.
| -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 -} | 
