fusefrontend: add dirCache

This commit is contained in:
Jakob Unterwurzacher 2019-01-02 22:32:21 +01:00
parent f6dad8d0fa
commit 4f66d66755
8 changed files with 145 additions and 117 deletions

View File

@ -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...)
}
}

View File

@ -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))

View File

@ -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
}

View File

@ -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.

View File

@ -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)

View File

@ -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
}

View File

@ -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

View File

@ -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