diff options
| author | Jakob Unterwurzacher | 2019-01-02 22:32:21 +0100 | 
|---|---|---|
| committer | Jakob Unterwurzacher | 2019-01-03 15:31:13 +0100 | 
| commit | 4f66d66755da63c78b09201c6c72353009251cf2 (patch) | |
| tree | 9eb4e937419e7255c56df04d3c30d36185e1a337 /internal | |
| parent | f6dad8d0fae25b5d88ad036b841fea10b7296ccb (diff) | |
fusefrontend: add dirCache
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/fusefrontend/dircache.go | 117 | ||||
| -rw-r--r-- | internal/fusefrontend/fs.go | 6 | ||||
| -rw-r--r-- | internal/fusefrontend/fs_dir.go | 8 | ||||
| -rw-r--r-- | internal/fusefrontend/openbackingdir.go | 15 | ||||
| -rw-r--r-- | internal/fusefrontend/openbackingdir_test.go | 7 | ||||
| -rw-r--r-- | internal/nametransform/dirivcache/dirivcache.go | 102 | ||||
| -rw-r--r-- | internal/nametransform/names.go | 6 | ||||
| -rw-r--r-- | internal/syscallcompat/open_nofollow.go | 1 | 
8 files changed, 145 insertions, 117 deletions
| diff --git a/internal/fusefrontend/dircache.go b/internal/fusefrontend/dircache.go new file mode 100644 index 0000000..5ae6d6b --- /dev/null +++ b/internal/fusefrontend/dircache.go @@ -0,0 +1,117 @@ +package fusefrontend + +import ( +	"fmt" +	"log" +	"sync" +	"syscall" +	"time" + +	"github.com/rfjakob/gocryptfs/internal/nametransform" +	"github.com/rfjakob/gocryptfs/internal/tlog" +) + +type dirCacheStruct struct { +	sync.Mutex +	// relative plaintext path to the directory +	dirRelPath string +	// fd to the directory (opened with O_PATH!) +	fd int +	// content of gocryptfs.diriv in this directory +	iv []byte +	// on the first Lookup(), the expire thread is stared, and this is set +	// to true. +	expireThreadRunning bool +} + +// Clear clears the cache contents. +func (d *dirCacheStruct) Clear() { +	d.Lock() +	defer d.Unlock() +	d.doClear() +} + +// doClear closes the fd and clears the cache contents. +// Caller must hold d.Lock()! +func (d *dirCacheStruct) doClear() { +	// An earlier clear may have already closed the fd, or the cache +	// has never been filled (fd is 0 in that case). +	if d.fd > 0 { +		err := syscall.Close(d.fd) +		if err != nil { +			tlog.Warn.Printf("dirCache.Clear: Close failed: %v", err) +		} +	} +	d.fd = -1 +	d.dirRelPath = "" +	d.iv = nil +} + +// Store the entry in the cache. The passed "fd" will be Dup()ed, and the caller +// can close their copy at will. +func (d *dirCacheStruct) Store(dirRelPath string, fd int, iv []byte) { +	if fd <= 0 || len(iv) != nametransform.DirIVLen { +		log.Panicf("Store sanity check failed: fd=%d len=%d", fd, len(iv)) +	} +	d.Lock() +	defer d.Unlock() +	// Close the old fd +	d.doClear() +	fd2, err := syscall.Dup(fd) +	if err != nil { +		tlog.Warn.Printf("dirCache.Store: Dup failed: %v", err) +		return +	} +	d.fd = fd2 +	d.dbg("Store: %q %d %x\n", dirRelPath, fd2, iv) +	d.dirRelPath = dirRelPath +	d.iv = iv +	// expireThread is started on the first Lookup() +	if !d.expireThreadRunning { +		d.expireThreadRunning = true +		go d.expireThread() +	} +} + +// Lookup checks if relPath is in the cache, and returns and (fd, iv) pair. +// It returns (-1, nil) if not found. The fd is internally Dup()ed and the +// caller must close it when done. +func (d *dirCacheStruct) Lookup(dirRelPath string) (fd int, iv []byte) { +	d.Lock() +	defer d.Unlock() +	if d.fd <= 0 { +		// Cache is empty +		d.dbg("Lookup %q: empty\n", dirRelPath) +		return -1, nil +	} +	if dirRelPath != d.dirRelPath { +		d.dbg("Lookup %q: miss\n", dirRelPath) +		return -1, nil +	} +	fd, err := syscall.Dup(d.fd) +	if err != nil { +		tlog.Warn.Printf("dirCache.Lookup: Dup failed: %v", err) +		return -1, nil +	} +	if fd <= 0 || len(d.iv) != nametransform.DirIVLen { +		log.Panicf("Lookup sanity check failed: fd=%d len=%d", fd, len(d.iv)) +	} +	d.dbg("Lookup %q: hit %d %x\n", dirRelPath, fd, d.iv) +	return fd, d.iv +} + +// expireThread is started on the first Lookup() +func (d *dirCacheStruct) expireThread() { +	for { +		time.Sleep(1 * time.Second) +		d.Clear() +	} +} + +// dbg prints a debug message. Usually disabled. +func (d *dirCacheStruct) dbg(format string, a ...interface{}) { +	const EnableDebugMessages = false +	if EnableDebugMessages { +		fmt.Printf(format, a...) +	} +} diff --git a/internal/fusefrontend/fs.go b/internal/fusefrontend/fs.go index 5adade6..c0a275f 100644 --- a/internal/fusefrontend/fs.go +++ b/internal/fusefrontend/fs.go @@ -53,6 +53,8 @@ type FS struct {  	// which is called as part of every filesystem operation.  	// (This flag uses a uint32 so that it can be reset with CompareAndSwapUint32.)  	AccessedSinceLastCheck uint32 + +	dirCache dirCacheStruct  }  var _ pathfs.FileSystem = &FS{} // Verify that interface is implemented. @@ -533,6 +535,7 @@ func (fs *FS) Symlink(target string, linkName string, context *fuse.Context) (co  //  // 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  	} @@ -546,9 +549,6 @@ func (fs *FS) Rename(oldPath string, newPath string, context *fuse.Context) (cod  		return fuse.ToStatus(err)  	}  	defer syscall.Close(newDirfd) -	// The Rename may cause a directory to take the place of another directory. -	// That directory may still be in the DirIV cache, clear it. -	fs.nameTransform.DirIVCache.Clear()  	// Easy case.  	if fs.args.PlaintextNames {  		return fuse.ToStatus(syscallcompat.Renameat(oldDirfd, oldCName, newDirfd, newCName)) diff --git a/internal/fusefrontend/fs_dir.go b/internal/fusefrontend/fs_dir.go index 302fe38..3c71e45 100644 --- a/internal/fusefrontend/fs_dir.go +++ b/internal/fusefrontend/fs_dir.go @@ -30,8 +30,6 @@ func (fs *FS) mkdirWithIv(dirfd int, cName string, mode uint32) error {  	// the directory is inconsistent. Take the lock to prevent other readers  	// from seeing it.  	fs.dirIVLock.Lock() -	// The new directory may take the place of an older one that is still in the cache -	fs.nameTransform.DirIVCache.Clear()  	defer fs.dirIVLock.Unlock()  	err := syscallcompat.Mkdirat(dirfd, cName, mode)  	if err != nil { @@ -57,6 +55,7 @@ func (fs *FS) mkdirWithIv(dirfd int, cName string, mode uint32) error {  //  // Symlink-safe through use of Mkdirat().  func (fs *FS) Mkdir(newPath string, mode uint32, context *fuse.Context) (code fuse.Status) { +	defer fs.dirCache.Clear()  	if fs.isFiltered(newPath) {  		return fuse.EPERM  	} @@ -142,6 +141,7 @@ func haveDsstore(entries []fuse.DirEntry) bool {  //  // 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) @@ -252,8 +252,8 @@ retry:  	if nametransform.IsLongContent(cName) {  		nametransform.DeleteLongNameAt(parentDirFd, cName)  	} -	// The now-deleted directory may have been in the DirIV cache. Clear it. -	fs.nameTransform.DirIVCache.Clear() +	// The now-deleted directory may have been in the dirCache. Clear it. +	fs.dirCache.Clear()  	return fuse.OK  } diff --git a/internal/fusefrontend/openbackingdir.go b/internal/fusefrontend/openbackingdir.go index 849a486..4da7fd6 100644 --- a/internal/fusefrontend/openbackingdir.go +++ b/internal/fusefrontend/openbackingdir.go @@ -10,7 +10,8 @@ import (  )  // openBackingDir opens the parent ciphertext directory of plaintext path -// "relPath" and returns the dirfd and the encrypted basename. +// "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 ".". @@ -18,10 +19,10 @@ import (  // 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 { -		dir := nametransform.Dir(relPath) -		dirfd, err = syscallcompat.OpenDirNofollow(fs.args.Cipherdir, dir) +		dirfd, err = syscallcompat.OpenDirNofollow(fs.args.Cipherdir, dirRelPath)  		if err != nil {  			return -1, "", err  		} @@ -29,6 +30,13 @@ func (fs *FS) openBackingDir(relPath string) (dirfd int, cName string, err error  		cName = filepath.Base(relPath)  		return dirfd, cName, nil  	} +	// Cache lookup +	dirfd, iv := fs.dirCache.Lookup(dirRelPath) +	if dirfd > 0 { +		name := filepath.Base(relPath) +		cName = fs.nameTransform.EncryptAndHashName(name, iv) +		return dirfd, cName, nil +	}  	// Open cipherdir (following symlinks)  	dirfd, err = syscall.Open(fs.args.Cipherdir, syscall.O_RDONLY|syscall.O_DIRECTORY|syscallcompat.O_PATH, 0)  	if err != nil { @@ -49,6 +57,7 @@ func (fs *FS) openBackingDir(relPath string) (dirfd int, cName string, err error  		cName = fs.nameTransform.EncryptAndHashName(name, iv)  		// 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. diff --git a/internal/fusefrontend/openbackingdir_test.go b/internal/fusefrontend/openbackingdir_test.go index 8453e52..f784989 100644 --- a/internal/fusefrontend/openbackingdir_test.go +++ b/internal/fusefrontend/openbackingdir_test.go @@ -1,9 +1,12 @@  package fusefrontend  import ( +	"fmt" +	"os"  	"strings"  	"syscall"  	"testing" +	"time"  	"golang.org/x/sys/unix" @@ -53,7 +56,9 @@ func TestOpenBackingDir(t *testing.T) {  	}  	err = syscallcompat.Faccessat(dirfd, cName, unix.R_OK)  	if err != nil { -		t.Error(err) +		fmt.Printf("pid=%d dirfd=%d dir1->cName=%q: %v\n", os.Getpid(), dirfd, cName, err) +		time.Sleep(600 * time.Second) +		t.Errorf("dirfd=%d cName=%q: %v", dirfd, cName, err)  	}  	syscall.Close(dirfd) diff --git a/internal/nametransform/dirivcache/dirivcache.go b/internal/nametransform/dirivcache/dirivcache.go deleted file mode 100644 index 962ae37..0000000 --- a/internal/nametransform/dirivcache/dirivcache.go +++ /dev/null @@ -1,102 +0,0 @@ -package dirivcache - -import ( -	"log" -	"strings" -	"sync" -	"time" -) - -const ( -	maxEntries = 100 -	expireTime = 1 * time.Second -) - -type cacheEntry struct { -	// DirIV of the directory. -	iv []byte -	// Relative ciphertext path of the directory. -	cDir string -} - -// DirIVCache stores up to "maxEntries" directory IVs. -type DirIVCache struct { -	// data in the cache, indexed by relative plaintext path -	// of the directory. -	data map[string]cacheEntry - -	// The DirIV of the root directory gets special treatment because it -	// cannot change (the root directory cannot be renamed or deleted). -	// It is unaffected by the expiry timer and cache clears. -	rootDirIV []byte - -	// expiry is the time when the whole cache expires. -	// The cached entry might become out-of-date if the ciphertext directory is -	// modified behind the back of gocryptfs. Having an expiry time limits the -	// inconstancy to one second, like attr_timeout does for the kernel -	// getattr cache. -	expiry time.Time - -	sync.RWMutex -} - -// Lookup - fetch entry for "dir" (relative plaintext path) from the cache. -// Returns the directory IV and the relative encrypted path, or (nil, "") -// if the entry was not found. -func (c *DirIVCache) Lookup(dir string) (iv []byte, cDir string) { -	c.RLock() -	defer c.RUnlock() -	if dir == "" { -		return c.rootDirIV, "" -	} -	if c.data == nil { -		return nil, "" -	} -	if time.Since(c.expiry) > 0 { -		c.data = nil -		return nil, "" -	} -	v := c.data[dir] -	return v.iv, v.cDir -} - -// Store - write an entry for directory "dir" into the cache. -// Arguments: -// dir ... relative plaintext path -// iv .... directory IV -// cDir .. relative ciphertext path -func (c *DirIVCache) Store(dir string, iv []byte, cDir string) { -	c.Lock() -	defer c.Unlock() -	if dir == "" { -		c.rootDirIV = iv -	} -	// Sanity check: plaintext and chiphertext paths must have the same number -	// of segments -	if strings.Count(dir, "/") != strings.Count(cDir, "/") { -		log.Panicf("inconsistent number of path segments: dir=%q cDir=%q", dir, cDir) -	} -	// Clear() may have cleared c.data: re-initialize -	if c.data == nil { -		c.data = make(map[string]cacheEntry, maxEntries) -		// Set expiry time one second into the future -		c.expiry = time.Now().Add(expireTime) -	} -	// Delete a random entry from the map if reached maxEntries -	if len(c.data) >= maxEntries { -		for k := range c.data { -			delete(c.data, k) -			break -		} -	} -	c.data[dir] = cacheEntry{iv, cDir} -} - -// Clear ... clear the cache. -// Called from fusefrontend when directories are renamed or deleted. -func (c *DirIVCache) Clear() { -	c.Lock() -	defer c.Unlock() -	// Will be re-initialized in the next Store() -	c.data = nil -} diff --git a/internal/nametransform/names.go b/internal/nametransform/names.go index 33128b9..638a9eb 100644 --- a/internal/nametransform/names.go +++ b/internal/nametransform/names.go @@ -9,15 +9,13 @@ import (  	"github.com/rfjakob/eme" -	"github.com/rfjakob/gocryptfs/internal/nametransform/dirivcache"  	"github.com/rfjakob/gocryptfs/internal/tlog"  )  // NameTransform is used to transform filenames.  type NameTransform struct { -	emeCipher  *eme.EMECipher -	longNames  bool -	DirIVCache dirivcache.DirIVCache +	emeCipher *eme.EMECipher +	longNames bool  	// B64 = either base64.URLEncoding or base64.RawURLEncoding, depending  	// on the Raw64 feature flag  	B64 *base64.Encoding diff --git a/internal/syscallcompat/open_nofollow.go b/internal/syscallcompat/open_nofollow.go index 3953a27..db39415 100644 --- a/internal/syscallcompat/open_nofollow.go +++ b/internal/syscallcompat/open_nofollow.go @@ -23,6 +23,7 @@ func OpenDirNofollow(baseDir string, relPath string) (fd int, err error) {  		return -1, syscall.EINVAL  	}  	// Open the base dir (following symlinks) +	// TODO: should this use syscallcompat.O_PATH?  	dirfd, err := syscall.Open(baseDir, syscall.O_RDONLY|syscall.O_DIRECTORY, 0)  	if err != nil {  		return -1, err | 
