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.
|
// which is called as part of every filesystem operation.
|
||||||
// (This flag uses a uint32 so that it can be reset with CompareAndSwapUint32.)
|
// (This flag uses a uint32 so that it can be reset with CompareAndSwapUint32.)
|
||||||
AccessedSinceLastCheck uint32
|
AccessedSinceLastCheck uint32
|
||||||
|
|
||||||
|
dirCache dirCacheStruct
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ pathfs.FileSystem = &FS{} // Verify that interface is implemented.
|
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().
|
// Symlink-safe through Renameat().
|
||||||
func (fs *FS) Rename(oldPath string, newPath string, context *fuse.Context) (code fuse.Status) {
|
func (fs *FS) Rename(oldPath string, newPath string, context *fuse.Context) (code fuse.Status) {
|
||||||
|
defer fs.dirCache.Clear()
|
||||||
if fs.isFiltered(newPath) {
|
if fs.isFiltered(newPath) {
|
||||||
return fuse.EPERM
|
return fuse.EPERM
|
||||||
}
|
}
|
||||||
@ -546,9 +549,6 @@ func (fs *FS) Rename(oldPath string, newPath string, context *fuse.Context) (cod
|
|||||||
return fuse.ToStatus(err)
|
return fuse.ToStatus(err)
|
||||||
}
|
}
|
||||||
defer syscall.Close(newDirfd)
|
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.
|
// Easy case.
|
||||||
if fs.args.PlaintextNames {
|
if fs.args.PlaintextNames {
|
||||||
return fuse.ToStatus(syscallcompat.Renameat(oldDirfd, oldCName, newDirfd, newCName))
|
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
|
// the directory is inconsistent. Take the lock to prevent other readers
|
||||||
// from seeing it.
|
// from seeing it.
|
||||||
fs.dirIVLock.Lock()
|
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()
|
defer fs.dirIVLock.Unlock()
|
||||||
err := syscallcompat.Mkdirat(dirfd, cName, mode)
|
err := syscallcompat.Mkdirat(dirfd, cName, mode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -57,6 +55,7 @@ func (fs *FS) mkdirWithIv(dirfd int, cName string, mode uint32) error {
|
|||||||
//
|
//
|
||||||
// Symlink-safe through use of Mkdirat().
|
// Symlink-safe through use of Mkdirat().
|
||||||
func (fs *FS) Mkdir(newPath string, mode uint32, context *fuse.Context) (code fuse.Status) {
|
func (fs *FS) Mkdir(newPath string, mode uint32, context *fuse.Context) (code fuse.Status) {
|
||||||
|
defer fs.dirCache.Clear()
|
||||||
if fs.isFiltered(newPath) {
|
if fs.isFiltered(newPath) {
|
||||||
return fuse.EPERM
|
return fuse.EPERM
|
||||||
}
|
}
|
||||||
@ -142,6 +141,7 @@ func haveDsstore(entries []fuse.DirEntry) bool {
|
|||||||
//
|
//
|
||||||
// Symlink-safe through Unlinkat() + AT_REMOVEDIR.
|
// Symlink-safe through Unlinkat() + AT_REMOVEDIR.
|
||||||
func (fs *FS) Rmdir(relPath string, context *fuse.Context) (code fuse.Status) {
|
func (fs *FS) Rmdir(relPath string, context *fuse.Context) (code fuse.Status) {
|
||||||
|
defer fs.dirCache.Clear()
|
||||||
parentDirFd, cName, err := fs.openBackingDir(relPath)
|
parentDirFd, cName, err := fs.openBackingDir(relPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fuse.ToStatus(err)
|
return fuse.ToStatus(err)
|
||||||
@ -252,8 +252,8 @@ retry:
|
|||||||
if nametransform.IsLongContent(cName) {
|
if nametransform.IsLongContent(cName) {
|
||||||
nametransform.DeleteLongNameAt(parentDirFd, cName)
|
nametransform.DeleteLongNameAt(parentDirFd, cName)
|
||||||
}
|
}
|
||||||
// The now-deleted directory may have been in the DirIV cache. Clear it.
|
// The now-deleted directory may have been in the dirCache. Clear it.
|
||||||
fs.nameTransform.DirIVCache.Clear()
|
fs.dirCache.Clear()
|
||||||
return fuse.OK
|
return fuse.OK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// openBackingDir opens the parent ciphertext directory of plaintext path
|
// 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.
|
// The caller should then use Openat(dirfd, cName, ...) and friends.
|
||||||
// For convenience, if relPath is "", cName is going to be ".".
|
// 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
|
// openBackingDir is secure against symlink races by using Openat and
|
||||||
// ReadDirIVAt.
|
// ReadDirIVAt.
|
||||||
func (fs *FS) openBackingDir(relPath string) (dirfd int, cName string, err error) {
|
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.
|
// With PlaintextNames, we don't need to read DirIVs. Easy.
|
||||||
if fs.args.PlaintextNames {
|
if fs.args.PlaintextNames {
|
||||||
dir := nametransform.Dir(relPath)
|
dirfd, err = syscallcompat.OpenDirNofollow(fs.args.Cipherdir, dirRelPath)
|
||||||
dirfd, err = syscallcompat.OpenDirNofollow(fs.args.Cipherdir, dir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, "", err
|
return -1, "", err
|
||||||
}
|
}
|
||||||
@ -29,6 +30,13 @@ func (fs *FS) openBackingDir(relPath string) (dirfd int, cName string, err error
|
|||||||
cName = filepath.Base(relPath)
|
cName = filepath.Base(relPath)
|
||||||
return dirfd, cName, nil
|
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)
|
// Open cipherdir (following symlinks)
|
||||||
dirfd, err = syscall.Open(fs.args.Cipherdir, syscall.O_RDONLY|syscall.O_DIRECTORY|syscallcompat.O_PATH, 0)
|
dirfd, err = syscall.Open(fs.args.Cipherdir, syscall.O_RDONLY|syscall.O_DIRECTORY|syscallcompat.O_PATH, 0)
|
||||||
if err != nil {
|
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)
|
cName = fs.nameTransform.EncryptAndHashName(name, iv)
|
||||||
// Last part? We are done.
|
// Last part? We are done.
|
||||||
if i == len(parts)-1 {
|
if i == len(parts)-1 {
|
||||||
|
fs.dirCache.Store(dirRelPath, dirfd, iv)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// Not the last part? Descend into next directory.
|
// Not the last part? Descend into next directory.
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
package fusefrontend
|
package fusefrontend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
@ -53,7 +56,9 @@ func TestOpenBackingDir(t *testing.T) {
|
|||||||
}
|
}
|
||||||
err = syscallcompat.Faccessat(dirfd, cName, unix.R_OK)
|
err = syscallcompat.Faccessat(dirfd, cName, unix.R_OK)
|
||||||
if err != nil {
|
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)
|
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,15 +9,13 @@ import (
|
|||||||
|
|
||||||
"github.com/rfjakob/eme"
|
"github.com/rfjakob/eme"
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/nametransform/dirivcache"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/tlog"
|
"github.com/rfjakob/gocryptfs/internal/tlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NameTransform is used to transform filenames.
|
// NameTransform is used to transform filenames.
|
||||||
type NameTransform struct {
|
type NameTransform struct {
|
||||||
emeCipher *eme.EMECipher
|
emeCipher *eme.EMECipher
|
||||||
longNames bool
|
longNames bool
|
||||||
DirIVCache dirivcache.DirIVCache
|
|
||||||
// B64 = either base64.URLEncoding or base64.RawURLEncoding, depending
|
// B64 = either base64.URLEncoding or base64.RawURLEncoding, depending
|
||||||
// on the Raw64 feature flag
|
// on the Raw64 feature flag
|
||||||
B64 *base64.Encoding
|
B64 *base64.Encoding
|
||||||
|
@ -23,6 +23,7 @@ func OpenDirNofollow(baseDir string, relPath string) (fd int, err error) {
|
|||||||
return -1, syscall.EINVAL
|
return -1, syscall.EINVAL
|
||||||
}
|
}
|
||||||
// Open the base dir (following symlinks)
|
// 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)
|
dirfd, err := syscall.Open(baseDir, syscall.O_RDONLY|syscall.O_DIRECTORY, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, err
|
return -1, err
|
||||||
|
Loading…
x
Reference in New Issue
Block a user