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