Jakob Unterwurzacher e8d8ae54d3 fusefrontend: use OpenDirNofollow in openBackingDir
Rename openBackingPath to openBackingDir and use OpenDirNofollow
to be safe against symlink races. Note that openBackingDir is
not used in several important code paths like Create().

But it is used in Unlink, and the performance impact in the RM benchmark
to be acceptable:

Before

	$ ./benchmark.bash
	Testing gocryptfs at /tmp/benchmark.bash.bYO: gocryptfs v1.6-12-g930c37e-dirty; go-fuse v20170619-49-gb11e293; 2018-09-08 go1.10.3
	WRITE: 262144000 bytes (262 MB, 250 MiB) copied, 1.07979 s, 243 MB/s
	READ:  262144000 bytes (262 MB, 250 MiB) copied, 0.882413 s, 297 MB/s
	UNTAR: 16.703
	MD5:   7.606
	LS:    1.349
	RM:    3.237

After

	$ ./benchmark.bash
	Testing gocryptfs at /tmp/benchmark.bash.jK3: gocryptfs v1.6-13-g84d6faf-dirty; go-fuse v20170619-49-gb11e293; 2018-09-08 go1.10.3
	WRITE: 262144000 bytes (262 MB, 250 MiB) copied, 1.06261 s, 247 MB/s
	READ:  262144000 bytes (262 MB, 250 MiB) copied, 0.947228 s, 277 MB/s
	UNTAR: 17.197
	MD5:   7.540
	LS:    1.364
	RM:    3.410
2018-09-08 19:27:33 +02:00

638 lines
20 KiB
Go

// Package fusefrontend interfaces directly with the go-fuse library.
package fusefrontend
// FUSE operations on paths
import (
"os"
"path/filepath"
"sync"
"syscall"
"time"
"golang.org/x/sys/unix"
"github.com/hanwen/go-fuse/fuse"
"github.com/hanwen/go-fuse/fuse/nodefs"
"github.com/hanwen/go-fuse/fuse/pathfs"
"github.com/rfjakob/gocryptfs/internal/contentenc"
"github.com/rfjakob/gocryptfs/internal/nametransform"
"github.com/rfjakob/gocryptfs/internal/serialize_reads"
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
"github.com/rfjakob/gocryptfs/internal/tlog"
)
// FS implements the go-fuse virtual filesystem interface.
type FS struct {
pathfs.FileSystem // loopbackFileSystem, see go-fuse/fuse/pathfs/loopback.go
args Args // Stores configuration arguments
// dirIVLock: Lock()ed if any "gocryptfs.diriv" file is modified
// Readers must RLock() it to prevent them from seeing intermediate
// states
dirIVLock sync.RWMutex
// Filename encryption helper
nameTransform *nametransform.NameTransform
// Content encryption helper
contentEnc *contentenc.ContentEnc
// This lock is used by openWriteOnlyFile() to block concurrent opens while
// it relaxes the permissions on a file.
openWriteOnlyLock sync.RWMutex
// MitigatedCorruptions is used to report data corruption that is internally
// mitigated by ignoring the corrupt item. For example, when OpenDir() finds
// a corrupt filename, we still return the other valid filenames.
// The corruption is logged to syslog to inform the user, and in addition,
// the corrupt filename is logged to this channel via
// reportMitigatedCorruption().
// "gocryptfs -fsck" reads from the channel to also catch these transparently-
// mitigated corruptions.
MitigatedCorruptions chan string
}
var _ pathfs.FileSystem = &FS{} // Verify that interface is implemented.
// NewFS returns a new encrypted FUSE overlay filesystem.
func NewFS(args Args, c *contentenc.ContentEnc, n *nametransform.NameTransform) *FS {
if args.SerializeReads {
serialize_reads.InitSerializer()
}
if len(args.Exclude) > 0 {
tlog.Warn.Printf("Forward mode does not support -exclude")
}
return &FS{
FileSystem: pathfs.NewLoopbackFileSystem(args.Cipherdir),
args: args,
nameTransform: n,
contentEnc: c,
}
}
// GetAttr implements pathfs.Filesystem.
func (fs *FS) GetAttr(name string, context *fuse.Context) (*fuse.Attr, fuse.Status) {
tlog.Debug.Printf("FS.GetAttr('%s')", name)
if fs.isFiltered(name) {
return nil, fuse.EPERM
}
cName, err := fs.encryptPath(name)
if err != nil {
return nil, fuse.ToStatus(err)
}
a, status := fs.FileSystem.GetAttr(cName, context)
if a == nil {
tlog.Debug.Printf("FS.GetAttr failed: %s", status.String())
return a, status
}
if a.IsRegular() {
a.Size = fs.contentEnc.CipherSizeToPlainSize(a.Size)
} else if a.IsSymlink() {
target, _ := fs.Readlink(name, context)
a.Size = uint64(len(target))
}
if fs.args.ForceOwner != nil {
a.Owner = *fs.args.ForceOwner
}
return a, status
}
// mangleOpenFlags is used by Create() and Open() to convert the open flags the user
// wants to the flags we internally use to open the backing file.
func (fs *FS) mangleOpenFlags(flags uint32) (newFlags int) {
newFlags = int(flags)
// Convert WRONLY to RDWR. We always need read access to do read-modify-write cycles.
if newFlags&os.O_WRONLY > 0 {
newFlags = newFlags ^ os.O_WRONLY | os.O_RDWR
}
// We also cannot open the file in append mode, we need to seek back for RMW
newFlags = newFlags &^ os.O_APPEND
// O_DIRECT accesses must be aligned in both offset and length. Due to our
// crypto header, alignment will be off, even if userspace makes aligned
// accesses. Running xfstests generic/013 on ext4 used to trigger lots of
// EINVAL errors due to missing alignment. Just fall back to buffered IO.
newFlags = newFlags &^ syscallcompat.O_DIRECT
return newFlags
}
// Open implements pathfs.Filesystem.
func (fs *FS) Open(path string, flags uint32, context *fuse.Context) (fuseFile nodefs.File, status fuse.Status) {
if fs.isFiltered(path) {
return nil, fuse.EPERM
}
// Taking this lock makes sure we don't race openWriteOnlyFile()
fs.openWriteOnlyLock.RLock()
defer fs.openWriteOnlyLock.RUnlock()
newFlags := fs.mangleOpenFlags(flags)
cPath, err := fs.getBackingPath(path)
if err != nil {
tlog.Debug.Printf("Open: getBackingPath: %v", err)
return nil, fuse.ToStatus(err)
}
tlog.Debug.Printf("Open: %s", cPath)
f, err := os.OpenFile(cPath, newFlags, 0)
if err != nil {
sysErr := err.(*os.PathError).Err
if sysErr == syscall.EMFILE {
var lim syscall.Rlimit
syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim)
tlog.Warn.Printf("Open %q: too many open files. Current \"ulimit -n\": %d", cPath, lim.Cur)
}
if sysErr == syscall.EACCES && (int(flags)&os.O_WRONLY > 0) {
return fs.openWriteOnlyFile(cPath, newFlags)
}
return nil, fuse.ToStatus(err)
}
return NewFile(f, fs)
}
// Due to RMW, we always need read permissions on the backing file. This is a
// problem if the file permissions do not allow reading (i.e. 0200 permissions).
// This function works around that problem by chmod'ing the file, obtaining a fd,
// and chmod'ing it back.
func (fs *FS) openWriteOnlyFile(cPath string, newFlags int) (fuseFile nodefs.File, status fuse.Status) {
woFd, err := os.OpenFile(cPath, os.O_WRONLY, 0)
if err != nil {
return nil, fuse.ToStatus(err)
}
defer woFd.Close()
fi, err := woFd.Stat()
if err != nil {
return nil, fuse.ToStatus(err)
}
perms := fi.Mode().Perm()
// Verify that we don't have read permissions
if perms&0400 != 0 {
tlog.Warn.Printf("openWriteOnlyFile: unexpected permissions %#o, returning EPERM", perms)
return nil, fuse.ToStatus(syscall.EPERM)
}
// Upgrade the lock to block other Open()s and downgrade again on return
fs.openWriteOnlyLock.RUnlock()
fs.openWriteOnlyLock.Lock()
defer func() {
fs.openWriteOnlyLock.Unlock()
fs.openWriteOnlyLock.RLock()
}()
// Relax permissions and revert on return
err = woFd.Chmod(perms | 0400)
if err != nil {
tlog.Warn.Printf("openWriteOnlyFile: changing permissions failed: %v", err)
return nil, fuse.ToStatus(err)
}
defer func() {
err2 := woFd.Chmod(perms)
if err2 != nil {
tlog.Warn.Printf("openWriteOnlyFile: reverting permissions failed: %v", err2)
}
}()
rwFd, err := os.OpenFile(cPath, newFlags, 0)
if err != nil {
return nil, fuse.ToStatus(err)
}
return NewFile(rwFd, fs)
}
// Create implements pathfs.Filesystem.
func (fs *FS) Create(path string, flags uint32, mode uint32, context *fuse.Context) (fuseFile nodefs.File, code fuse.Status) {
if fs.isFiltered(path) {
return nil, fuse.EPERM
}
newFlags := fs.mangleOpenFlags(flags)
cPath, err := fs.getBackingPath(path)
if err != nil {
return nil, fuse.ToStatus(err)
}
var fd *os.File
cName := filepath.Base(cPath)
// Handle long file name
if !fs.args.PlaintextNames && nametransform.IsLongContent(cName) {
var dirfd *os.File
dirfd, err = os.Open(filepath.Dir(cPath))
if err != nil {
return nil, fuse.ToStatus(err)
}
defer dirfd.Close()
// Create ".name"
err = fs.nameTransform.WriteLongName(dirfd, cName, path)
if err != nil {
return nil, fuse.ToStatus(err)
}
// Create content
var fdRaw int
fdRaw, err = syscallcompat.Openat(int(dirfd.Fd()), cName, newFlags|os.O_CREATE|os.O_EXCL, mode)
if err != nil {
nametransform.DeleteLongName(dirfd, cName)
return nil, fuse.ToStatus(err)
}
fd = os.NewFile(uintptr(fdRaw), cName)
} else {
// Normal (short) file name
fd, err = os.OpenFile(cPath, newFlags|os.O_CREATE|os.O_EXCL, os.FileMode(mode))
if err != nil {
return nil, fuse.ToStatus(err)
}
}
// Set owner
if fs.args.PreserveOwner {
err = fd.Chown(int(context.Owner.Uid), int(context.Owner.Gid))
if err != nil {
tlog.Warn.Printf("Create: fd.Chown failed: %v", err)
}
}
return NewFile(fd, fs)
}
// Chmod implements pathfs.Filesystem.
func (fs *FS) Chmod(path string, mode uint32, context *fuse.Context) (code fuse.Status) {
if fs.isFiltered(path) {
return fuse.EPERM
}
dirfd, cName, err := fs.openBackingDir(path)
if err != nil {
return fuse.ToStatus(err)
}
defer dirfd.Close()
// os.Chmod goes through the "syscallMode" translation function that messes
// up the suid and sgid bits. So use a syscall directly.
err = syscallcompat.Fchmodat(int(dirfd.Fd()), cName, mode, unix.AT_SYMLINK_NOFOLLOW)
return fuse.ToStatus(err)
}
// Chown implements pathfs.Filesystem.
func (fs *FS) Chown(path string, uid uint32, gid uint32, context *fuse.Context) (code fuse.Status) {
if fs.isFiltered(path) {
return fuse.EPERM
}
dirfd, cName, err := fs.openBackingDir(path)
if err != nil {
return fuse.ToStatus(err)
}
defer dirfd.Close()
code = fuse.ToStatus(syscallcompat.Fchownat(int(dirfd.Fd()), cName, int(uid), int(gid), unix.AT_SYMLINK_NOFOLLOW))
if !code.Ok() {
return code
}
if !fs.args.PlaintextNames {
// When filename encryption is active, every directory contains
// a "gocryptfs.diriv" file. This file should also change the owner.
// Instead of checking if "cName" is a directory, we just blindly
// execute the chown on "cName/gocryptfs.diriv" and ignore errors.
dirIVPath := filepath.Join(cName, nametransform.DirIVFilename)
syscallcompat.Fchownat(int(dirfd.Fd()), dirIVPath, int(uid), int(gid), unix.AT_SYMLINK_NOFOLLOW)
}
return fuse.OK
}
// Mknod implements pathfs.Filesystem.
func (fs *FS) Mknod(path string, mode uint32, dev uint32, context *fuse.Context) (code fuse.Status) {
if fs.isFiltered(path) {
return fuse.EPERM
}
dirfd, cName, err := fs.openBackingDir(path)
if err != nil {
return fuse.ToStatus(err)
}
defer dirfd.Close()
// Create ".name" file to store long file name (except in PlaintextNames mode)
if !fs.args.PlaintextNames && nametransform.IsLongContent(cName) {
err = fs.nameTransform.WriteLongName(dirfd, cName, path)
if err != nil {
return fuse.ToStatus(err)
}
// Create "gocryptfs.longfile." device node
err = syscallcompat.Mknodat(int(dirfd.Fd()), cName, mode, int(dev))
if err != nil {
nametransform.DeleteLongName(dirfd, cName)
}
} else {
// Create regular device node
err = syscallcompat.Mknodat(int(dirfd.Fd()), cName, mode, int(dev))
}
if err != nil {
return fuse.ToStatus(err)
}
// Set owner
if fs.args.PreserveOwner {
err = syscallcompat.Fchownat(int(dirfd.Fd()), cName, int(context.Owner.Uid),
int(context.Owner.Gid), unix.AT_SYMLINK_NOFOLLOW)
if err != nil {
tlog.Warn.Printf("Mknod: Fchownat failed: %v", err)
}
}
return fuse.OK
}
// Truncate implements pathfs.Filesystem.
// Support truncate(2) by opening the file and calling ftruncate(2)
// While the glibc "truncate" wrapper seems to always use ftruncate, fsstress from
// xfstests uses this a lot by calling "truncate64" directly.
func (fs *FS) Truncate(path string, offset uint64, context *fuse.Context) (code fuse.Status) {
file, code := fs.Open(path, uint32(os.O_RDWR), context)
if code != fuse.OK {
return code
}
code = file.Truncate(offset)
file.Release()
return code
}
// Utimens implements pathfs.Filesystem.
func (fs *FS) Utimens(path string, a *time.Time, m *time.Time, context *fuse.Context) (code fuse.Status) {
if fs.isFiltered(path) {
return fuse.EPERM
}
cPath, err := fs.encryptPath(path)
if err != nil {
return fuse.ToStatus(err)
}
return fs.FileSystem.Utimens(cPath, a, m, context)
}
// StatFs implements pathfs.Filesystem.
func (fs *FS) StatFs(path string) *fuse.StatfsOut {
if fs.isFiltered(path) {
return nil
}
cPath, err := fs.encryptPath(path)
if err != nil {
return nil
}
return fs.FileSystem.StatFs(cPath)
}
// decryptSymlinkTarget: "cData64" is base64-decoded and decrypted
// like file contents (GCM).
// The empty string decrypts to the empty string.
func (fs *FS) decryptSymlinkTarget(cData64 string) (string, error) {
if cData64 == "" {
return "", nil
}
cData, err := fs.nameTransform.B64.DecodeString(cData64)
if err != nil {
return "", err
}
data, err := fs.contentEnc.DecryptBlock([]byte(cData), 0, nil)
if err != nil {
return "", err
}
return string(data), nil
}
// Readlink implements pathfs.Filesystem.
func (fs *FS) Readlink(relPath string, context *fuse.Context) (out string, status fuse.Status) {
cPath, err := fs.encryptPath(relPath)
if err != nil {
return "", fuse.ToStatus(err)
}
cAbsPath := filepath.Join(fs.args.Cipherdir, cPath)
cTarget, err := os.Readlink(cAbsPath)
if err != nil {
return "", fuse.ToStatus(err)
}
if fs.args.PlaintextNames {
return cTarget, fuse.OK
}
// Symlinks are encrypted like file contents (GCM) and base64-encoded
target, err := fs.decryptSymlinkTarget(cTarget)
if err != nil {
tlog.Warn.Printf("Readlink %q: decrypting target failed: %v", cPath, err)
return "", fuse.EIO
}
return string(target), fuse.OK
}
// Unlink implements pathfs.Filesystem.
func (fs *FS) Unlink(path string, context *fuse.Context) (code fuse.Status) {
if fs.isFiltered(path) {
return fuse.EPERM
}
dirfd, cName, err := fs.openBackingDir(path)
if err != nil {
return fuse.ToStatus(err)
}
defer dirfd.Close()
// Delete content
err = syscallcompat.Unlinkat(int(dirfd.Fd()), cName, 0)
if err != nil {
return fuse.ToStatus(err)
}
// Delete ".name" file
if !fs.args.PlaintextNames && nametransform.IsLongContent(cName) {
err = nametransform.DeleteLongName(dirfd, cName)
if err != nil {
tlog.Warn.Printf("Unlink: could not delete .name file: %v", err)
}
}
return fuse.ToStatus(err)
}
// encryptSymlinkTarget: "data" is encrypted like file contents (GCM)
// and base64-encoded.
// The empty string encrypts to the empty string.
func (fs *FS) encryptSymlinkTarget(data string) (cData64 string) {
if data == "" {
return ""
}
cData := fs.contentEnc.EncryptBlock([]byte(data), 0, nil)
cData64 = fs.nameTransform.B64.EncodeToString(cData)
return cData64
}
// Symlink implements pathfs.Filesystem.
func (fs *FS) Symlink(target string, linkName string, context *fuse.Context) (code fuse.Status) {
tlog.Debug.Printf("Symlink(\"%s\", \"%s\")", target, linkName)
if fs.isFiltered(linkName) {
return fuse.EPERM
}
dirfd, cName, err := fs.openBackingDir(linkName)
if err != nil {
return fuse.ToStatus(err)
}
defer dirfd.Close()
cTarget := target
if !fs.args.PlaintextNames {
// Symlinks are encrypted like file contents (GCM) and base64-encoded
cTarget = fs.encryptSymlinkTarget(target)
}
// Create ".name" file to store long file name (except in PlaintextNames mode)
if !fs.args.PlaintextNames && nametransform.IsLongContent(cName) {
err = fs.nameTransform.WriteLongName(dirfd, cName, linkName)
if err != nil {
return fuse.ToStatus(err)
}
// Create "gocryptfs.longfile." symlink
err = syscallcompat.Symlinkat(cTarget, int(dirfd.Fd()), cName)
if err != nil {
nametransform.DeleteLongName(dirfd, cName)
}
} else {
// Create symlink
err = syscallcompat.Symlinkat(cTarget, int(dirfd.Fd()), cName)
}
if err != nil {
return fuse.ToStatus(err)
}
// Set owner
if fs.args.PreserveOwner {
err = syscallcompat.Fchownat(int(dirfd.Fd()), cName, int(context.Owner.Uid),
int(context.Owner.Gid), unix.AT_SYMLINK_NOFOLLOW)
if err != nil {
tlog.Warn.Printf("Symlink: Fchownat failed: %v", err)
}
}
return fuse.OK
}
// Rename implements pathfs.Filesystem.
func (fs *FS) Rename(oldPath string, newPath string, context *fuse.Context) (code fuse.Status) {
if fs.isFiltered(newPath) {
return fuse.EPERM
}
cOldPath, err := fs.getBackingPath(oldPath)
if err != nil {
return fuse.ToStatus(err)
}
cNewPath, err := fs.getBackingPath(newPath)
if err != nil {
return fuse.ToStatus(err)
}
// The Rename may cause a directory to take the place of another directory.
// That directory may still be in the DirIV cache, clear it.
fs.nameTransform.DirIVCache.Clear()
// Easy case.
if fs.args.PlaintextNames {
return fuse.ToStatus(syscall.Rename(cOldPath, cNewPath))
}
// Handle long source file name
var oldDirFd *os.File
var finalOldDirFd int
var finalOldPath = cOldPath
cOldName := filepath.Base(cOldPath)
if nametransform.IsLongContent(cOldName) {
oldDirFd, err = os.Open(filepath.Dir(cOldPath))
if err != nil {
return fuse.ToStatus(err)
}
defer oldDirFd.Close()
finalOldDirFd = int(oldDirFd.Fd())
// Use relative path
finalOldPath = cOldName
}
// Handle long destination file name
var newDirFd *os.File
var finalNewDirFd int
var finalNewPath = cNewPath
cNewName := filepath.Base(cNewPath)
if nametransform.IsLongContent(cNewName) {
newDirFd, err = os.Open(filepath.Dir(cNewPath))
if err != nil {
return fuse.ToStatus(err)
}
defer newDirFd.Close()
finalNewDirFd = int(newDirFd.Fd())
// Use relative path
finalNewPath = cNewName
// Create destination .name file
err = fs.nameTransform.WriteLongName(newDirFd, cNewName, newPath)
// Failure to write the .name file is expected when the target path already
// exists. Since hashes are pretty unique, there is no need to modify the
// file anyway. We still set newDirFd to nil to ensure that we do not delete
// the file on error.
if err == syscall.EEXIST {
newDirFd = nil
} else if err != nil {
return fuse.ToStatus(err)
}
}
// Actual rename
tlog.Debug.Printf("Renameat oldfd=%d oldpath=%s newfd=%d newpath=%s\n", finalOldDirFd, finalOldPath, finalNewDirFd, finalNewPath)
err = syscallcompat.Renameat(finalOldDirFd, finalOldPath, finalNewDirFd, finalNewPath)
if err == syscall.ENOTEMPTY || err == syscall.EEXIST {
// If an empty directory is overwritten we will always get an error as
// the "empty" directory will still contain gocryptfs.diriv.
// Interestingly, ext4 returns ENOTEMPTY while xfs returns EEXIST.
// We handle that by trying to fs.Rmdir() the target directory and trying
// again.
tlog.Debug.Printf("Rename: Handling ENOTEMPTY")
if fs.Rmdir(newPath, context) == fuse.OK {
err = syscallcompat.Renameat(finalOldDirFd, finalOldPath, finalNewDirFd, finalNewPath)
}
}
if err != nil {
if newDirFd != nil {
// Roll back .name creation
nametransform.DeleteLongName(newDirFd, cNewName)
}
return fuse.ToStatus(err)
}
if oldDirFd != nil {
nametransform.DeleteLongName(oldDirFd, cOldName)
}
return fuse.OK
}
// Link implements pathfs.Filesystem.
func (fs *FS) Link(oldPath string, newPath string, context *fuse.Context) (code fuse.Status) {
if fs.isFiltered(newPath) {
return fuse.EPERM
}
oldDirFd, cOldName, err := fs.openBackingDir(oldPath)
if err != nil {
return fuse.ToStatus(err)
}
defer oldDirFd.Close()
newDirFd, cNewName, err := fs.openBackingDir(newPath)
if err != nil {
return fuse.ToStatus(err)
}
defer newDirFd.Close()
// Handle long file name (except in PlaintextNames mode)
if !fs.args.PlaintextNames && nametransform.IsLongContent(cNewName) {
err = fs.nameTransform.WriteLongName(newDirFd, cNewName, newPath)
if err != nil {
return fuse.ToStatus(err)
}
// Create "gocryptfs.longfile." link
err = syscallcompat.Linkat(int(oldDirFd.Fd()), cOldName, int(newDirFd.Fd()), cNewName, 0)
if err != nil {
nametransform.DeleteLongName(newDirFd, cNewName)
}
} else {
// Create regular link
err = syscallcompat.Linkat(int(oldDirFd.Fd()), cOldName, int(newDirFd.Fd()), cNewName, 0)
}
return fuse.ToStatus(err)
}
// Access implements pathfs.Filesystem.
func (fs *FS) Access(path string, mode uint32, context *fuse.Context) (code fuse.Status) {
if fs.isFiltered(path) {
return fuse.EPERM
}
cPath, err := fs.getBackingPath(path)
if err != nil {
return fuse.ToStatus(err)
}
return fuse.ToStatus(syscall.Access(cPath, mode))
}
// reportMitigatedCorruption is used to report a corruption that was transparently
// mitigated and did not return an error to the user. Pass the name of the corrupt
// item (filename for OpenDir(), xattr name for ListXAttr() etc).
// See the MitigatedCorruptions channel for more info.
func (fs *FS) reportMitigatedCorruption(item string) {
if fs.MitigatedCorruptions == nil {
return
}
select {
case fs.MitigatedCorruptions <- item:
case <-time.After(1 * time.Second):
tlog.Warn.Printf("BUG: reportCorruptItem: timeout")
//debug.PrintStack()
return
}
}