v2api/reverse: finish -exclude

Tests pass now.
This commit is contained in:
Jakob Unterwurzacher 2020-08-15 17:31:25 +02:00
parent 15b0b4a5fd
commit 94e8fc12ea
10 changed files with 109 additions and 64 deletions

View File

@ -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 {

View File

@ -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"}

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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" {