diff options
Diffstat (limited to 'internal/fusefrontend')
-rw-r--r-- | internal/fusefrontend/node_api_check.go | 1 | ||||
-rw-r--r-- | internal/fusefrontend/node_dir_ops.go | 128 |
2 files changed, 129 insertions, 0 deletions
diff --git a/internal/fusefrontend/node_api_check.go b/internal/fusefrontend/node_api_check.go index e6d1681..8b3eef1 100644 --- a/internal/fusefrontend/node_api_check.go +++ b/internal/fusefrontend/node_api_check.go @@ -9,3 +9,4 @@ var _ = (fs.NodeLookuper)((*Node)(nil)) var _ = (fs.NodeReaddirer)((*Node)(nil)) var _ = (fs.NodeCreater)((*Node)(nil)) var _ = (fs.NodeMkdirer)((*Node)(nil)) +var _ = (fs.NodeRmdirer)((*Node)(nil)) 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 +} |