From b2724070d95234a8cd281f275211e0f827a8bbe1 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Mon, 16 Aug 2021 18:40:48 +0200 Subject: reverse mode: implement -one-file-system Fixes https://github.com/rfjakob/gocryptfs/issues/475 --- Documentation/MANPAGE.md | 8 +++ cli_args.go | 3 +- crossbuild.bash | 6 +-- internal/fusefrontend/args.go | 4 ++ internal/fusefrontend_reverse/node.go | 13 ++++- internal/fusefrontend_reverse/node_dir_ops.go | 23 +++++--- internal/fusefrontend_reverse/node_helpers.go | 5 +- internal/fusefrontend_reverse/root_node.go | 14 +++++ mount.go | 1 + tests/reverse/one_file_system_test.go | 77 +++++++++++++++++++++++++++ 10 files changed, 140 insertions(+), 14 deletions(-) create mode 100644 tests/reverse/one_file_system_test.go 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) diff --git a/mount.go b/mount.go index 3f190d4..ababa81 100644 --- a/mount.go +++ b/mount.go @@ -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) }) +} -- cgit v1.2.3