fusefrontend: add dirCache
This commit is contained in:
parent
f6dad8d0fa
commit
4f66d66755
117
internal/fusefrontend/dircache.go
Normal file
117
internal/fusefrontend/dircache.go
Normal 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...)
|
||||
}
|
||||
}
|
@ -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))
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/rfjakob/eme"
|
||||
|
||||
"github.com/rfjakob/gocryptfs/internal/nametransform/dirivcache"
|
||||
"github.com/rfjakob/gocryptfs/internal/tlog"
|
||||
)
|
||||
|
||||
@ -17,7 +16,6 @@ import (
|
||||
type NameTransform struct {
|
||||
emeCipher *eme.EMECipher
|
||||
longNames bool
|
||||
DirIVCache dirivcache.DirIVCache
|
||||
// B64 = either base64.URLEncoding or base64.RawURLEncoding, depending
|
||||
// on the Raw64 feature flag
|
||||
B64 *base64.Encoding
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user