Add option for autounmount

Even though filesystem notifications aren't implemented for FUSE, I decided to
try my hand at implementing the autounmount feature (#128). I based it on the
EncFS autounmount code, which records filesystem accesses and checks every X
seconds whether it's idled long enough to unmount.

I've tested the feature locally, but I haven't added any tests for this flag.
I also haven't worked with Go before. So please let me know if there's
anything that should be done differently.

One particular concern: I worked from the assumption that the open files table
is unique per-filesystem. If that's not true, I'll need to add an open file
count and associated lock to the Filesystem type instead.

https://github.com/rfjakob/gocryptfs/pull/265
This commit is contained in:
Jesse Dunietz 2018-10-06 15:49:33 -04:00 committed by Jakob Unterwurzacher
parent 57a5a8791f
commit 87d3ed9187
6 changed files with 91 additions and 13 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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