diff options
| -rw-r--r-- | cli_args.go | 12 | ||||
| -rw-r--r-- | help.go | 1 | ||||
| -rw-r--r-- | internal/fusefrontend/fs.go | 5 | ||||
| -rw-r--r-- | internal/fusefrontend/names.go | 6 | ||||
| -rw-r--r-- | internal/openfiletable/open_file_table.go | 8 | ||||
| -rw-r--r-- | mount.go | 72 | 
6 files changed, 91 insertions, 13 deletions
| diff --git a/cli_args.go b/cli_args.go index dd00658..c073958 100644 --- a/cli_args.go +++ b/cli_args.go @@ -7,6 +7,7 @@ import (  	"os"  	"strconv"  	"strings" +	"time"  	"github.com/hanwen/go-fuse/fuse"  	"github.com/rfjakob/gocryptfs/internal/configfile" @@ -33,6 +34,8 @@ type argContainer struct {  	// Configuration file name override  	config             string  	notifypid, scryptn int +	// Idle time before autounmount +	idle time.Duration  	// Helper variables that are NOT cli options all start with an underscore  	// _configCustom is true when the user sets a custom config file name.  	_configCustom bool @@ -187,6 +190,11 @@ func parseCliOpts() (args argContainer) {  		"successful mount - used internally for daemonization")  	flagSet.IntVar(&args.scryptn, "scryptn", configfile.ScryptDefaultLogN, "scrypt cost parameter logN. Possible values: 10-28. "+  		"A lower value speeds up mounting and reduces its memory needs, but makes the password susceptible to brute-force attacks") + +	flagSet.DurationVar(&args.idle, "i", 0, "Alias for -idle") +	flagSet.DurationVar(&args.idle, "idle", 0, "Auto-unmount after specified idle duration (ignored in reverse mode). "+ +		"Durations are specified like \"500s\" or \"2h45m\". 0 means stay mounted indefinitely.") +  	var dummyString string  	flagSet.StringVar(&dummyString, "o", "", "For compatibility with mount(1), options can be also passed as a comma-separated list to -o on the end.")  	// Actual parsing @@ -247,6 +255,10 @@ func parseCliOpts() (args argContainer) {  		tlog.Fatal.Printf("The options -extpass and -trezor cannot be used at the same time")  		os.Exit(exitcodes.Usage)  	} +	if args.idle < 0 { +		tlog.Fatal.Printf("Idle timeout cannot be less than 0") +		os.Exit(exitcodes.Usage) +	}  	return args  } @@ -19,6 +19,7 @@ func helpShort() {  Common Options (use -hh to show all):    -aessiv            Use AES-SIV encryption (with -init)    -allow_other       Allow other users to access the mount +  -i, -idle          Unmount automatically after specified idle duration    -config            Custom path to config file    -ctlsock           Create control socket at location    -extpass           Call external program to prompt for the password diff --git a/internal/fusefrontend/fs.go b/internal/fusefrontend/fs.go index 1b6941e..c0d6151 100644 --- a/internal/fusefrontend/fs.go +++ b/internal/fusefrontend/fs.go @@ -47,6 +47,11 @@ type FS struct {  	// "gocryptfs -fsck" reads from the channel to also catch these transparently-  	// mitigated corruptions.  	MitigatedCorruptions chan string +	// Track accesses to the filesystem so that we can know when to autounmount. +	// An access is considered to have happened on every call to encryptPath, +	// which is called as part of every filesystem operation. +	// (This flag uses a uint32 so that it can be reset with CompareAndSwapUint32.) +	AccessedSinceLastCheck uint32  }  var _ pathfs.FileSystem = &FS{} // Verify that interface is implemented. diff --git a/internal/fusefrontend/names.go b/internal/fusefrontend/names.go index 5530e3e..3bf64d5 100644 --- a/internal/fusefrontend/names.go +++ b/internal/fusefrontend/names.go @@ -60,9 +60,15 @@ func (fs *FS) openBackingDir(relPath string) (dirfd int, cName string, err error  // encryptPath - encrypt relative plaintext path  func (fs *FS) encryptPath(plainPath string) (string, error) { +	if plainPath != "" { // Empty path gets encrypted all the time without actual file accesses. +		fs.AccessedSinceLastCheck = 1 +	} else { // Empty string gets encrypted as empty string +		return plainPath, nil +	}  	if fs.args.PlaintextNames {  		return plainPath, nil  	} +  	fs.dirIVLock.RLock()  	cPath, err := fs.nameTransform.EncryptPathDirIV(plainPath, fs.args.Cipherdir)  	tlog.Debug.Printf("encryptPath '%s' -> '%s' (err: %v)", plainPath, cPath, err) diff --git a/internal/openfiletable/open_file_table.go b/internal/openfiletable/open_file_table.go index e21c96d..4a8ce28 100644 --- a/internal/openfiletable/open_file_table.go +++ b/internal/openfiletable/open_file_table.go @@ -112,3 +112,11 @@ func (c *countingMutex) Lock() {  func WriteOpCount() uint64 {  	return atomic.LoadUint64(&t.writeOpCount)  } + +// CountOpenFiles returns how many entries are currently in the table +// in a threadsafe manner. +func CountOpenFiles() int { +	t.Lock() +	defer t.Unlock() +	return len(t.entries) +} @@ -5,6 +5,7 @@ import (  	"fmt"  	"log"  	"log/syslog" +	"math"  	"net"  	"os"  	"os/exec" @@ -14,6 +15,7 @@ import (  	"runtime"  	"runtime/debug"  	"strings" +	"sync/atomic"  	"syscall"  	"time" @@ -29,6 +31,7 @@ import (  	"github.com/rfjakob/gocryptfs/internal/fusefrontend"  	"github.com/rfjakob/gocryptfs/internal/fusefrontend_reverse"  	"github.com/rfjakob/gocryptfs/internal/nametransform" +	"github.com/rfjakob/gocryptfs/internal/openfiletable"  	"github.com/rfjakob/gocryptfs/internal/tlog"  ) @@ -98,7 +101,7 @@ func doMount(args *argContainer) {  	fs, wipeKeys := initFuseFrontend(args)  	// Initialize go-fuse FUSE server  	srv := initGoFuse(fs, args) -	// Try to wipe secrect keys from memory after unmount +	// Try to wipe secret keys from memory after unmount  	defer wipeKeys()  	tlog.Info.Println(tlog.ColorGreen + "Filesystem mounted and ready." + tlog.ColorReset) @@ -137,10 +140,49 @@ func doMount(args *argContainer) {  	// Return memory that was allocated for scrypt (64M by default!) and other  	// stuff that is no longer needed to the OS  	debug.FreeOSMemory() +	// Set up autounmount, if requested. +	if args.idle > 0 && !args.reverse { +		// Not being in reverse mode means we always have a forward file system. +		fwdFs := fs.(*fusefrontend.FS) +		go idleMonitor(args.idle, fwdFs, srv, args.mountpoint) +	}  	// Jump into server loop. Returns when it gets an umount request from the kernel.  	srv.Serve()  } +// Based on the EncFS idle monitor: +// https://github.com/vgough/encfs/blob/1974b417af189a41ffae4c6feb011d2a0498e437/encfs/main.cpp#L851 +// idleMonitor is a function to be run as a thread that checks for +// filesystem idleness and unmounts if we've been idle for long enough. +const checksDuringTimeoutPeriod = 4 + +func idleMonitor(idleTimeout time.Duration, fs *fusefrontend.FS, srv *fuse.Server, mountpoint string) { +	sleepTimeBetweenChecks := contentenc.MinUint64( +		uint64(idleTimeout/checksDuringTimeoutPeriod), +		uint64(2*time.Minute)) +	timeoutCycles := int(math.Ceil(float64(idleTimeout) / float64(sleepTimeBetweenChecks))) +	idleCount := 0 +	for { +		// Atomically check whether the access flag is set and reset it to 0 if so. +		recentAccess := atomic.CompareAndSwapUint32(&fs.AccessedSinceLastCheck, 1, 0) +		// Any form of current or recent access resets the idle counter. +		openFileCount := openfiletable.CountOpenFiles() +		if recentAccess || openFileCount > 0 { +			idleCount = 0 +		} else { +			idleCount++ +		} +		tlog.Debug.Printf( +			"Checking for idle (recentAccess = %t, open = %d): %s", +			recentAccess, openFileCount, time.Now().String()) +		if idleCount > 0 && idleCount%timeoutCycles == 0 { +			tlog.Info.Printf("Filesystem idle; unmounting: %s", mountpoint) +			unmount(srv, mountpoint) +		} +		time.Sleep(time.Duration(sleepTimeBetweenChecks)) +	} +} +  // setOpenFileLimit tries to increase the open file limit to 4096 (the default hard  // limit on Linux).  func setOpenFileLimit() { @@ -379,18 +421,22 @@ func handleSigint(srv *fuse.Server, mountpoint string) {  	signal.Notify(ch, syscall.SIGTERM)  	go func() {  		<-ch -		err := srv.Unmount() -		if err != nil { -			tlog.Warn.Printf("handleSigint: srv.Unmount returned %v", err) -			if runtime.GOOS == "linux" { -				// MacOSX does not support lazy unmount -				tlog.Info.Printf("Trying lazy unmount") -				cmd := exec.Command("fusermount", "-u", "-z", mountpoint) -				cmd.Stdout = os.Stdout -				cmd.Stderr = os.Stderr -				cmd.Run() -			} -		} +		unmount(srv, mountpoint)  		os.Exit(exitcodes.SigInt)  	}()  } + +func unmount(srv *fuse.Server, mountpoint string) { +	err := srv.Unmount() +	if err != nil { +		tlog.Warn.Printf("unmount: srv.Unmount returned %v", err) +		if runtime.GOOS == "linux" { +			// MacOSX does not support lazy unmount +			tlog.Info.Printf("Trying lazy unmount") +			cmd := exec.Command("fusermount", "-u", "-z", mountpoint) +			cmd.Stdout = os.Stdout +			cmd.Stderr = os.Stderr +			cmd.Run() +		} +	} +} | 
