summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakob Unterwurzacher2021-08-16 18:40:48 +0200
committerJakob Unterwurzacher2021-08-16 19:23:58 +0200
commitb2724070d95234a8cd281f275211e0f827a8bbe1 (patch)
tree0c4efc4714d0826ac99ec40c905a111c9f9c7015
parentad4b99170b9ad438909f5cba8c32109a18697a7a (diff)
reverse mode: implement -one-file-system
Fixes https://github.com/rfjakob/gocryptfs/issues/475
-rw-r--r--Documentation/MANPAGE.md8
-rw-r--r--cli_args.go3
-rwxr-xr-xcrossbuild.bash6
-rw-r--r--internal/fusefrontend/args.go4
-rw-r--r--internal/fusefrontend_reverse/node.go13
-rw-r--r--internal/fusefrontend_reverse/node_dir_ops.go23
-rw-r--r--internal/fusefrontend_reverse/node_helpers.go5
-rw-r--r--internal/fusefrontend_reverse/root_node.go14
-rw-r--r--mount.go1
-rw-r--r--tests/reverse/one_file_system_test.go77
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)
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) })
+}