aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/fusefrontend/dircache.go117
-rw-r--r--internal/fusefrontend/fs.go6
-rw-r--r--internal/fusefrontend/fs_dir.go8
-rw-r--r--internal/fusefrontend/openbackingdir.go15
-rw-r--r--internal/fusefrontend/openbackingdir_test.go7
-rw-r--r--internal/nametransform/dirivcache/dirivcache.go102
-rw-r--r--internal/nametransform/names.go6
-rw-r--r--internal/syscallcompat/open_nofollow.go1
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