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 } diff --git a/help.go b/help.go index 714fcbf..6216a9b 100644 --- a/help.go +++ b/help.go @@ -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) +} diff --git a/mount.go b/mount.go index f473e1e..b4b6e61 100644 --- a/mount.go +++ b/mount.go @@ -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() + } + } +}