reverse mode: implement -one-file-system

Fixes https://github.com/rfjakob/gocryptfs/issues/475
This commit is contained in:
Jakob Unterwurzacher 2021-08-16 18:40:48 +02:00
parent ad4b99170b
commit b2724070d9
10 changed files with 140 additions and 14 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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