Skip to content

Commit

Permalink
Rmdir: handle creating and removing unreadable directories
Browse files Browse the repository at this point in the history
This patch also splits off Mkdir and Rmdir into its own file.

Fixes issue #8, thanks to @diseq for the bug report.
  • Loading branch information
rfjakob committed Dec 11, 2015
1 parent 78cd97c commit e99e841
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 100 deletions.
7 changes: 7 additions & 0 deletions cryptfs/nonce.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cryptfs

import (
"encoding/binary"
"bytes"
"crypto/rand"
"encoding/hex"
Expand All @@ -17,6 +18,12 @@ func RandBytes(n int) []byte {
return b
}

// Return a secure random uint64
func RandUint64() uint64 {
b := RandBytes(8)
return binary.BigEndian.Uint64(b)
}

var gcmNonce nonce96

type nonce96 struct {
Expand Down
32 changes: 32 additions & 0 deletions integration_tests/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,38 @@ func testMkdirRmdir(t *testing.T, plainDir string) {
if err != nil {
t.Fatal(err)
}

// Removing a non-empty dir should fail with ENOTEMPTY
if os.Mkdir(dir, 0777) != nil {
t.Fatal(err)
}
f, err := os.Create(dir + "/file")
if err != nil {
t.Fatal(err)
}
f.Close()
err = syscall.Rmdir(dir)
errno := err.(syscall.Errno)
if errno != syscall.ENOTEMPTY {
t.Errorf("Should have gotten ENOTEMPTY, go %v", errno)
}
if syscall.Unlink(dir + "/file") != nil {
t.Fatal(err)
}
if syscall.Rmdir(dir) != nil {
t.Fatal(err)
}

// We should also be able to remove a directory we do not have permissions to
// read or write
err = os.Mkdir(dir, 0000)
if err != nil {
t.Fatal(err)
}
err = syscall.Rmdir(dir)
if err != nil {
t.Fatal(err)
}
}

// Create and rename a file
Expand Down
7 changes: 6 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ func main() {
"Setting this to a lower value speeds up mounting but makes the password susceptible to brute-force attacks")
flagSet.Parse(os.Args[1:])

// Fork a child into the background if "-f" is not set and we are mounting a filesystem
// Fork a child into the background if "-f" is not set AND we are mounting a filesystem
if !args.foreground && flagSet.NArg() == 2 {
forkChild() // does not return
}
Expand Down Expand Up @@ -339,6 +339,11 @@ func pathfsFrontend(key []byte, args argContainer, confFile *cryptfs.ConfFile) *
}
srv.SetDebug(args.fusedebug)

// All FUSE file and directory create calls carry explicit permission
// information. We need an unrestricted umask to create the files and
// directories with the requested permissions.
syscall.Umask(0000)

return srv
}

Expand Down
99 changes: 0 additions & 99 deletions pathfs_frontend/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package pathfs_frontend

import (
"encoding/base64"
"fmt"
"os"
"path/filepath"
"sync"
Expand Down Expand Up @@ -245,40 +244,7 @@ func (fs *FS) Readlink(path string, context *fuse.Context) (out string, status f
return string(target), fuse.OK
}

func (fs *FS) Mkdir(relPath string, mode uint32, context *fuse.Context) (code fuse.Status) {
if fs.isFiltered(relPath) {
return fuse.EPERM
}
encPath, err := fs.getBackingPath(relPath)
if err != nil {
return fuse.ToStatus(err)
}
if !fs.args.DirIV {
return fuse.ToStatus(os.Mkdir(encPath, os.FileMode(mode)))
}

// The new directory may take the place of an older one that is still in the cache
fs.CryptFS.DirIVCacheEnc.Clear()
// Create directory
fs.dirIVLock.Lock()
defer fs.dirIVLock.Unlock()
err = os.Mkdir(encPath, os.FileMode(mode))
if err != nil {
return fuse.ToStatus(err)
}
// Create gocryptfs.diriv inside
err = cryptfs.WriteDirIV(encPath)
if err != nil {
// This should not happen
cryptfs.Warn.Printf("Creating %s in dir %s failed: %v\n", cryptfs.DIRIV_FILENAME, encPath, err)
err2 := syscall.Rmdir(encPath)
if err2 != nil {
cryptfs.Warn.Printf("Mkdir: Rollback failed: %v\n", err2)
}
return fuse.ToStatus(err)
}
return fuse.OK
}

func (fs *FS) Unlink(path string, context *fuse.Context) (code fuse.Status) {
if fs.isFiltered(path) {
Expand All @@ -291,71 +257,6 @@ func (fs *FS) Unlink(path string, context *fuse.Context) (code fuse.Status) {
return fuse.ToStatus(syscall.Unlink(cPath))
}

func (fs *FS) Rmdir(name string, context *fuse.Context) (code fuse.Status) {
encPath, err := fs.getBackingPath(name)
if err != nil {
return fuse.ToStatus(err)
}
if !fs.args.DirIV {
return fuse.ToStatus(syscall.Rmdir(encPath))
}

// If the directory is not empty besides gocryptfs.diriv, do not even
// attempt the dance around gocryptfs.diriv.
fd, err := os.Open(encPath)
if err != nil {
return fuse.ToStatus(err)
}
defer fd.Close()
list, err := fd.Readdirnames(10)
if err != nil {
return fuse.ToStatus(err)
}
if len(list) > 1 {
return fuse.ToStatus(syscall.ENOTEMPTY)
}

// Move "gocryptfs.diriv" to the parent dir under name "gocryptfs.diriv.rmdir.INODENUMBER"
var st syscall.Stat_t
err = syscall.Fstat(int(fd.Fd()), &st)
if err != nil {
return fuse.ToStatus(err)
}
dirivPath := filepath.Join(encPath, cryptfs.DIRIV_FILENAME)
parentDir := filepath.Dir(encPath)
tmpName := fmt.Sprintf("gocryptfs.diriv.rmdir.%d", st.Ino)
tmpDirivPath := filepath.Join(parentDir, tmpName)
cryptfs.Debug.Printf("Rmdir: Renaming %s to %s\n", cryptfs.DIRIV_FILENAME, tmpDirivPath)
// The directory is in an inconsistent state between rename and rmdir. Protect against
// concurrent readers.
fs.dirIVLock.Lock()
defer fs.dirIVLock.Unlock()
err = os.Rename(dirivPath, tmpDirivPath)
if err != nil {
cryptfs.Warn.Printf("Rmdir: Renaming %s to %s failed: %v\n", cryptfs.DIRIV_FILENAME, tmpDirivPath, err)
return fuse.ToStatus(err)
}
// Actual Rmdir
err = syscall.Rmdir(encPath)
if err != nil {
// This can happen if another file in the directory was created in the
// meantime, undo the rename
err2 := os.Rename(tmpDirivPath, dirivPath)
if err2 != nil {
cryptfs.Warn.Printf("Rmdir: Rollback failed: %v\n", err2)
}
return fuse.ToStatus(err)
}
// Delete "gocryptfs.diriv.rmdir.INODENUMBER"
err = syscall.Unlink(tmpDirivPath)
if err != nil {
cryptfs.Warn.Printf("Rmdir: Could not clean up %s: %v\n", tmpName, err)
}
// The now-deleted directory may have been in the DirIV cache. Clear it.
fs.CryptFS.DirIVCacheEnc.Clear()
return fuse.OK
}

func (fs *FS) Symlink(target string, linkName string, context *fuse.Context) (code fuse.Status) {
cryptfs.Debug.Printf("Symlink(\"%s\", \"%s\")\n", target, linkName)
if fs.isFiltered(linkName) {
Expand Down
151 changes: 151 additions & 0 deletions pathfs_frontend/fs_dir.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package pathfs_frontend

import (
"os"
"path/filepath"
"syscall"
"fmt"

"github.com/hanwen/go-fuse/fuse"
"github.com/rfjakob/gocryptfs/cryptfs"
)

func (fs *FS) Mkdir(relPath string, mode uint32, context *fuse.Context) (code fuse.Status) {
if fs.isFiltered(relPath) {
return fuse.EPERM
}
encPath, err := fs.getBackingPath(relPath)
if err != nil {
return fuse.ToStatus(err)
}
if !fs.args.DirIV {
return fuse.ToStatus(os.Mkdir(encPath, os.FileMode(mode)))
}

// We need write and execute permissions to create gocryptfs.diriv
origMode := mode
mode = mode | 0300

// The new directory may take the place of an older one that is still in the cache
fs.CryptFS.DirIVCacheEnc.Clear()
// Create directory
fs.dirIVLock.Lock()
defer fs.dirIVLock.Unlock()
err = os.Mkdir(encPath, os.FileMode(mode))
if err != nil {
return fuse.ToStatus(err)
}
// Create gocryptfs.diriv inside
err = cryptfs.WriteDirIV(encPath)
if err != nil {
// This should not happen
cryptfs.Warn.Printf("Mkdir: WriteDirIV failed: %v\n", err)
err2 := syscall.Rmdir(encPath)
if err2 != nil {
cryptfs.Warn.Printf("Mkdir: Rmdir rollback failed: %v\n", err2)
}
return fuse.ToStatus(err)
}

// Set permissions back to what the user wanted
if origMode != mode {
err = os.Chmod(encPath, os.FileMode(origMode))
if err != nil {
cryptfs.Warn.Printf("Mkdir: Chmod failed: %v\n", err)
}
}

return fuse.OK
}

func (fs *FS) Rmdir(name string, context *fuse.Context) (code fuse.Status) {
encPath, err := fs.getBackingPath(name)
if err != nil {
return fuse.ToStatus(err)
}
if !fs.args.DirIV {
return fuse.ToStatus(syscall.Rmdir(encPath))
}

// If the directory is not empty besides gocryptfs.diriv, do not even
// attempt the dance around gocryptfs.diriv.
fd, err := os.Open(encPath)
if perr, ok := err.(*os.PathError); ok && perr.Err == syscall.EACCES {
// We need permission to read and modify the directory
cryptfs.Debug.Printf("Rmdir: handling EACCESS\n")
fi, err2 := os.Stat(encPath)
if err2 != nil {
cryptfs.Debug.Printf("Rmdir: Stat: %v\n", err2)
return fuse.ToStatus(err2)
}
origMode := fi.Mode()
newMode := origMode | 0700
err2 = os.Chmod(encPath, newMode)
if err2 != nil {
cryptfs.Debug.Printf("Rmdir: Chmod failed: %v\n", err2)
return fuse.ToStatus(err)
}
defer func () {
if code != fuse.OK {
// Undo the chmod if removing the directory failed
err3 := os.Chmod(encPath, origMode)
if err3 != nil {
cryptfs.Warn.Printf("Rmdir: Chmod rollback failed: %v\n", err2)
}
}
}()
// Retry open
fd, err = os.Open(encPath)
}
if err != nil {
cryptfs.Debug.Printf("Rmdir: Open: %v\n", err)
return fuse.ToStatus(err)
}
list, err := fd.Readdirnames(10)
fd.Close()
if err != nil {
cryptfs.Debug.Printf("Rmdir: Readdirnames: %v\n", err)
return fuse.ToStatus(err)
}
if len(list) > 1 {
return fuse.ToStatus(syscall.ENOTEMPTY)
} else if len(list) == 0 {
cryptfs.Warn.Printf("Rmdir: gocryptfs.diriv missing, allowing deletion\n")
return fuse.ToStatus(syscall.Rmdir(encPath))
}

// Move "gocryptfs.diriv" to the parent dir as "gocryptfs.diriv.rmdir.XYZ"
dirivPath := filepath.Join(encPath, cryptfs.DIRIV_FILENAME)
parentDir := filepath.Dir(encPath)
tmpName := fmt.Sprintf("gocryptfs.diriv.rmdir.%d", cryptfs.RandUint64())
tmpDirivPath := filepath.Join(parentDir, tmpName)
cryptfs.Debug.Printf("Rmdir: Renaming %s to %s\n", cryptfs.DIRIV_FILENAME, tmpDirivPath)
// The directory is in an inconsistent state between rename and rmdir. Protect against
// concurrent readers.
fs.dirIVLock.Lock()
defer fs.dirIVLock.Unlock()
err = os.Rename(dirivPath, tmpDirivPath)
if err != nil {
cryptfs.Warn.Printf("Rmdir: Renaming %s to %s failed: %v\n", cryptfs.DIRIV_FILENAME, tmpDirivPath, err)
return fuse.ToStatus(err)
}
// Actual Rmdir
err = syscall.Rmdir(encPath)
if err != nil {
// This can happen if another file in the directory was created in the
// meantime, undo the rename
err2 := os.Rename(tmpDirivPath, dirivPath)
if err2 != nil {
cryptfs.Warn.Printf("Rmdir: Rename rollback failed: %v\n", err2)
}
return fuse.ToStatus(err)
}
// Delete "gocryptfs.diriv.rmdir.INODENUMBER"
err = syscall.Unlink(tmpDirivPath)
if err != nil {
cryptfs.Warn.Printf("Rmdir: Could not clean up %s: %v\n", tmpName, err)
}
// The now-deleted directory may have been in the DirIV cache. Clear it.
fs.CryptFS.DirIVCacheEnc.Clear()
return fuse.OK
}

0 comments on commit e99e841

Please sign in to comment.