From 4f66d66755da63c78b09201c6c72353009251cf2 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Wed, 2 Jan 2019 22:32:21 +0100 Subject: fusefrontend: add dirCache --- internal/fusefrontend/dircache.go | 117 +++++++++++++++++++++++++++ internal/fusefrontend/fs.go | 6 +- internal/fusefrontend/fs_dir.go | 8 +- internal/fusefrontend/openbackingdir.go | 15 +++- internal/fusefrontend/openbackingdir_test.go | 7 +- 5 files changed, 142 insertions(+), 11 deletions(-) create mode 100644 internal/fusefrontend/dircache.go (limited to 'internal/fusefrontend') 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) -- cgit v1.2.3