Skip to content

Commit

Permalink
Merge branch 'hanwen:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
macos-fuse-t authored Nov 14, 2023
2 parents 3d1b0d9 + 3502146 commit 37474d9
Show file tree
Hide file tree
Showing 29 changed files with 561 additions and 267 deletions.
81 changes: 71 additions & 10 deletions fs/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,67 @@
// system issuing file operations in parallel, and using the race
// detector to weed out data races.
//
// # Deadlocks
//
// The Go runtime multiplexes Goroutines onto operating system
// threads, and makes assumptions that some system calls do not
// block. When accessing a file system from the same process that
// serves the file system (e.g. in unittests), this can lead to
// deadlocks, especially when GOMAXPROCS=1, when the Go runtime
// assumes a system call does not block, but actually is served by the
// Go-FUSE process.
//
// The following deadlocks are known:
//
// 1. Spawning a subprocess uses a fork/exec sequence: the process
// forks itself into a parent and child. The parent waits for the
// child to signal that the exec failed or succeeded, while the child
// prepares for calling exec(). Any setup step in the child that
// triggers a FUSE request can cause a deadlock.
//
// 1a. If the subprocess has a directory specified, the child will
// chdir into that directory. This generates an ACCESS operation on
// the directory.
//
// This deadlock can be avoided by disabling the ACCESS
// operation: return syscall.ENOSYS in the Access implementation, and
// ensure it is triggered called before initiating the subprocess.
//
// 1b. If the subprocess inherits files, the child process uses dup3()
// to remap file descriptors. If the destination fd happens to be
// backed by Go-FUSE, the dup3() call will implicitly close the fd,
// generating a FLUSH operation, eg.
//
// f1, err := os.Open("/fusemnt/file1")
// // f1.Fd() == 3
// f2, err := os.Open("/fusemnt/file1")
// // f2.Fd() == 4
//
// cmd := exec.Command("/bin/true")
// cmd.ExtraFiles = []*os.File{f2}
// // f2 (fd 4) is moved to fd 3. Deadlocks with GOMAXPROCS=1.
// cmd.Start()
//
// This deadlock can be avoided by ensuring that file descriptors
// pointing into FUSE mounts and file descriptors passed into
// subprocesses do not overlap, e.g. inserting the following before
// the above example:
//
// for {
// f, _ := os.Open("/dev/null")
// defer f.Close()
// if f.Fd() > 3 {
// break
// }
// }
//
// 2. The Go runtime uses the epoll system call to understand which
// goroutines can respond to I/O. The runtime assumes that epoll does
// not block, but if files are on a FUSE filesystem, the kernel will
// generate a POLL operation. To prevent this from happening, Go-FUSE
// disables the POLL opcode on mount. To ensure this has happened, call
// WaitMount.
//
// # Dynamically discovered file systems
//
// File system data usually cannot fit all in RAM, so the kernel must
Expand Down Expand Up @@ -233,7 +294,7 @@ type NodeGetattrer interface {
Getattr(ctx context.Context, f FileHandle, out *fuse.AttrOut) syscall.Errno
}

// SetAttr sets attributes for an Inode.
// SetAttr sets attributes for an Inode. Default is to return ENOTSUP.
type NodeSetattrer interface {
Setattr(ctx context.Context, f FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno
}
Expand Down Expand Up @@ -409,14 +470,14 @@ type NodeLookuper interface {
}

// OpenDir opens a directory Inode for reading its
// contents. The actual reading is driven from ReadDir, so
// contents. The actual reading is driven from Readdir, so
// this method is just for performing sanity/permission
// checks. The default is to return success.
type NodeOpendirer interface {
Opendir(ctx context.Context) syscall.Errno
}

// ReadDir opens a stream of directory entries.
// Readdir opens a stream of directory entries.
//
// Readdir essentiallly returns a list of strings, and it is allowed
// for Readdir to return different results from Lookup. For example,
Expand All @@ -435,25 +496,25 @@ type NodeReaddirer interface {
}

// Mkdir is similar to Lookup, but must create a directory entry and Inode.
// Default is to return EROFS.
// Default is to return ENOTSUP.
type NodeMkdirer interface {
Mkdir(ctx context.Context, name string, mode uint32, out *fuse.EntryOut) (*Inode, syscall.Errno)
}

// Mknod is similar to Lookup, but must create a device entry and Inode.
// Default is to return EROFS.
// Default is to return ENOTSUP.
type NodeMknoder interface {
Mknod(ctx context.Context, name string, mode uint32, dev uint32, out *fuse.EntryOut) (*Inode, syscall.Errno)
}

// Link is similar to Lookup, but must create a new link to an existing Inode.
// Default is to return EROFS.
// Default is to return ENOTSUP.
type NodeLinker interface {
Link(ctx context.Context, target InodeEmbedder, name string, out *fuse.EntryOut) (node *Inode, errno syscall.Errno)
}

// Symlink is similar to Lookup, but must create a new symbolic link.
// Default is to return EROFS.
// Default is to return ENOTSUP.
type NodeSymlinker interface {
Symlink(ctx context.Context, target, name string, out *fuse.EntryOut) (node *Inode, errno syscall.Errno)
}
Expand All @@ -468,20 +529,20 @@ type NodeCreater interface {

// Unlink should remove a child from this directory. If the
// return status is OK, the Inode is removed as child in the
// FS tree automatically. Default is to return EROFS.
// FS tree automatically. Default is to return success.
type NodeUnlinker interface {
Unlink(ctx context.Context, name string) syscall.Errno
}

// Rmdir is like Unlink but for directories.
// Default is to return EROFS.
// Default is to return success.
type NodeRmdirer interface {
Rmdir(ctx context.Context, name string) syscall.Errno
}

// Rename should move a child from one directory to a different
// one. The change is effected in the FS tree if the return status is
// OK. Default is to return EROFS.
// OK. Default is to return ENOTSUP.
type NodeRenamer interface {
Rename(ctx context.Context, name string, newParent InodeEmbedder, newName string, flags uint32) syscall.Errno
}
Expand Down
18 changes: 13 additions & 5 deletions fs/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,8 @@ func (b *rawBridge) Rmdir(cancel <-chan struct{}, header *fuse.InHeader, name st
errno = mops.Rmdir(&fuse.Context{Caller: header.Caller, Cancel: cancel}, name)
}

// TODO - this should not succeed silently.

if errno == 0 {
parent.RmChild(name)
}
Expand All @@ -391,6 +393,8 @@ func (b *rawBridge) Unlink(cancel <-chan struct{}, header *fuse.InHeader, name s
errno = mops.Unlink(&fuse.Context{Caller: header.Caller, Cancel: cancel}, name)
}

// TODO - this should not succeed silently.

if errno == 0 {
parent.RmChild(name)
}
Expand Down Expand Up @@ -1171,18 +1175,22 @@ func (b *rawBridge) Lseek(cancel <-chan struct{}, in *fuse.LseekIn, out *fuse.Ls
out.Offset = off
return errnoToStatus(errno)
}

var attr fuse.AttrOut
if s := b.getattr(ctx, n, nil, &attr); s != 0 {
return errnoToStatus(s)
}
if in.Whence == _SEEK_DATA {
if in.Offset >= attr.Size {
return errnoToStatus(syscall.ENXIO)
}
out.Offset = in.Offset
return fuse.OK
}

if in.Whence == _SEEK_HOLE {
var attr fuse.AttrOut
if s := b.getattr(ctx, n, nil, &attr); s != 0 {
return errnoToStatus(s)
if in.Offset > attr.Size {
return errnoToStatus(syscall.ENXIO)
}

out.Offset = attr.Size
return fuse.OK
}
Expand Down
36 changes: 36 additions & 0 deletions fs/loopback.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"syscall"

"github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/internal/renameat"
)

// LoopbackRoot holds the parameters for creating a new loopback
Expand Down Expand Up @@ -218,6 +219,41 @@ func (n *LoopbackNode) Create(ctx context.Context, name string, flags uint32, mo
return ch, lf, 0, 0
}

func (n *LoopbackNode) renameExchange(name string, newparent InodeEmbedder, newName string) syscall.Errno {
fd1, err := syscall.Open(n.path(), syscall.O_DIRECTORY, 0)
if err != nil {
return ToErrno(err)
}
defer syscall.Close(fd1)
p2 := filepath.Join(n.RootData.Path, newparent.EmbeddedInode().Path(nil))
fd2, err := syscall.Open(p2, syscall.O_DIRECTORY, 0)
defer syscall.Close(fd2)
if err != nil {
return ToErrno(err)
}

var st syscall.Stat_t
if err := syscall.Fstat(fd1, &st); err != nil {
return ToErrno(err)
}

// Double check that nodes didn't change from under us.
inode := &n.Inode
if inode.Root() != inode && inode.StableAttr().Ino != n.RootData.idFromStat(&st).Ino {
return syscall.EBUSY
}
if err := syscall.Fstat(fd2, &st); err != nil {
return ToErrno(err)
}

newinode := newparent.EmbeddedInode()
if newinode.Root() != newinode && newinode.StableAttr().Ino != n.RootData.idFromStat(&st).Ino {
return syscall.EBUSY
}

return ToErrno(renameat.Renameat(fd1, name, fd2, newName, renameat.RENAME_EXCHANGE))
}

var _ = (NodeSymlinker)((*LoopbackNode)(nil))

func (n *LoopbackNode) Symlink(ctx context.Context, target, name string, out *fuse.EntryOut) (*Inode, syscall.Errno) {
Expand Down
4 changes: 0 additions & 4 deletions fs/loopback_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,6 @@ func (n *LoopbackNode) Listxattr(ctx context.Context, dest []byte) (uint32, sysc
return 0, syscall.ENOSYS
}

func (n *LoopbackNode) renameExchange(name string, newparent InodeEmbedder, newName string) syscall.Errno {
return syscall.ENOSYS
}

func (f *loopbackFile) Allocate(ctx context.Context, off uint64, sz uint64, mode uint32) syscall.Errno {
// TODO: Handle `mode` parameter.

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

import (
"context"
"path/filepath"
"syscall"

"golang.org/x/sys/unix"
Expand Down Expand Up @@ -43,41 +42,6 @@ func (n *LoopbackNode) Listxattr(ctx context.Context, dest []byte) (uint32, sysc
return uint32(sz), ToErrno(err)
}

func (n *LoopbackNode) renameExchange(name string, newparent InodeEmbedder, newName string) syscall.Errno {
fd1, err := syscall.Open(n.path(), syscall.O_DIRECTORY, 0)
if err != nil {
return ToErrno(err)
}
defer syscall.Close(fd1)
p2 := filepath.Join(n.RootData.Path, newparent.EmbeddedInode().Path(nil))
fd2, err := syscall.Open(p2, syscall.O_DIRECTORY, 0)
defer syscall.Close(fd2)
if err != nil {
return ToErrno(err)
}

var st syscall.Stat_t
if err := syscall.Fstat(fd1, &st); err != nil {
return ToErrno(err)
}

// Double check that nodes didn't change from under us.
inode := &n.Inode
if inode.Root() != inode && inode.StableAttr().Ino != n.RootData.idFromStat(&st).Ino {
return syscall.EBUSY
}
if err := syscall.Fstat(fd2, &st); err != nil {
return ToErrno(err)
}

newinode := newparent.EmbeddedInode()
if newinode.Root() != newinode && newinode.StableAttr().Ino != n.RootData.idFromStat(&st).Ino {
return syscall.EBUSY
}

return ToErrno(unix.Renameat2(fd1, name, fd2, newName, unix.RENAME_EXCHANGE))
}

var _ = (NodeCopyFileRanger)((*LoopbackNode)(nil))

func (n *LoopbackNode) CopyFileRange(ctx context.Context, fhIn FileHandle,
Expand Down
74 changes: 0 additions & 74 deletions fs/loopback_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,90 +8,16 @@ import (
"bytes"
"io/ioutil"
"os"
"reflect"
"sync"
"syscall"
"testing"
"time"

"github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/internal/testutil"
"github.com/kylelemons/godebug/pretty"
"golang.org/x/sys/unix"
)

func TestRenameExchange(t *testing.T) {
tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})

if err := os.Mkdir(tc.origDir+"/dir", 0755); err != nil {
t.Fatalf("Mkdir: %v", err)
}
tc.writeOrig("file", "hello", 0644)
tc.writeOrig("dir/file", "x", 0644)

f1, err := syscall.Open(tc.mntDir+"/", syscall.O_DIRECTORY, 0)
if err != nil {
t.Fatalf("open 1: %v", err)
}
defer syscall.Close(f1)
f2, err := syscall.Open(tc.mntDir+"/dir", syscall.O_DIRECTORY, 0)
if err != nil {
t.Fatalf("open 2: %v", err)
}
defer syscall.Close(f2)

var before1, before2 unix.Stat_t
if err := unix.Fstatat(f1, "file", &before1, 0); err != nil {
t.Fatalf("Fstatat: %v", err)
}
if err := unix.Fstatat(f2, "file", &before2, 0); err != nil {
t.Fatalf("Fstatat: %v", err)
}

if err := unix.Renameat2(f1, "file", f2, "file", unix.RENAME_EXCHANGE); err != nil {
t.Errorf("rename EXCHANGE: %v", err)
}

var after1, after2 unix.Stat_t
if err := unix.Fstatat(f1, "file", &after1, 0); err != nil {
t.Fatalf("Fstatat: %v", err)
}
if err := unix.Fstatat(f2, "file", &after2, 0); err != nil {
t.Fatalf("Fstatat: %v", err)
}
clearCtime := func(s *unix.Stat_t) {
s.Ctim.Sec = 0
s.Ctim.Nsec = 0
}

clearCtime(&after1)
clearCtime(&after2)
clearCtime(&before2)
clearCtime(&before1)
if diff := pretty.Compare(after1, before2); diff != "" {
t.Errorf("after1, before2: %s", diff)
}
if !reflect.DeepEqual(after2, before1) {
t.Errorf("after2, before1: %#v, %#v", after2, before1)
}

root := tc.loopback.EmbeddedInode().Root()
ino1 := root.GetChild("file")
if ino1 == nil {
t.Fatalf("root.GetChild(%q): null inode", "file")
}
ino2 := root.GetChild("dir").GetChild("file")
if ino2 == nil {
t.Fatalf("dir.GetChild(%q): null inode", "file")
}
if ino1.StableAttr().Ino != after1.Ino {
t.Errorf("got inode %d for %q, want %d", ino1.StableAttr().Ino, "file", after1.Ino)
}
if ino2.StableAttr().Ino != after2.Ino {
t.Errorf("got inode %d for %q want %d", ino2.StableAttr().Ino, "dir/file", after2.Ino)
}
}

func TestRenameNoOverwrite(t *testing.T) {
tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})

Expand Down
Loading

0 comments on commit 37474d9

Please sign in to comment.