diff options
| -rw-r--r-- | Documentation/MANPAGE.md | 8 | ||||
| -rw-r--r-- | cli_args.go | 3 | ||||
| -rwxr-xr-x | crossbuild.bash | 6 | ||||
| -rw-r--r-- | internal/fusefrontend/args.go | 4 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/node.go | 13 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/node_dir_ops.go | 23 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/node_helpers.go | 5 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/root_node.go | 14 | ||||
| -rw-r--r-- | mount.go | 1 | ||||
| -rw-r--r-- | tests/reverse/one_file_system_test.go | 77 | 
10 files changed, 140 insertions, 14 deletions
| diff --git a/Documentation/MANPAGE.md b/Documentation/MANPAGE.md index 648a6c7..3a2f6c5 100644 --- a/Documentation/MANPAGE.md +++ b/Documentation/MANPAGE.md @@ -331,6 +331,14 @@ See `-suid, -nosuid`.  Send USR1 to the specified process after successful mount. This is  used internally for daemonization. +#### -one-file-system +Don't cross filesystem boundaries (like rsync's `--one-file-system`). +Mountpoints will appear as empty directories. + +Only applicable to reverse mode. + +Limitation: Mounted single files (yes this is possible) are NOT hidden. +  #### -rw, -ro  Mount the filesystem read-write (`-rw`, default) or read-only (`-ro`).  If both are specified, `-ro` takes precedence. diff --git a/cli_args.go b/cli_args.go index fa9e35c..1ece378 100644 --- a/cli_args.go +++ b/cli_args.go @@ -30,7 +30,7 @@ type argContainer struct {  	plaintextnames, quiet, nosyslog, wpanic,  	longnames, allow_other, reverse, aessiv, nonempty, raw64,  	noprealloc, speed, hkdf, serialize_reads, forcedecode, hh, info, -	sharedstorage, devrandom, fsck bool +	sharedstorage, devrandom, fsck, one_file_system bool  	// Mount options with opposites  	dev, nodev, suid, nosuid, exec, noexec, rw, ro, kernel_cache, acl bool  	masterkey, mountpoint, cipherdir, cpuprofile, @@ -178,6 +178,7 @@ func parseCliOpts(osArgs []string) (args argContainer) {  	flagSet.BoolVar(&args.sharedstorage, "sharedstorage", false, "Make concurrent access to a shared CIPHERDIR safer")  	flagSet.BoolVar(&args.devrandom, "devrandom", false, "Use /dev/random for generating master key")  	flagSet.BoolVar(&args.fsck, "fsck", false, "Run a filesystem check on CIPHERDIR") +	flagSet.BoolVar(&args.one_file_system, "one-file-system", false, "Don't cross filesystem boundaries")  	// Mount options with opposites  	flagSet.BoolVar(&args.dev, "dev", false, "Allow device files") diff --git a/crossbuild.bash b/crossbuild.bash index 0904d54..43bfd32 100755 --- a/crossbuild.bash +++ b/crossbuild.bash @@ -5,7 +5,8 @@  cd "$(dirname "$0")"  export GO111MODULE=on -B="go build -tags without_openssl" +# Discard resulting binary by writing to /dev/null +B="go build -tags without_openssl -o /dev/null"  set -x @@ -26,6 +27,3 @@ GOOS=darwin GOARCH=amd64 $B  if go tool dist list | grep ios/arm64 ; then  	GOOS=darwin GOARCH=arm64 $B  fi - -# The cross-built binary is not useful on the compile host. -rm gocryptfs diff --git a/internal/fusefrontend/args.go b/internal/fusefrontend/args.go index ae1c30c..d92c3ff 100644 --- a/internal/fusefrontend/args.go +++ b/internal/fusefrontend/args.go @@ -49,4 +49,8 @@ type Args struct {  	// SharedStorage disables caching & hard link tracking,  	// enabled via cli flag "-sharedstorage"  	SharedStorage bool +	// OneFileSystem disables crossing filesystem boundaries, +	// like rsync's `--one-file-system` does. +	// Only applicable to reverse mode. +	OneFileSystem bool  } diff --git a/internal/fusefrontend_reverse/node.go b/internal/fusefrontend_reverse/node.go index 787b99b..1b2fd67 100644 --- a/internal/fusefrontend_reverse/node.go +++ b/internal/fusefrontend_reverse/node.go @@ -22,6 +22,10 @@ import (  // in a `gocryptfs -reverse` mount.  type Node struct {  	fs.Inode +	// isOtherFilesystem is used for --one-filesystem. +	// It is set when the device number of this file or directory +	// is different from n.rootNode().rootDev. +	isOtherFilesystem bool  }  // Lookup - FUSE call for discovering a file. @@ -31,7 +35,14 @@ func (n *Node) Lookup(ctx context.Context, cName string, out *fuse.EntryOut) (ch  	if t == typeDiriv {  		// gocryptfs.diriv  		return n.lookupDiriv(ctx, out) -	} else if t == typeName { +	} +	rn := n.rootNode() +	if rn.args.OneFileSystem && n.isOtherFilesystem { +		// With --one-file-system, we present mountpoints as empty. That is, +		// it contains only a gocryptfs.diriv file (allowed above). +		return nil, syscall.ENOENT +	} +	if t == typeName {  		// gocryptfs.longname.*.name  		return n.lookupLongnameName(ctx, cName, out)  	} else if t == typeConfig { diff --git a/internal/fusefrontend_reverse/node_dir_ops.go b/internal/fusefrontend_reverse/node_dir_ops.go index c287284..21b9775 100644 --- a/internal/fusefrontend_reverse/node_dir_ops.go +++ b/internal/fusefrontend_reverse/node_dir_ops.go @@ -23,6 +23,22 @@ 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) { +	// Virtual files: at least one gocryptfs.diriv file +	virtualFiles := []fuse.DirEntry{ +		{Mode: virtualFileMode, Name: nametransform.DirIVFilename}, +	} +	rn := n.rootNode() + +	// This directory is a mountpoint. Present it as empty. +	if rn.args.OneFileSystem && n.isOtherFilesystem { +		if rn.args.PlaintextNames { +			return fs.NewListDirStream(nil), 0 +		} else { +			// An "empty" directory still has a gocryptfs.diriv file! +			return fs.NewListDirStream(virtualFiles), 0 +		} +	} +  	d, errno := n.prepareAtSyscall("")  	if errno != 0 {  		return @@ -41,8 +57,6 @@ func (n *Node) Readdir(ctx context.Context) (stream fs.DirStream, errno syscall.  		return nil, fs.ToErrno(err)  	} -	rn := n.rootNode() -  	// Filter out excluded entries  	entries = rn.excludeDirEntries(d, entries) @@ -50,11 +64,6 @@ func (n *Node) Readdir(ctx context.Context) (stream fs.DirStream, errno syscall.  		return n.readdirPlaintextnames(entries)  	} -	// Virtual files: at least one gocryptfs.diriv file -	virtualFiles := []fuse.DirEntry{ -		{Mode: virtualFileMode, Name: nametransform.DirIVFilename}, -	} -  	dirIV := pathiv.Derive(d.cPath, pathiv.PurposeDirIV)  	// Encrypt names  	for i := range entries { diff --git a/internal/fusefrontend_reverse/node_helpers.go b/internal/fusefrontend_reverse/node_helpers.go index 92f6a87..7b286a0 100644 --- a/internal/fusefrontend_reverse/node_helpers.go +++ b/internal/fusefrontend_reverse/node_helpers.go @@ -91,6 +91,7 @@ func (n *Node) prepareAtSyscall(child string) (d *dirfdPlus, errno syscall.Errno  // newChild attaches a new child inode to n.  // The passed-in `st` will be modified to get a unique inode number.  func (n *Node) newChild(ctx context.Context, st *syscall.Stat_t, out *fuse.EntryOut) *fs.Inode { +	isOtherFilesystem := (uint64(st.Dev) != n.rootNode().rootDev)  	// Get unique inode number  	rn := n.rootNode()  	rn.inoMap.TranslateStat(st) @@ -101,7 +102,9 @@ func (n *Node) newChild(ctx context.Context, st *syscall.Stat_t, out *fuse.Entry  		Gen:  1,  		Ino:  st.Ino,  	} -	node := &Node{} +	node := &Node{ +		isOtherFilesystem: isOtherFilesystem, +	}  	return n.NewInode(ctx, node, id)  } diff --git a/internal/fusefrontend_reverse/root_node.go b/internal/fusefrontend_reverse/root_node.go index b072f85..d57e1e6 100644 --- a/internal/fusefrontend_reverse/root_node.go +++ b/internal/fusefrontend_reverse/root_node.go @@ -36,17 +36,31 @@ type RootNode struct {  	// inoMap translates inode numbers from different devices to unique inode  	// numbers.  	inoMap *inomap.InoMap +	// rootDev stores the device number of the backing directory. Used for +	// --one-file-system. +	rootDev uint64  }  // NewRootNode returns an encrypted FUSE overlay filesystem.  // 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.NameTransform) *RootNode { +	var rootDev uint64 +	if args.OneFileSystem { +		var st syscall.Stat_t +		err := syscall.Stat(args.Cipherdir, &st) +		if err != nil { +			log.Panicf("Could not stat backing directory %q: %v", args.Cipherdir, err) +		} +		rootDev = uint64(st.Dev) +	} +  	rn := &RootNode{  		args:          args,  		nameTransform: n,  		contentEnc:    c,  		inoMap:        inomap.New(), +		rootDev:       rootDev,  	}  	if len(args.Exclude) > 0 || len(args.ExcludeWildcard) > 0 || len(args.ExcludeFrom) > 0 {  		rn.excluder = prepareExcluder(args) @@ -275,6 +275,7 @@ func initFuseFrontend(args *argContainer) (rootNode fs.InodeEmbedder, wipeKeys f  		Suid:            args.suid,  		KernelCache:     args.kernel_cache,  		SharedStorage:   args.sharedstorage, +		OneFileSystem:   args.one_file_system,  	}  	// confFile is nil when "-zerokey" or "-masterkey" was used  	if confFile != nil { diff --git a/tests/reverse/one_file_system_test.go b/tests/reverse/one_file_system_test.go new file mode 100644 index 0000000..5d8b76b --- /dev/null +++ b/tests/reverse/one_file_system_test.go @@ -0,0 +1,77 @@ +package reverse + +import ( +	"io/ioutil" +	"net/url" +	"os" +	"runtime" +	"syscall" +	"testing" + +	"github.com/rfjakob/gocryptfs/tests/test_helpers" +) + +func doTestOneFileSystem(t *testing.T, plaintextnames bool) { +	// Let's not explode with "TempDir: pattern contains path separator" +	myEscapedName := url.PathEscape(t.Name()) +	mnt, err := ioutil.TempDir(test_helpers.TmpDir, myEscapedName) +	if err != nil { +		t.Fatal(err) +	} +	cliArgs := []string{"-reverse", "-zerokey", "-one-file-system"} +	if plaintextnames { +		cliArgs = append(cliArgs, "-plaintextnames") +	} +	test_helpers.MountOrFatal(t, "/", mnt, cliArgs...) +	defer test_helpers.UnmountErr(mnt) + +	// Copied from inomap +	const maxPassthruIno = 1<<48 - 1 + +	entries, err := os.ReadDir(mnt) +	if err != nil { +		t.Fatal(err) +	} +	mountpoints := []string{} +	for _, e := range entries { +		i, err := e.Info() +		if err != nil { +			continue +		} +		if !e.IsDir() { +			// We are only interested in directories +			continue +		} +		st := i.Sys().(*syscall.Stat_t) +		// The inode numbers of files with a different device number are remapped +		// to something above maxPassthruIno +		if st.Ino > maxPassthruIno { +			mountpoints = append(mountpoints, e.Name()) +		} +	} +	if len(mountpoints) == 0 { +		t.Skip("no mountpoints found, nothing to test") +	} +	for _, m := range mountpoints { +		e, err := os.ReadDir(mnt + "/" + m) +		if err != nil { +			t.Error(err) +		} +		expected := 1 +		if plaintextnames { +			expected = 0 +		} +		if len(e) != expected { +			t.Errorf("mountpoint %q does not look empty: %v", m, e) +		} +	} +	t.Logf("tested %d mountpoints: %v", len(mountpoints), mountpoints) +} + +func TestOneFileSystem(t *testing.T) { +	if runtime.GOOS != "linux" { +		t.Skip("only works on linux") +	} +	t.Run("normal", func(t *testing.T) { doTestOneFileSystem(t, false) }) +	t.Run("plaintextnames", func(t *testing.T) { doTestOneFileSystem(t, true) }) +} | 
