From 94e8fc12ea5756a130e7ac9ed67ddd519b5f3a22 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sat, 15 Aug 2020 17:31:25 +0200 Subject: [PATCH] v2api/reverse: finish -exclude Tests pass now. --- internal/fusefrontend_reverse/excluder.go | 6 +-- .../fusefrontend_reverse/excluder_test.go | 8 --- internal/fusefrontend_reverse/node.go | 33 ++++++------ internal/fusefrontend_reverse/node_dir_ops.go | 17 +++---- internal/fusefrontend_reverse/node_helpers.go | 51 ++++++++++++++----- internal/fusefrontend_reverse/root_node.go | 37 ++++++++++++-- internal/fusefrontend_reverse/rpath.go | 13 +++-- tests/reverse/correctness_test.go | 2 +- tests/reverse/exclude_test.go | 2 + tests/reverse/inomap_test.go | 4 +- 10 files changed, 109 insertions(+), 64 deletions(-) diff --git a/internal/fusefrontend_reverse/excluder.go b/internal/fusefrontend_reverse/excluder.go index b6cb961..8a13fa7 100644 --- a/internal/fusefrontend_reverse/excluder.go +++ b/internal/fusefrontend_reverse/excluder.go @@ -2,6 +2,7 @@ package fusefrontend_reverse import ( "io/ioutil" + "log" "os" "strings" @@ -15,12 +16,9 @@ import ( // prepareExcluder creates an object to check if paths are excluded // based on the patterns specified in the command line. func prepareExcluder(args fusefrontend.Args) *ignore.GitIgnore { - if len(args.Exclude) == 0 && len(args.ExcludeWildcard) == 0 && len(args.ExcludeFrom) == 0 { - return nil - } patterns := getExclusionPatterns(args) if len(patterns) == 0 { - panic(patterns) + log.Panic(patterns) } excluder, err := ignore.CompileIgnoreLines(patterns...) if err != nil { diff --git a/internal/fusefrontend_reverse/excluder_test.go b/internal/fusefrontend_reverse/excluder_test.go index 47b430a..d6cfef3 100644 --- a/internal/fusefrontend_reverse/excluder_test.go +++ b/internal/fusefrontend_reverse/excluder_test.go @@ -9,14 +9,6 @@ import ( "github.com/rfjakob/gocryptfs/internal/fusefrontend" ) -func TestShouldNoCreateExcluderIfNoPattersWereSpecified(t *testing.T) { - var args fusefrontend.Args - excluder := prepareExcluder(args) - if excluder != nil { - t.Error("Should not have created excluder") - } -} - func TestShouldPrefixExcludeValuesWithSlash(t *testing.T) { var args fusefrontend.Args args.Exclude = []string{"file1", "dir1/file2.txt"} diff --git a/internal/fusefrontend_reverse/node.go b/internal/fusefrontend_reverse/node.go index deaf953..de1ee49 100644 --- a/internal/fusefrontend_reverse/node.go +++ b/internal/fusefrontend_reverse/node.go @@ -26,8 +26,7 @@ type Node struct { // Lookup - FUSE call for discovering a file. func (n *Node) Lookup(ctx context.Context, cName string, out *fuse.EntryOut) (ch *fs.Inode, errno syscall.Errno) { - dirfd := int(-1) - pName := "" + var d *dirfdPlus t := n.lookupFileType(cName) if t == typeDiriv { // gocryptfs.diriv @@ -40,14 +39,15 @@ func (n *Node) Lookup(ctx context.Context, cName string, out *fuse.EntryOut) (ch return n.lookupConf(ctx, out) } else if t == typeReal { // real file - dirfd, pName, errno = n.prepareAtSyscall(cName) + d, errno = n.prepareAtSyscall(cName) + //fmt.Printf("Lookup: prepareAtSyscall -> d=%#v, errno=%d\n", d, errno) if errno != 0 { return } - defer syscall.Close(dirfd) + defer syscall.Close(d.dirfd) } // Get device number and inode number into `st` - st, err := syscallcompat.Fstatat2(dirfd, pName, unix.AT_SYMLINK_NOFOLLOW) + st, err := syscallcompat.Fstatat2(d.dirfd, d.pName, unix.AT_SYMLINK_NOFOLLOW) if err != nil { return nil, fs.ToErrno(err) } @@ -55,7 +55,7 @@ func (n *Node) Lookup(ctx context.Context, cName string, out *fuse.EntryOut) (ch ch = n.newChild(ctx, st, out) // Translate ciphertext size in `out.Attr.Size` to plaintext size if t == typeReal { - n.translateSize(dirfd, cName, pName, &out.Attr) + n.translateSize(d.dirfd, cName, d.pName, &out.Attr) } return ch, 0 } @@ -69,13 +69,13 @@ func (n *Node) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) return f.(fs.FileGetattrer).Getattr(ctx, out) } - dirfd, pName, errno := n.prepareAtSyscall("") + d, errno := n.prepareAtSyscall("") if errno != 0 { return } - defer syscall.Close(dirfd) + defer syscall.Close(d.dirfd) - st, err := syscallcompat.Fstatat2(dirfd, pName, unix.AT_SYMLINK_NOFOLLOW) + st, err := syscallcompat.Fstatat2(d.dirfd, d.pName, unix.AT_SYMLINK_NOFOLLOW) if err != nil { return fs.ToErrno(err) } @@ -87,7 +87,7 @@ func (n *Node) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) // Translate ciphertext size in `out.Attr.Size` to plaintext size cName := filepath.Base(n.Path()) - n.translateSize(dirfd, cName, pName, &out.Attr) + n.translateSize(d.dirfd, cName, d.pName, &out.Attr) if rn.args.ForceOwner != nil { out.Owner = *rn.args.ForceOwner @@ -99,27 +99,26 @@ func (n *Node) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) // // Symlink-safe through openBackingDir() + Readlinkat(). func (n *Node) Readlink(ctx context.Context) (out []byte, errno syscall.Errno) { - dirfd, pName, errno := n.prepareAtSyscall("") + d, errno := n.prepareAtSyscall("") if errno != 0 { return } - defer syscall.Close(dirfd) + defer syscall.Close(d.dirfd) - cName := filepath.Base(n.Path()) - return n.readlink(dirfd, cName, pName) + return n.readlink(d.dirfd, d.cName, d.pName) } // Open - FUSE call. Open already-existing file. // // Symlink-safe through Openat(). func (n *Node) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { - dirfd, pName, errno := n.prepareAtSyscall("") + d, errno := n.prepareAtSyscall("") if errno != 0 { return } - defer syscall.Close(dirfd) + defer syscall.Close(d.dirfd) - fd, err := syscallcompat.Openat(dirfd, pName, syscall.O_RDONLY|syscall.O_NOFOLLOW, 0) + fd, err := syscallcompat.Openat(d.dirfd, d.pName, syscall.O_RDONLY|syscall.O_NOFOLLOW, 0) if err != nil { errno = fs.ToErrno(err) return diff --git a/internal/fusefrontend_reverse/node_dir_ops.go b/internal/fusefrontend_reverse/node_dir_ops.go index 5ec1e95..22f8122 100644 --- a/internal/fusefrontend_reverse/node_dir_ops.go +++ b/internal/fusefrontend_reverse/node_dir_ops.go @@ -23,15 +23,15 @@ import ( // This function is symlink-safe through use of openBackingDir() and // ReadDirIVAt(). func (n *Node) Readdir(ctx context.Context) (stream fs.DirStream, errno syscall.Errno) { - dirfd, cName, errno := n.prepareAtSyscall("") + d, errno := n.prepareAtSyscall("") if errno != 0 { return } - defer syscall.Close(dirfd) + defer syscall.Close(d.dirfd) // Read plaintext directory var entries []fuse.DirEntry - fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0) + fd, err := syscallcompat.Openat(d.dirfd, d.pName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0) if err != nil { return nil, fs.ToErrno(err) } @@ -42,21 +42,20 @@ func (n *Node) Readdir(ctx context.Context) (stream fs.DirStream, errno syscall. } rn := n.rootNode() + + // Filter out excluded entries + entries = rn.excludeDirEntries(d, entries) + if rn.args.PlaintextNames { return n.readdirPlaintextnames(entries) } - // Filter out excluded entries - //TODO - //entries = rfs.excludeDirEntries(relPath, entries) - // Virtual files: at least one gocryptfs.diriv file virtualFiles := []fuse.DirEntry{ {Mode: virtualFileMode, Name: nametransform.DirIVFilename}, } - cipherPath := n.Path() - dirIV := pathiv.Derive(cipherPath, pathiv.PurposeDirIV) + dirIV := pathiv.Derive(d.cPath, pathiv.PurposeDirIV) // Encrypt names for i := range entries { var cName string diff --git a/internal/fusefrontend_reverse/node_helpers.go b/internal/fusefrontend_reverse/node_helpers.go index 76ddd42..92f6a87 100644 --- a/internal/fusefrontend_reverse/node_helpers.go +++ b/internal/fusefrontend_reverse/node_helpers.go @@ -47,6 +47,20 @@ func (n *Node) rootNode() *RootNode { return n.Root().Operations().(*RootNode) } +// dirfdPlus gets filled out as we gather information about a node +type dirfdPlus struct { + // fd to the directory, opened with O_DIRECTORY|O_PATH + dirfd int + // Relative plaintext path + pPath string + // Plaintext basename: filepath.Base(pPath) + pName string + // Relative ciphertext path + cPath string + // Ciphertext basename: filepath.Base(cPath) + cName string +} + // 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. @@ -54,16 +68,23 @@ func (n *Node) rootNode() *RootNode { // 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. -func (n *Node) prepareAtSyscall(child string) (dirfd int, pName string, errno syscall.Errno) { - p := n.Path() +func (n *Node) prepareAtSyscall(child string) (d *dirfdPlus, errno syscall.Errno) { + cPath := n.Path() if child != "" { - p = filepath.Join(p, child) + cPath = filepath.Join(cPath, child) } rn := n.rootNode() - dirfd, pName, err := rn.openBackingDir(p) + dirfd, pPath, err := rn.openBackingDir(cPath) if err != nil { errno = fs.ToErrno(err) } + d = &dirfdPlus{ + dirfd: dirfd, + pPath: pPath, + pName: filepath.Base(pPath), + cPath: cPath, + cName: filepath.Base(cPath), + } return } @@ -91,28 +112,32 @@ func (n *Node) isRoot() bool { } func (n *Node) lookupLongnameName(ctx context.Context, nameFile string, out *fuse.EntryOut) (ch *fs.Inode, errno syscall.Errno) { - dirfd, pName1, errno := n.prepareAtSyscall("") + d, errno := n.prepareAtSyscall("") if errno != 0 { return } - defer syscall.Close(dirfd) + defer syscall.Close(d.dirfd) // Find the file the gocryptfs.longname.XYZ.name file belongs to in the // directory listing - fd, err := syscallcompat.Openat(dirfd, pName1, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0) + fd, err := syscallcompat.Openat(d.dirfd, d.pName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0) if err != nil { errno = fs.ToErrno(err) return } defer syscall.Close(fd) - diriv := pathiv.Derive(n.Path(), pathiv.PurposeDirIV) + diriv := pathiv.Derive(d.cPath, pathiv.PurposeDirIV) rn := n.rootNode() pName, cFullname, errno := rn.findLongnameParent(fd, diriv, nameFile) if errno != 0 { return } + if rn.isExcludedPlain(filepath.Join(d.cPath, pName)) { + errno = syscall.EPERM + return + } // Get attrs from parent file - st, err := syscallcompat.Fstatat2(dirfd, pName, unix.AT_SYMLINK_NOFOLLOW) + st, err := syscallcompat.Fstatat2(fd, pName, unix.AT_SYMLINK_NOFOLLOW) if err != nil { errno = fs.ToErrno(err) return @@ -132,17 +157,17 @@ func (n *Node) lookupLongnameName(ctx context.Context, nameFile string, out *fus // lookupDiriv returns a new Inode for a gocryptfs.diriv file inside `n`. func (n *Node) lookupDiriv(ctx context.Context, out *fuse.EntryOut) (ch *fs.Inode, errno syscall.Errno) { - dirfd, pName, errno := n.prepareAtSyscall("") + d, errno := n.prepareAtSyscall("") if errno != 0 { return } - defer syscall.Close(dirfd) - st, err := syscallcompat.Fstatat2(dirfd, pName, unix.AT_SYMLINK_NOFOLLOW) + defer syscall.Close(d.dirfd) + st, err := syscallcompat.Fstatat2(d.dirfd, d.pName, unix.AT_SYMLINK_NOFOLLOW) if err != nil { errno = fs.ToErrno(err) return } - content := pathiv.Derive(n.Path(), pathiv.PurposeDirIV) + content := pathiv.Derive(d.cPath, pathiv.PurposeDirIV) var vf *VirtualMemNode vf, errno = n.newVirtualMemNode(content, st, inoTagDirIV) if errno != 0 { diff --git a/internal/fusefrontend_reverse/root_node.go b/internal/fusefrontend_reverse/root_node.go index 4346306..b7a259a 100644 --- a/internal/fusefrontend_reverse/root_node.go +++ b/internal/fusefrontend_reverse/root_node.go @@ -2,12 +2,16 @@ package fusefrontend_reverse import ( "log" + "path/filepath" "strings" "syscall" + "github.com/rfjakob/gocryptfs/internal/tlog" + "golang.org/x/sys/unix" "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" "github.com/rfjakob/gocryptfs/internal/contentenc" "github.com/rfjakob/gocryptfs/internal/fusefrontend" @@ -28,7 +32,7 @@ type RootNode struct { // Content encryption helper contentEnc *contentenc.ContentEnc // Tests whether a path is excluded (hidden) from the user. Used by -exclude. - excluder *ignore.GitIgnore + excluder ignore.IgnoreParser // inoMap translates inode numbers from different devices to unique inode // numbers. inoMap *inomap.InoMap @@ -38,17 +42,23 @@ type RootNode struct { // In this case (reverse mode) the backing directory is plain-text and // ReverseFS provides an encrypted view. func NewRootNode(args fusefrontend.Args, c *contentenc.ContentEnc, n nametransform.NameTransformer) *RootNode { - return &RootNode{ + rn := &RootNode{ args: args, nameTransform: n, contentEnc: c, inoMap: inomap.New(), - excluder: prepareExcluder(args), } + if len(args.Exclude) > 0 || len(args.ExcludeWildcard) > 0 || len(args.ExcludeFrom) > 0 { + rn.excluder = prepareExcluder(args) + } + return rn } // You can pass either gocryptfs.longname.XYZ.name or gocryptfs.longname.XYZ. func (rn *RootNode) findLongnameParent(fd int, diriv []byte, longname string) (pName string, cFullName string, errno syscall.Errno) { + defer func() { + tlog.Debug.Printf("findLongnameParent: %d %x %q -> %q %q %d\n", fd, diriv, longname, pName, cFullName, errno) + }() if strings.HasSuffix(longname, nametransform.LongNameSuffix) { longname = nametransform.RemoveLongNameSuffix(longname) } @@ -84,3 +94,24 @@ func (rn *RootNode) findLongnameParent(fd int, diriv []byte, longname string) (p func (rn *RootNode) isExcludedPlain(pPath string) bool { return rn.excluder != nil && rn.excluder.MatchesPath(pPath) } + +// excludeDirEntries filters out directory entries that are "-exclude"d. +// pDir is the relative plaintext path to the directory these entries are +// from. The entries should be plaintext files. +func (rn *RootNode) excludeDirEntries(d *dirfdPlus, entries []fuse.DirEntry) (filtered []fuse.DirEntry) { + if rn.excluder == nil { + return entries + } + filtered = make([]fuse.DirEntry, 0, len(entries)) + for _, entry := range entries { + // filepath.Join handles the case of pDir="" correctly: + // Join("", "foo") -> "foo". This does not: pDir + "/" + name" + p := filepath.Join(d.pPath, entry.Name) + if rn.isExcludedPlain(p) { + // Skip file + continue + } + filtered = append(filtered, entry) + } + return filtered +} diff --git a/internal/fusefrontend_reverse/rpath.go b/internal/fusefrontend_reverse/rpath.go index 1e44638..f29bbf5 100644 --- a/internal/fusefrontend_reverse/rpath.go +++ b/internal/fusefrontend_reverse/rpath.go @@ -100,25 +100,24 @@ func (rn *RootNode) decryptPath(cPath string) (string, error) { // and returns the fd to the directory and the decrypted name of the // target file. The fd/name pair is intended for use with fchownat and // friends. -func (rn *RootNode) openBackingDir(cPath string) (dirfd int, pName string, err error) { +func (rn *RootNode) openBackingDir(cPath string) (dirfd int, pPath string, err error) { defer func() { - tlog.Debug.Printf("openBackingDir %q -> %d %q %v\n", cPath, dirfd, pName, err) + tlog.Debug.Printf("openBackingDir %q -> %d %q %v\n", cPath, dirfd, pPath, err) }() dirfd = -1 - pRelPath, err := rn.decryptPath(cPath) + pPath, err = rn.decryptPath(cPath) if err != nil { return } - if rn.isExcludedPlain(pRelPath) { + if rn.isExcludedPlain(pPath) { err = syscall.EPERM return } // Open directory, safe against symlink races - pDir := filepath.Dir(pRelPath) + pDir := filepath.Dir(pPath) dirfd, err = syscallcompat.OpenDirNofollow(rn.args.Cipherdir, pDir) if err != nil { return } - pName = filepath.Base(pRelPath) - return dirfd, pName, nil + return dirfd, pPath, nil } diff --git a/tests/reverse/correctness_test.go b/tests/reverse/correctness_test.go index 47b8b73..6b7ed5f 100644 --- a/tests/reverse/correctness_test.go +++ b/tests/reverse/correctness_test.go @@ -87,7 +87,7 @@ func TestSymlinkDentrySize(t *testing.T) { fi, err := os.Lstat(mnt + "/" + symlinkResponse.Result) if err != nil { - t.Errorf("Lstat: %v", err) + t.Fatalf("Lstat: %v", err) } target, err := os.Readlink(mnt + "/" + symlinkResponse.Result) diff --git a/tests/reverse/exclude_test.go b/tests/reverse/exclude_test.go index b5d0f5b..c493d95 100644 --- a/tests/reverse/exclude_test.go +++ b/tests/reverse/exclude_test.go @@ -118,6 +118,7 @@ func testExclude(t *testing.T, flag string) { cExclude := encryptExcludeTestPaths(t, sock, pExclude) // Check that "excluded" paths are not there and "ok" paths are there for _, v := range cExclude { + t.Logf("File %q should be invisible", v) if test_helpers.VerifyExistence(t, mnt+"/"+v) { t.Errorf("File %q is visible, but should be excluded", v) } @@ -126,6 +127,7 @@ func testExclude(t *testing.T, flag string) { } } for _, v := range cOk { + t.Logf("File %q should be visible", v) if !test_helpers.VerifyExistence(t, mnt+"/"+v) { t.Errorf("File %q is hidden, but should be visible", v) } diff --git a/tests/reverse/inomap_test.go b/tests/reverse/inomap_test.go index 5f7fb45..e6fc525 100644 --- a/tests/reverse/inomap_test.go +++ b/tests/reverse/inomap_test.go @@ -45,7 +45,7 @@ func findIno(dir string, ino uint64) string { // ├── gocryptfs.longname.e31v1ax4h_F0l4jhlN8kCjaWWMq8rO9VVBZ15IYsV50 <---- child // └── gocryptfs.longname.e31v1ax4h_F0l4jhlN8kCjaWWMq8rO9VVBZ15IYsV50.name <---- name // -// And verifies that the inode numbers match what we expect. +// It verifies that the inode numbers match what we expect. func TestVirtualFileIno(t *testing.T) { if plaintextnames { t.Skip("plaintextnames mode does not have virtual files") @@ -111,7 +111,7 @@ func TestVirtualFileIno(t *testing.T) { var st2 syscall.Stat_t err = syscall.Lstat(encryptedParent+"/"+entry, &st2) if err != nil { - t.Logf("stat %q: %v", entry, err) + t.Errorf("stat %q: %v", entry, err) continue } if entry == "gocryptfs.diriv" {