aboutsummaryrefslogtreecommitdiff
path: root/internal/fusefrontend/node_dir_ops.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/fusefrontend/node_dir_ops.go')
-rw-r--r--internal/fusefrontend/node_dir_ops.go128
1 files changed, 128 insertions, 0 deletions
diff --git a/internal/fusefrontend/node_dir_ops.go b/internal/fusefrontend/node_dir_ops.go
index 0789c92..b4bce1b 100644
--- a/internal/fusefrontend/node_dir_ops.go
+++ b/internal/fusefrontend/node_dir_ops.go
@@ -2,7 +2,10 @@ package fusefrontend
import (
"context"
+ "fmt"
+ "io"
"path/filepath"
+ "runtime"
"syscall"
"golang.org/x/sys/unix"
@@ -11,6 +14,7 @@ import (
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/rfjakob/gocryptfs/internal/configfile"
+ "github.com/rfjakob/gocryptfs/internal/cryptocore"
"github.com/rfjakob/gocryptfs/internal/nametransform"
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
"github.com/rfjakob/gocryptfs/internal/tlog"
@@ -216,3 +220,127 @@ func (n *Node) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) {
return fs.NewListDirStream(plain), 0
}
+
+// Rmdir - FUSE call.
+//
+// Symlink-safe through Unlinkat() + AT_REMOVEDIR.
+func (n *Node) Rmdir(ctx context.Context, name string) (code syscall.Errno) {
+ rn := n.rootNode()
+ p := filepath.Join(n.path(), name)
+ parentDirFd, cName, err := rn.openBackingDir(p)
+ if err != nil {
+ return fs.ToErrno(err)
+ }
+ defer syscall.Close(parentDirFd)
+ if rn.args.PlaintextNames {
+ // Unlinkat with AT_REMOVEDIR is equivalent to Rmdir
+ err = unix.Unlinkat(parentDirFd, cName, unix.AT_REMOVEDIR)
+ return fs.ToErrno(err)
+ }
+ // Unless we are running as root, we need read, write and execute permissions
+ // to handle gocryptfs.diriv.
+ permWorkaround := false
+ var origMode uint32
+ if !rn.args.PreserveOwner {
+ var st unix.Stat_t
+ err = syscallcompat.Fstatat(parentDirFd, cName, &st, unix.AT_SYMLINK_NOFOLLOW)
+ if err != nil {
+ return fs.ToErrno(err)
+ }
+ if st.Mode&0700 != 0700 {
+ tlog.Debug.Printf("Rmdir: permWorkaround")
+ permWorkaround = true
+ // This cast is needed on Darwin, where st.Mode is uint16.
+ origMode = uint32(st.Mode)
+ err = syscallcompat.FchmodatNofollow(parentDirFd, cName, origMode|0700)
+ if err != nil {
+ tlog.Debug.Printf("Rmdir: permWorkaround: chmod failed: %v", err)
+ return fs.ToErrno(err)
+ }
+ }
+ }
+ dirfd, err := syscallcompat.Openat(parentDirFd, cName,
+ syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0)
+ if err != nil {
+ tlog.Debug.Printf("Rmdir: Open: %v", err)
+ return fs.ToErrno(err)
+ }
+ defer syscall.Close(dirfd)
+ // Undo the chmod if removing the directory failed. This must run before
+ // closing dirfd, so defer it after (defer is LIFO).
+ if permWorkaround {
+ defer func() {
+ if code != 0 {
+ err = unix.Fchmod(dirfd, origMode)
+ if err != nil {
+ tlog.Warn.Printf("Rmdir: permWorkaround: rollback failed: %v", err)
+ }
+ }
+ }()
+ }
+retry:
+ // Check directory contents
+ children, err := syscallcompat.Getdents(dirfd)
+ if err == io.EOF {
+ // The directory is empty
+ tlog.Warn.Printf("Rmdir: %q: %s is missing", cName, nametransform.DirIVFilename)
+ err = unix.Unlinkat(parentDirFd, cName, unix.AT_REMOVEDIR)
+ return fs.ToErrno(err)
+ }
+ if err != nil {
+ tlog.Warn.Printf("Rmdir: Readdirnames: %v", err)
+ return fs.ToErrno(err)
+ }
+ // MacOS sprinkles .DS_Store files everywhere. This is hard to avoid for
+ // users, so handle it transparently here.
+ if runtime.GOOS == "darwin" && len(children) <= 2 && haveDsstore(children) {
+ err = unix.Unlinkat(dirfd, dsStoreName, 0)
+ if err != nil {
+ tlog.Warn.Printf("Rmdir: failed to delete blocking file %q: %v", dsStoreName, err)
+ return fs.ToErrno(err)
+ }
+ tlog.Warn.Printf("Rmdir: had to delete blocking file %q", dsStoreName)
+ goto retry
+ }
+ // If the directory is not empty besides gocryptfs.diriv, do not even
+ // attempt the dance around gocryptfs.diriv.
+ if len(children) > 1 {
+ return fs.ToErrno(syscall.ENOTEMPTY)
+ }
+ // Move "gocryptfs.diriv" to the parent dir as "gocryptfs.diriv.rmdir.XYZ"
+ tmpName := fmt.Sprintf("%s.rmdir.%d", nametransform.DirIVFilename, cryptocore.RandUint64())
+ tlog.Debug.Printf("Rmdir: Renaming %s to %s", nametransform.DirIVFilename, tmpName)
+ // The directory is in an inconsistent state between rename and rmdir.
+ // Protect against concurrent readers.
+ rn.dirIVLock.Lock()
+ defer rn.dirIVLock.Unlock()
+ err = syscallcompat.Renameat(dirfd, nametransform.DirIVFilename,
+ parentDirFd, tmpName)
+ if err != nil {
+ tlog.Warn.Printf("Rmdir: Renaming %s to %s failed: %v",
+ nametransform.DirIVFilename, tmpName, err)
+ return fs.ToErrno(err)
+ }
+ // Actual Rmdir
+ err = syscallcompat.Unlinkat(parentDirFd, cName, unix.AT_REMOVEDIR)
+ if err != nil {
+ // This can happen if another file in the directory was created in the
+ // meantime, undo the rename
+ err2 := syscallcompat.Renameat(parentDirFd, tmpName,
+ dirfd, nametransform.DirIVFilename)
+ if err2 != nil {
+ tlog.Warn.Printf("Rmdir: Rename rollback failed: %v", err2)
+ }
+ return fs.ToErrno(err)
+ }
+ // Delete "gocryptfs.diriv.rmdir.XYZ"
+ err = syscallcompat.Unlinkat(parentDirFd, tmpName, 0)
+ if err != nil {
+ tlog.Warn.Printf("Rmdir: Could not clean up %s: %v", tmpName, err)
+ }
+ // Delete .name file
+ if nametransform.IsLongContent(cName) {
+ nametransform.DeleteLongNameAt(parentDirFd, cName)
+ }
+ return 0
+}