Skip to content

Commit

Permalink
fusefrontend: implement recursive diriv caching
Browse files Browse the repository at this point in the history
The new contrib/maxlen.bash showed that we have exponential
runtime with respect to directory depth.

The new recursive diriv caching is a lot smarter as it caches
intermediate lookups. maxlen.bash now completes in a few seconds.

xfstests results same as
https://github.com/rfjakob/fuse-xfstests/blob/2d158e4c82be85c15269af77498e353f928f4fab/screenlog.0 :

  Failures: generic/035 generic/062 generic/080 generic/093 generic/099 generic/215 generic/285 generic/319 generic/426 generic/444 generic/467 generic/477 generic/523
  Failed 13 of 580 tests

benchmark.bash results are identical:

  $ ./benchmark.bash
  Testing gocryptfs at /tmp/benchmark.bash.BdQ: gocryptfs v2.0.1-17-g6b09bc0; go-fuse v2.1.1-0.20210611132105-24a1dfe6b4f8; 2021-06-25 go1.16.5 linux/amd64
  /tmp/benchmark.bash.BdQ.mnt is a mountpoint
  WRITE: 262144000 bytes (262 MB, 250 MiB) copied, 0,4821 s, 544 MB/s
  READ:  262144000 bytes (262 MB, 250 MiB) copied, 0,266061 s, 985 MB/s
  UNTAR: 8,280
  MD5:   4,564
  LS:    1,745
  RM:    2,244
  • Loading branch information
rfjakob committed Jun 25, 2021
1 parent 05b813f commit 84e7021
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 110 deletions.
8 changes: 5 additions & 3 deletions internal/fusefrontend/dircache.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"syscall"
"time"

"github.com/rfjakob/gocryptfs/internal/nametransform"
"github.com/rfjakob/gocryptfs/internal/tlog"
)

Expand Down Expand Up @@ -50,6 +49,9 @@ func (e *dirCacheEntry) Clear() {

type dirCache struct {
sync.Mutex
// Expected length of the stored IVs. Only used for sanity checks.
// Usually set to 16, but 0 in plaintextnames mode.
ivLen int
// Cache entries
entries [dirCacheSize]dirCacheEntry
// Where to store the next entry (index into entries)
Expand Down Expand Up @@ -77,7 +79,7 @@ func (d *dirCache) Clear() {
func (d *dirCache) Store(node *Node, fd int, iv []byte) {
// Note: package ensurefds012, imported from main, guarantees that dirCache
// can never get fds 0,1,2.
if fd <= 0 || len(iv) != nametransform.DirIVLen {
if fd <= 0 || len(iv) != d.ivLen {
log.Panicf("Store sanity check failed: fd=%d len=%d", fd, len(iv))
}
d.Lock()
Expand Down Expand Up @@ -139,7 +141,7 @@ func (d *dirCache) Lookup(node *Node) (fd int, iv []byte) {
if enableStats {
d.hits++
}
if fd <= 0 || len(iv) != nametransform.DirIVLen {
if fd <= 0 || len(iv) != d.ivLen {
log.Panicf("Lookup sanity check failed: fd=%d len=%d", fd, len(iv))
}
d.dbg("dirCache.Lookup %p hit fd=%d dup=%d iv=%x\n", node, e.fd, fd, iv)
Expand Down
8 changes: 4 additions & 4 deletions internal/fusefrontend/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (n *Node) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut)
return f.(fs.FileGetattrer).Getattr(ctx, out)
}

dirfd, cName, errno := n.prepareAtSyscall("")
dirfd, cName, errno := n.prepareAtSyscallMyself()
if errno != 0 {
return
}
Expand Down Expand Up @@ -106,7 +106,7 @@ func (n *Node) Unlink(ctx context.Context, name string) (errno syscall.Errno) {
//
// Symlink-safe through openBackingDir() + Readlinkat().
func (n *Node) Readlink(ctx context.Context) (out []byte, errno syscall.Errno) {
dirfd, cName, errno := n.prepareAtSyscall("")
dirfd, cName, errno := n.prepareAtSyscallMyself()
if errno != 0 {
return
}
Expand All @@ -123,7 +123,7 @@ func (n *Node) Setattr(ctx context.Context, f fs.FileHandle, in *fuse.SetAttrIn,
return f2.Setattr(ctx, in, out)
}

dirfd, cName, errno := n.prepareAtSyscall("")
dirfd, cName, errno := n.prepareAtSyscallMyself()
if errno != 0 {
return
}
Expand Down Expand Up @@ -271,7 +271,7 @@ func (n *Node) Link(ctx context.Context, target fs.InodeEmbedder, name string, o
defer syscall.Close(dirfd)

n2 := toNode(target)
dirfd2, cName2, errno := n2.prepareAtSyscall("")
dirfd2, cName2, errno := n2.prepareAtSyscallMyself()
if errno != 0 {
return
}
Expand Down
4 changes: 2 additions & 2 deletions internal/fusefrontend/node_dir_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ func (n *Node) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.En
// This function is symlink-safe through use of openBackingDir() and
// ReadDirIVAt().
func (n *Node) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) {
parentDirFd, cDirName, errno := n.prepareAtSyscall("")
parentDirFd, cDirName, errno := n.prepareAtSyscallMyself()
if errno != 0 {
return nil, errno
}
Expand Down Expand Up @@ -360,7 +360,7 @@ retry:

// Opendir is a FUSE call to check if the directory can be opened.
func (n *Node) Opendir(ctx context.Context) (errno syscall.Errno) {
dirfd, cName, errno := n.prepareAtSyscall("")
dirfd, cName, errno := n.prepareAtSyscallMyself()
if errno != 0 {
return
}
Expand Down
89 changes: 0 additions & 89 deletions internal/fusefrontend/node_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@ package fusefrontend

import (
"context"
"log"
"path/filepath"
"sync/atomic"
"syscall"

"github.com/hanwen/go-fuse/v2/fs"

"github.com/hanwen/go-fuse/v2/fuse"

"github.com/rfjakob/gocryptfs/internal/nametransform"
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
"github.com/rfjakob/gocryptfs/internal/tlog"
)
Expand Down Expand Up @@ -80,91 +76,6 @@ func (n *Node) rootNode() *RootNode {
return n.Root().Operations().(*RootNode)
}

// prepareAtSyscall returns a (dirfd, cName) pair that can be used
// with the "___at" family of system calls (openat, fstatat, unlinkat...) to
// access the backing encrypted directory.
//
// If you pass a `child` file name, the (dirfd, cName) pair will refer to
// a child of this node.
// If `child` is empty, the (dirfd, cName) pair refers to this node itself. For
// the root node, that means (dirfd, ".").
func (n *Node) prepareAtSyscall(child string) (dirfd int, cName string, errno syscall.Errno) {
rn := n.rootNode()
// all filesystem operations go through prepareAtSyscall(), so this is a
// good place to reset the idle marker.
atomic.StoreUint32(&rn.IsIdle, 0)

// root node itself is special
if child == "" && n.IsRoot() {
var err error
dirfd, cName, err = rn.openBackingDir("")
if err != nil {
errno = fs.ToErrno(err)
}
return
}

// normal node itself can be converted to child of parent node
if child == "" {
name, p1 := n.Parent()
if p1 == nil || name == "" {
return -1, "", syscall.ENOENT
}
p2 := toNode(p1.Operations())
return p2.prepareAtSyscall(name)
}

// Cache lookup
// TODO make it work for plaintextnames as well?
cacheable := (!rn.args.PlaintextNames)
if cacheable {
var iv []byte
dirfd, iv = rn.dirCache.Lookup(n)
if dirfd > 0 {
var cName string
var err error
if rn.nameTransform.HaveBadnamePatterns() {
//BadName allowed, try to determine filenames
cName, err = rn.nameTransform.EncryptAndHashBadName(child, iv, dirfd)
} else {
cName, err = rn.nameTransform.EncryptAndHashName(child, iv)
}

if err != nil {
return -1, "", fs.ToErrno(err)
}
return dirfd, cName, 0
}
}

// Slowpath
if child == "" {
log.Panicf("BUG: child name is empty - this cannot happen")
}
p := filepath.Join(n.Path(), child)
if rn.isFiltered(p) {
errno = syscall.EPERM
return
}
dirfd, cName, err := rn.openBackingDir(p)
if err != nil {
errno = fs.ToErrno(err)
return
}

// Cache store
if cacheable {
// TODO: openBackingDir already calls ReadDirIVAt(). Avoid duplicate work?
iv, err := nametransform.ReadDirIVAt(dirfd)
if err != nil {
syscall.Close(dirfd)
return -1, "", fs.ToErrno(err)
}
rn.dirCache.Store(n, dirfd, iv)
}
return
}

// newChild attaches a new child inode to n.
// The passed-in `st` will be modified to get a unique inode number
// (or, in `-sharedstorage` mode, the inode number will be set to zero).
Expand Down
2 changes: 1 addition & 1 deletion internal/fusefrontend/node_open_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
//
// Symlink-safe through Openat().
func (n *Node) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
dirfd, cName, errno := n.prepareAtSyscall("")
dirfd, cName, errno := n.prepareAtSyscallMyself()
if errno != 0 {
return
}
Expand Down
116 changes: 116 additions & 0 deletions internal/fusefrontend/node_prepare_syscall.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package fusefrontend

import (
"sync/atomic"
"syscall"

"github.com/rfjakob/gocryptfs/internal/tlog"

"github.com/hanwen/go-fuse/v2/fs"

"github.com/rfjakob/gocryptfs/internal/nametransform"
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
)

// prepareAtSyscall returns a (dirfd, cName) pair that can be used
// with the "___at" family of system calls (openat, fstatat, unlinkat...) to
// access the backing encrypted child file.
func (n *Node) prepareAtSyscall(child string) (dirfd int, cName string, errno syscall.Errno) {
if child == "" {
tlog.Warn.Printf("BUG: prepareAtSyscall: child=%q, should have called prepareAtSyscallMyself", child)
return n.prepareAtSyscallMyself()
}

rn := n.rootNode()

// All filesystem operations go through here, so this is a good place
// to reset the idle marker.
atomic.StoreUint32(&rn.IsIdle, 0)

if n.IsRoot() && rn.isFiltered(child) {
return -1, "", syscall.EPERM
}

var encryptName func(int, string, []byte) (string, error)
if !rn.args.PlaintextNames {
encryptName = func(dirfd int, child string, iv []byte) (cName string, err error) {
// Badname allowed, try to determine filenames
if rn.nameTransform.HaveBadnamePatterns() {
return rn.nameTransform.EncryptAndHashBadName(child, iv, dirfd)
}
return rn.nameTransform.EncryptAndHashName(child, iv)
}
}

// Cache lookup
var iv []byte
dirfd, iv = rn.dirCache.Lookup(n)
if dirfd > 0 {
if rn.args.PlaintextNames {
return dirfd, child, 0
}
var err error
cName, err = encryptName(dirfd, child, iv)
if err != nil {
syscall.Close(dirfd)
return -1, "", fs.ToErrno(err)
}
return
}

// Slowpath: Open ourselves & read diriv
parentDirfd, myCName, errno := n.prepareAtSyscallMyself()
if errno != 0 {
return
}
defer syscall.Close(parentDirfd)

dirfd, err := syscallcompat.Openat(parentDirfd, myCName, syscall.O_NOFOLLOW|syscall.O_DIRECTORY|syscallcompat.O_PATH, 0)

// Cache store
if !rn.args.PlaintextNames {
var err error
iv, err = nametransform.ReadDirIVAt(dirfd)
if err != nil {
syscall.Close(dirfd)
return -1, "", fs.ToErrno(err)
}
}
rn.dirCache.Store(n, dirfd, iv)

if rn.args.PlaintextNames {
return dirfd, child, 0
}

cName, err = encryptName(dirfd, child, iv)
if err != nil {
syscall.Close(dirfd)
return -1, "", fs.ToErrno(err)
}

return
}

func (n *Node) prepareAtSyscallMyself() (dirfd int, cName string, errno syscall.Errno) {
dirfd = -1

// Handle root node
if n.IsRoot() {
var err error
rn := n.rootNode()
dirfd, cName, err = rn.openBackingDir("")
if err != nil {
errno = fs.ToErrno(err)
}
return
}

// Otherwise convert to prepareAtSyscall of parent node
myName, p1 := n.Parent()
if p1 == nil || myName == "" {
errno = syscall.ENOENT
return
}
parent := toNode(p1.Operations())
return parent.prepareAtSyscall(myName)
}
8 changes: 4 additions & 4 deletions internal/fusefrontend/node_xattr_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func filterXattrSetFlags(flags int) int {
}

func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) {
dirfd, cName, errno := n.prepareAtSyscall("")
dirfd, cName, errno := n.prepareAtSyscallMyself()
if errno != 0 {
return
}
Expand All @@ -42,7 +42,7 @@ func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) {
}

func (n *Node) setXAttr(context *fuse.Context, cAttr string, cData []byte, flags uint32) (errno syscall.Errno) {
dirfd, cName, errno := n.prepareAtSyscall("")
dirfd, cName, errno := n.prepareAtSyscallMyself()
if errno != 0 {
return
}
Expand All @@ -64,7 +64,7 @@ func (n *Node) setXAttr(context *fuse.Context, cAttr string, cData []byte, flags
}

func (n *Node) removeXAttr(cAttr string) (errno syscall.Errno) {
dirfd, cName, errno := n.prepareAtSyscall("")
dirfd, cName, errno := n.prepareAtSyscallMyself()
if errno != 0 {
return
}
Expand All @@ -86,7 +86,7 @@ func (n *Node) removeXAttr(cAttr string) (errno syscall.Errno) {
}

func (n *Node) listXAttr() (out []string, errno syscall.Errno) {
dirfd, cName, errno := n.prepareAtSyscall("")
dirfd, cName, errno := n.prepareAtSyscallMyself()
if errno != 0 {
return
}
Expand Down
8 changes: 4 additions & 4 deletions internal/fusefrontend/node_xattr_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func filterXattrSetFlags(flags int) int {
}

func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) {
dirfd, cName, errno := n.prepareAtSyscall("")
dirfd, cName, errno := n.prepareAtSyscallMyself()
if errno != 0 {
return
}
Expand All @@ -32,7 +32,7 @@ func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) {
}

func (n *Node) setXAttr(context *fuse.Context, cAttr string, cData []byte, flags uint32) (errno syscall.Errno) {
dirfd, cName, errno := n.prepareAtSyscall("")
dirfd, cName, errno := n.prepareAtSyscallMyself()
if errno != 0 {
return
}
Expand All @@ -44,7 +44,7 @@ func (n *Node) setXAttr(context *fuse.Context, cAttr string, cData []byte, flags
}

func (n *Node) removeXAttr(cAttr string) (errno syscall.Errno) {
dirfd, cName, errno := n.prepareAtSyscall("")
dirfd, cName, errno := n.prepareAtSyscallMyself()
if errno != 0 {
return
}
Expand All @@ -55,7 +55,7 @@ func (n *Node) removeXAttr(cAttr string) (errno syscall.Errno) {
}

func (n *Node) listXAttr() (out []string, errno syscall.Errno) {
dirfd, cName, errno := n.prepareAtSyscall("")
dirfd, cName, errno := n.prepareAtSyscallMyself()
if errno != 0 {
return
}
Expand Down
Loading

0 comments on commit 84e7021

Please sign in to comment.