fusefrontend: Allow to set/remove xattr on directory without read permission.
Setting/removing extended attributes on directories was partially fixed with commiteff35e60b6
. However, on most file systems it is also possible to do these operations without read access (see tests). Since we cannot open a write-access fd to a directory, we have to use the /proc/self/fd trick (already used for ListXAttr) for the other operations aswell. For simplicity, let's separate the Linux and Darwin code again (basically revert commitf320b76fd1
), and always use the /proc/self/fd trick on Linux. On Darwin we use the best-effort approach with openBackingFile() as a fallback. More discussion about the available options is available in https://github.com/rfjakob/gocryptfs/issues/308.
This commit is contained in:
parent
f17721c364
commit
5055f39bd5
|
@ -2,16 +2,11 @@
|
||||||
package fusefrontend
|
package fusefrontend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/hanwen/go-fuse/fuse"
|
"github.com/hanwen/go-fuse/fuse"
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/tlog"
|
"github.com/rfjakob/gocryptfs/internal/tlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -36,18 +31,11 @@ func (fs *FS) GetXAttr(relPath string, attr string, context *fuse.Context) ([]by
|
||||||
return nil, _EOPNOTSUPP
|
return nil, _EOPNOTSUPP
|
||||||
}
|
}
|
||||||
|
|
||||||
// O_NONBLOCK to not block on FIFOs.
|
|
||||||
fd, err := fs.openBackingFile(relPath, syscall.O_RDONLY|syscall.O_NONBLOCK)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(fd)
|
|
||||||
|
|
||||||
cAttr := fs.encryptXattrName(attr)
|
cAttr := fs.encryptXattrName(attr)
|
||||||
|
|
||||||
cData, err := syscallcompat.Fgetxattr(fd, cAttr)
|
cData, status := fs.getXAttr(relPath, cAttr, context)
|
||||||
if err != nil {
|
if !status.Ok() {
|
||||||
return nil, fuse.ToStatus(err)
|
return nil, status
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := fs.decryptXattrValue(cData)
|
data, err := fs.decryptXattrValue(cData)
|
||||||
|
@ -69,26 +57,10 @@ func (fs *FS) SetXAttr(relPath string, attr string, data []byte, flags int, cont
|
||||||
return _EOPNOTSUPP
|
return _EOPNOTSUPP
|
||||||
}
|
}
|
||||||
|
|
||||||
// O_NONBLOCK to not block on FIFOs.
|
|
||||||
fd, err := fs.openBackingFile(relPath, syscall.O_WRONLY|syscall.O_NONBLOCK)
|
|
||||||
// Directories cannot be opened read-write. Retry.
|
|
||||||
if err == syscall.EISDIR {
|
|
||||||
fd, err = fs.openBackingFile(relPath, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(fd)
|
|
||||||
|
|
||||||
flags = filterXattrSetFlags(flags)
|
flags = filterXattrSetFlags(flags)
|
||||||
cAttr := fs.encryptXattrName(attr)
|
cAttr := fs.encryptXattrName(attr)
|
||||||
cData := fs.encryptXattrValue(data)
|
cData := fs.encryptXattrValue(data)
|
||||||
|
return fs.setXAttr(relPath, cAttr, cData, flags, context)
|
||||||
err = unix.Fsetxattr(fd, cAttr, cData, flags)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
return fuse.OK
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveXAttr - FUSE call.
|
// RemoveXAttr - FUSE call.
|
||||||
|
@ -102,23 +74,8 @@ func (fs *FS) RemoveXAttr(relPath string, attr string, context *fuse.Context) fu
|
||||||
return _EOPNOTSUPP
|
return _EOPNOTSUPP
|
||||||
}
|
}
|
||||||
|
|
||||||
// O_NONBLOCK to not block on FIFOs.
|
|
||||||
fd, err := fs.openBackingFile(relPath, syscall.O_WRONLY|syscall.O_NONBLOCK)
|
|
||||||
// Directories cannot be opened read-write. Retry.
|
|
||||||
if err == syscall.EISDIR {
|
|
||||||
fd, err = fs.openBackingFile(relPath, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(fd)
|
|
||||||
|
|
||||||
cAttr := fs.encryptXattrName(attr)
|
cAttr := fs.encryptXattrName(attr)
|
||||||
err = unix.Fremovexattr(fd, cAttr)
|
return fs.removeXAttr(relPath, cAttr, context)
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
return fuse.OK
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListXAttr - FUSE call. Lists extended attributes on the file at "relPath".
|
// ListXAttr - FUSE call. Lists extended attributes on the file at "relPath".
|
||||||
|
@ -128,32 +85,10 @@ func (fs *FS) ListXAttr(relPath string, context *fuse.Context) ([]string, fuse.S
|
||||||
if fs.isFiltered(relPath) {
|
if fs.isFiltered(relPath) {
|
||||||
return nil, fuse.EPERM
|
return nil, fuse.EPERM
|
||||||
}
|
}
|
||||||
var cNames []string
|
|
||||||
var err error
|
cNames, status := fs.listXAttr(relPath, context)
|
||||||
if runtime.GOOS == "linux" {
|
if !status.Ok() {
|
||||||
dirfd, cName, err2 := fs.openBackingDir(relPath)
|
return nil, status
|
||||||
if err2 != nil {
|
|
||||||
return nil, fuse.ToStatus(err2)
|
|
||||||
}
|
|
||||||
defer syscall.Close(dirfd)
|
|
||||||
procPath := fmt.Sprintf("/proc/self/fd/%d/%s", dirfd, cName)
|
|
||||||
cNames, err = syscallcompat.Llistxattr(procPath)
|
|
||||||
} else {
|
|
||||||
// O_NONBLOCK to not block on FIFOs.
|
|
||||||
fd, err2 := fs.openBackingFile(relPath, syscall.O_RDONLY|syscall.O_NONBLOCK)
|
|
||||||
// On a symlink, openBackingFile fails with ELOOP. Let's pretend there
|
|
||||||
// can be no xattrs on symlinks, and always return an empty result.
|
|
||||||
if err2 == syscall.ELOOP {
|
|
||||||
return nil, fuse.OK
|
|
||||||
}
|
|
||||||
if err2 != nil {
|
|
||||||
return nil, fuse.ToStatus(err2)
|
|
||||||
}
|
|
||||||
defer syscall.Close(fd)
|
|
||||||
cNames, err = syscallcompat.Flistxattr(fd)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
names := make([]string, 0, len(cNames))
|
names := make([]string, 0, len(cNames))
|
||||||
|
|
|
@ -3,6 +3,16 @@
|
||||||
// Package fusefrontend interfaces directly with the go-fuse library.
|
// Package fusefrontend interfaces directly with the go-fuse library.
|
||||||
package fusefrontend
|
package fusefrontend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
"github.com/hanwen/go-fuse/fuse"
|
||||||
|
|
||||||
|
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
|
||||||
|
)
|
||||||
|
|
||||||
func disallowedXAttrName(attr string) bool {
|
func disallowedXAttrName(attr string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -14,3 +24,71 @@ func filterXattrSetFlags(flags int) int {
|
||||||
|
|
||||||
return flags &^ XATTR_NOSECURITY
|
return flags &^ XATTR_NOSECURITY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fs *FS) getXAttr(relPath string, cAttr string, context *fuse.Context) ([]byte, fuse.Status) {
|
||||||
|
// O_NONBLOCK to not block on FIFOs.
|
||||||
|
fd, err := fs.openBackingFile(relPath, syscall.O_RDONLY|syscall.O_NONBLOCK)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
defer syscall.Close(fd)
|
||||||
|
|
||||||
|
cData, err := syscallcompat.Fgetxattr(fd, cAttr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cData, fuse.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FS) setXAttr(relPath string, cAttr string, cData []byte, flags int, context *fuse.Context) fuse.Status {
|
||||||
|
// O_NONBLOCK to not block on FIFOs.
|
||||||
|
fd, err := fs.openBackingFile(relPath, syscall.O_WRONLY|syscall.O_NONBLOCK)
|
||||||
|
// Directories cannot be opened read-write. Retry.
|
||||||
|
if err == syscall.EISDIR {
|
||||||
|
fd, err = fs.openBackingFile(relPath, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
defer syscall.Close(fd)
|
||||||
|
|
||||||
|
err = unix.Fsetxattr(fd, cAttr, cData, flags)
|
||||||
|
return fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FS) removeXAttr(relPath string, cAttr string, context *fuse.Context) fuse.Status {
|
||||||
|
// O_NONBLOCK to not block on FIFOs.
|
||||||
|
fd, err := fs.openBackingFile(relPath, syscall.O_WRONLY|syscall.O_NONBLOCK)
|
||||||
|
// Directories cannot be opened read-write. Retry.
|
||||||
|
if err == syscall.EISDIR {
|
||||||
|
fd, err = fs.openBackingFile(relPath, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
defer syscall.Close(fd)
|
||||||
|
|
||||||
|
err = unix.Fremovexattr(fd, cAttr)
|
||||||
|
return fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FS) listXAttr(relPath string, context *fuse.Context) ([]string, fuse.Status) {
|
||||||
|
// O_NONBLOCK to not block on FIFOs.
|
||||||
|
fd, err := fs.openBackingFile(relPath, syscall.O_RDONLY|syscall.O_NONBLOCK)
|
||||||
|
// On a symlink, openBackingFile fails with ELOOP. Let's pretend there
|
||||||
|
// can be no xattrs on symlinks, and always return an empty result.
|
||||||
|
if err == syscall.ELOOP {
|
||||||
|
return nil, fuse.OK
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
defer syscall.Close(fd)
|
||||||
|
|
||||||
|
cNames, err := syscallcompat.Flistxattr(fd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
return cNames, fuse.OK
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,15 @@
|
||||||
package fusefrontend
|
package fusefrontend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
"github.com/hanwen/go-fuse/fuse"
|
||||||
|
|
||||||
|
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Only allow the "user" namespace, block "trusted" and "security", as
|
// Only allow the "user" namespace, block "trusted" and "security", as
|
||||||
|
@ -19,3 +27,57 @@ func disallowedXAttrName(attr string) bool {
|
||||||
func filterXattrSetFlags(flags int) int {
|
func filterXattrSetFlags(flags int) int {
|
||||||
return flags
|
return flags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fs *FS) getXAttr(relPath string, cAttr string, context *fuse.Context) ([]byte, fuse.Status) {
|
||||||
|
dirfd, cName, err := fs.openBackingDir(relPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
defer syscall.Close(dirfd)
|
||||||
|
|
||||||
|
procPath := fmt.Sprintf("/proc/self/fd/%d/%s", dirfd, cName)
|
||||||
|
cData, err := syscallcompat.Lgetxattr(procPath, cAttr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
return cData, fuse.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FS) setXAttr(relPath string, cAttr string, cData []byte, flags int, context *fuse.Context) fuse.Status {
|
||||||
|
dirfd, cName, err := fs.openBackingDir(relPath)
|
||||||
|
if err != nil {
|
||||||
|
return fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
defer syscall.Close(dirfd)
|
||||||
|
|
||||||
|
procPath := fmt.Sprintf("/proc/self/fd/%d/%s", dirfd, cName)
|
||||||
|
err = unix.Lsetxattr(procPath, cAttr, cData, flags)
|
||||||
|
return fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FS) removeXAttr(relPath string, cAttr string, context *fuse.Context) fuse.Status {
|
||||||
|
dirfd, cName, err := fs.openBackingDir(relPath)
|
||||||
|
if err != nil {
|
||||||
|
return fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
defer syscall.Close(dirfd)
|
||||||
|
|
||||||
|
procPath := fmt.Sprintf("/proc/self/fd/%d/%s", dirfd, cName)
|
||||||
|
err = unix.Lremovexattr(procPath, cAttr)
|
||||||
|
return fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FS) listXAttr(relPath string, context *fuse.Context) ([]string, fuse.Status) {
|
||||||
|
dirfd, cName, err := fs.openBackingDir(relPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
defer syscall.Close(dirfd)
|
||||||
|
|
||||||
|
procPath := fmt.Sprintf("/proc/self/fd/%d/%s", dirfd, cName)
|
||||||
|
cNames, err := syscallcompat.Llistxattr(procPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
return cNames, fuse.OK
|
||||||
|
}
|
||||||
|
|
|
@ -84,6 +84,29 @@ func Fgetxattr(fd int, attr string) (val []byte, err error) {
|
||||||
return val, nil
|
return val, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lgetxattr is a wrapper around unix.Lgetxattr that handles the buffer sizing.
|
||||||
|
func Lgetxattr(path string, attr string) (val []byte, err error) {
|
||||||
|
// See the buffer sizing comments in Fgetxattr.
|
||||||
|
// TODO: smarter buffer sizing?
|
||||||
|
buf := make([]byte, XATTR_BUFSZ)
|
||||||
|
sz, err := unix.Lgetxattr(path, attr, buf)
|
||||||
|
if err == syscall.ERANGE {
|
||||||
|
// Do NOT return ERANGE - the user might retry ad inifinitum!
|
||||||
|
return nil, syscall.EOVERFLOW
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if sz >= XATTR_SIZE_MAX {
|
||||||
|
return nil, syscall.EOVERFLOW
|
||||||
|
}
|
||||||
|
// Copy only the actually used bytes to a new (smaller) buffer
|
||||||
|
// so "buf" never leaves the function and can be allocated on the stack.
|
||||||
|
val = make([]byte, sz)
|
||||||
|
copy(val, buf)
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Flistxattr is a wrapper for unix.Flistxattr that handles buffer sizing and
|
// Flistxattr is a wrapper for unix.Flistxattr that handles buffer sizing and
|
||||||
// parsing the returned blob to a string slice.
|
// parsing the returned blob to a string slice.
|
||||||
func Flistxattr(fd int) (attrs []string, err error) {
|
func Flistxattr(fd int) (attrs []string, err error) {
|
||||||
|
|
|
@ -129,7 +129,7 @@ func TestSetGetRmDir(t *testing.T) {
|
||||||
fn := test_helpers.DefaultPlainDir + "/TestSetGetRmDir"
|
fn := test_helpers.DefaultPlainDir + "/TestSetGetRmDir"
|
||||||
err := syscall.Mkdir(fn, 0700)
|
err := syscall.Mkdir(fn, 0700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("creating fifo failed: %v", err)
|
t.Fatalf("creating directory failed: %v", err)
|
||||||
}
|
}
|
||||||
setGetRmList(fn)
|
setGetRmList(fn)
|
||||||
}
|
}
|
||||||
|
@ -316,3 +316,31 @@ func TestSet0200File(t *testing.T) {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listing xattrs should work even when we don't have read access
|
||||||
|
func TestList0000Dir(t *testing.T) {
|
||||||
|
fn := test_helpers.DefaultPlainDir + "/TestList0000Dir"
|
||||||
|
err := syscall.Mkdir(fn, 0000)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("creating directory failed: %v", err)
|
||||||
|
}
|
||||||
|
_, err = xattr.LList(fn)
|
||||||
|
os.Chmod(fn, 0700)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setting xattrs should work even when we don't have read access
|
||||||
|
func TestSet0200Dir(t *testing.T) {
|
||||||
|
fn := test_helpers.DefaultPlainDir + "/TestSet0200Dir"
|
||||||
|
err := syscall.Mkdir(fn, 0200)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("creating directory failed: %v", err)
|
||||||
|
}
|
||||||
|
err = xattr.LSet(fn, "user.foo", []byte("bar"))
|
||||||
|
os.Chmod(fn, 0700)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue