aboutsummaryrefslogtreecommitdiff
path: root/internal/fusefrontend
diff options
context:
space:
mode:
Diffstat (limited to 'internal/fusefrontend')
-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
5 files changed, 142 insertions, 11 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)