diff --git a/fs/api.go b/fs/api.go index 1873ca43e..3050c7499 100644 --- a/fs/api.go +++ b/fs/api.go @@ -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 @@ -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 } @@ -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, @@ -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) } @@ -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 } diff --git a/fs/bridge.go b/fs/bridge.go index a5242f525..6c180725b 100644 --- a/fs/bridge.go +++ b/fs/bridge.go @@ -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) } @@ -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) } @@ -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 } diff --git a/fs/loopback.go b/fs/loopback.go index 475342125..c9ff956f3 100644 --- a/fs/loopback.go +++ b/fs/loopback.go @@ -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 @@ -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) { diff --git a/fs/loopback_darwin.go b/fs/loopback_darwin.go index ed6177095..4287d6180 100644 --- a/fs/loopback_darwin.go +++ b/fs/loopback_darwin.go @@ -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. diff --git a/fs/loopback_linux.go b/fs/loopback_linux.go index c3497fd5d..2640f9524 100644 --- a/fs/loopback_linux.go +++ b/fs/loopback_linux.go @@ -9,7 +9,6 @@ package fs import ( "context" - "path/filepath" "syscall" "golang.org/x/sys/unix" @@ -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, diff --git a/fs/loopback_linux_test.go b/fs/loopback_linux_test.go index bccbf325c..2a10b0931 100644 --- a/fs/loopback_linux_test.go +++ b/fs/loopback_linux_test.go @@ -8,7 +8,6 @@ import ( "bytes" "io/ioutil" "os" - "reflect" "sync" "syscall" "testing" @@ -16,82 +15,9 @@ import ( "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}) diff --git a/fs/loopback_test.go b/fs/loopback_test.go new file mode 100644 index 000000000..a3299c346 --- /dev/null +++ b/fs/loopback_test.go @@ -0,0 +1,83 @@ +package fs + +import ( + "os" + "reflect" + "syscall" + "testing" + + "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) + } +} diff --git a/fuse/api.go b/fuse/api.go index a0ec84fd7..7f310384e 100644 --- a/fuse/api.go +++ b/fuse/api.go @@ -122,6 +122,8 @@ // [2] https://sylabs.io/guides/3.7/user-guide/bind_paths_and_mounts.html#fuse-mounts package fuse +import "log" + // Types for users to implement. // The result of Read is an array of bytes, but for performance @@ -219,6 +221,9 @@ type MountOptions struct { // If set, print debugging information. Debug bool + // If set, sink for debug statements. + Logger *log.Logger + // If set, ask kernel to forward file locks to FUSE. If using, // you must implement the GetLk/SetLk/SetLkw methods. EnableLocks bool diff --git a/fuse/misc.go b/fuse/misc.go index 5e93972bb..6af5af125 100644 --- a/fuse/misc.go +++ b/fuse/misc.go @@ -10,10 +10,8 @@ import ( "fmt" "log" "os" - "reflect" "syscall" "time" - "unsafe" ) func (code Status) String() string { @@ -65,15 +63,6 @@ func ToStatus(err error) Status { return ENOSYS } -func toSlice(dest *[]byte, ptr unsafe.Pointer, byteCount uintptr) { - h := (*reflect.SliceHeader)(unsafe.Pointer(dest)) - *h = reflect.SliceHeader{ - Data: uintptr(ptr), - Len: int(byteCount), - Cap: int(byteCount), - } -} - func CurrentOwner() *Owner { return &Owner{ Uid: uint32(os.Getuid()), diff --git a/fuse/mount_linux.go b/fuse/mount_linux.go index 5e8992e78..c6abf035e 100644 --- a/fuse/mount_linux.go +++ b/fuse/mount_linux.go @@ -7,7 +7,6 @@ package fuse import ( "bytes" "fmt" - "log" "os" "os/exec" "path" @@ -69,7 +68,7 @@ func mountDirect(mountPoint string, opts *MountOptions, ready chan<- error) (fd } if opts.Debug { - log.Printf("mountDirect: calling syscall.Mount(%q, %q, %q, %#x, %q)", + opts.Logger.Printf("mountDirect: calling syscall.Mount(%q, %q, %q, %#x, %q)", source, mountPoint, "fuse."+opts.Name, flags, strings.Join(r, ",")) } err = syscall.Mount(source, mountPoint, "fuse."+opts.Name, flags, strings.Join(r, ",")) @@ -108,7 +107,7 @@ func callFusermount(mountPoint string, opts *MountOptions) (fd int, err error) { cmd = append(cmd, "-o", strings.Join(s, ",")) } if opts.Debug { - log.Printf("callFusermount: executing %q", cmd) + opts.Logger.Printf("callFusermount: executing %q", cmd) } proc, err := os.StartProcess(bin, cmd, @@ -145,7 +144,7 @@ func mount(mountPoint string, opts *MountOptions, ready chan<- error) (fd int, e if err == nil { return fd, nil } else if opts.Debug { - log.Printf("mount: failed to do direct mount: %s", err) + opts.Logger.Printf("mount: failed to do direct mount: %s", err) } if opts.DirectMountStrict { return -1, err @@ -157,7 +156,7 @@ func mount(mountPoint string, opts *MountOptions, ready chan<- error) (fd int, e fd = parseFuseFd(mountPoint) if fd >= 0 { if opts.Debug { - log.Printf("mount: magic mountpoint %q, using fd %d", mountPoint, fd) + opts.Logger.Printf("mount: magic mountpoint %q, using fd %d", mountPoint, fd) } } else { // Usual case: mount via the `fusermount` suid helper @@ -194,7 +193,7 @@ func unmount(mountPoint string, opts *MountOptions) (err error) { cmd := exec.Command(bin, "-u", mountPoint) cmd.Stderr = &errBuf if opts.Debug { - log.Printf("unmount: executing %q", cmd.Args) + opts.Logger.Printf("unmount: executing %q", cmd.Args) } err = cmd.Run() if errBuf.Len() > 0 { diff --git a/fuse/opcode.go b/fuse/opcode.go index 24c1683e3..3297fee6b 100644 --- a/fuse/opcode.go +++ b/fuse/opcode.go @@ -62,6 +62,12 @@ const ( _OP_LSEEK = uint32(46) // protocol version 24 _OP_COPY_FILE_RANGE = uint32(47) // protocol version 28. + _OP_SETUPMAPPING = 48 + _OP_REMOVEMAPPING = 49 + _OP_SYNCFS = 50 + _OP_TMPFILE = 51 + _OP_STATX = 52 + // The following entries don't have to be compatible across Go-FUSE versions. _OP_NOTIFY_INVAL_ENTRY = uint32(100) _OP_NOTIFY_INVAL_INODE = uint32(101) @@ -96,7 +102,7 @@ func doInit(server *Server, req *request) { server.reqMu.Lock() server.kernelSettings = *input server.kernelSettings.Flags = input.Flags & (CAP_ASYNC_READ | CAP_BIG_WRITES | CAP_FILE_OPS | - CAP_READDIRPLUS | CAP_NO_OPEN_SUPPORT | CAP_PARALLEL_DIROPS | CAP_MAX_PAGES) + CAP_READDIRPLUS | CAP_NO_OPEN_SUPPORT | CAP_PARALLEL_DIROPS | CAP_MAX_PAGES | CAP_RENAME_SWAP) if server.opts.EnableLocks { server.kernelSettings.Flags |= CAP_FLOCK_LOCKS | CAP_POSIX_LOCKS @@ -229,7 +235,7 @@ func doNotifyReply(server *Server, req *request) { server.retrieveMu.Unlock() badf := func(format string, argv ...interface{}) { - log.Printf("notify reply: "+format, argv...) + server.opts.Logger.Printf("notify reply: "+format, argv...) } if reading == nil { @@ -328,7 +334,7 @@ func doBatchForget(server *Server, req *request) { wantBytes := uintptr(in.Count) * unsafe.Sizeof(_ForgetOne{}) if uintptr(len(req.arg)) < wantBytes { // We have no return value to complain, so log an error. - log.Printf("Too few bytes for batch forget. Got %d bytes, want %d (%d entries)", + server.opts.Logger.Printf("Too few bytes for batch forget. Got %d bytes, want %d (%d entries)", len(req.arg), wantBytes, in.Count) } @@ -341,7 +347,7 @@ func doBatchForget(server *Server, req *request) { forgets := *(*[]_ForgetOne)(unsafe.Pointer(h)) for i, f := range forgets { if server.opts.Debug { - log.Printf("doBatchForget: rx %d %d/%d: FORGET n%d {Nlookup=%d}", + server.opts.Logger.Printf("doBatchForget: rx %d %d/%d: FORGET n%d {Nlookup=%d}", req.inHeader.Unique, i+1, len(forgets), f.NodeId, f.Nlookup) } if f.NodeId == pollHackInode { @@ -437,6 +443,10 @@ func doSymlink(server *Server, req *request) { } func doRename(server *Server, req *request) { + if server.kernelSettings.supportsRenameSwap() { + doRename2(server, req) + return + } in1 := (*Rename1In)(req.inData) in := RenameIn{ InHeader: in1.InHeader, @@ -692,6 +702,10 @@ func init() { _OP_RENAME2: "RENAME2", _OP_LSEEK: "LSEEK", _OP_COPY_FILE_RANGE: "COPY_FILE_RANGE", + _OP_SETUPMAPPING: "SETUPMAPPING", + _OP_REMOVEMAPPING: "REMOVEMAPPING", + _OP_SYNCFS: "SYNCFS", + _OP_TMPFILE: "TMPFILE", } { operationHandlers[op].Name = v } diff --git a/fuse/print.go b/fuse/print.go index 6ee8a438a..9f0b38061 100644 --- a/fuse/print.go +++ b/fuse/print.go @@ -20,48 +20,51 @@ var ( READ_LOCKOWNER: "LOCKOWNER", }) initFlagNames = newFlagNames(map[int64]string{ - CAP_ASYNC_READ: "ASYNC_READ", - CAP_POSIX_LOCKS: "POSIX_LOCKS", - CAP_FILE_OPS: "FILE_OPS", - CAP_ATOMIC_O_TRUNC: "ATOMIC_O_TRUNC", - CAP_EXPORT_SUPPORT: "EXPORT_SUPPORT", - CAP_BIG_WRITES: "BIG_WRITES", - CAP_DONT_MASK: "DONT_MASK", - CAP_SPLICE_WRITE: "SPLICE_WRITE", - CAP_SPLICE_MOVE: "SPLICE_MOVE", - CAP_SPLICE_READ: "SPLICE_READ", - CAP_FLOCK_LOCKS: "FLOCK_LOCKS", - CAP_IOCTL_DIR: "IOCTL_DIR", - CAP_AUTO_INVAL_DATA: "AUTO_INVAL_DATA", - CAP_READDIRPLUS: "READDIRPLUS", - CAP_READDIRPLUS_AUTO: "READDIRPLUS_AUTO", - CAP_ASYNC_DIO: "ASYNC_DIO", - CAP_WRITEBACK_CACHE: "WRITEBACK_CACHE", - CAP_NO_OPEN_SUPPORT: "NO_OPEN_SUPPORT", - CAP_PARALLEL_DIROPS: "PARALLEL_DIROPS", - CAP_POSIX_ACL: "POSIX_ACL", - CAP_HANDLE_KILLPRIV: "HANDLE_KILLPRIV", - CAP_ABORT_ERROR: "ABORT_ERROR", - CAP_MAX_PAGES: "MAX_PAGES", - CAP_CACHE_SYMLINKS: "CACHE_SYMLINKS", - CAP_NO_OPENDIR_SUPPORT: "NO_OPENDIR_SUPPORT", - CAP_EXPLICIT_INVAL_DATA: "EXPLICIT_INVAL_DATA", + CAP_ASYNC_READ: "ASYNC_READ", + CAP_POSIX_LOCKS: "POSIX_LOCKS", + CAP_FILE_OPS: "FILE_OPS", + CAP_ATOMIC_O_TRUNC: "ATOMIC_O_TRUNC", + CAP_EXPORT_SUPPORT: "EXPORT_SUPPORT", + CAP_BIG_WRITES: "BIG_WRITES", + CAP_DONT_MASK: "DONT_MASK", + CAP_SPLICE_WRITE: "SPLICE_WRITE", + CAP_SPLICE_MOVE: "SPLICE_MOVE", + CAP_SPLICE_READ: "SPLICE_READ", + CAP_FLOCK_LOCKS: "FLOCK_LOCKS", + CAP_IOCTL_DIR: "IOCTL_DIR", + CAP_AUTO_INVAL_DATA: "AUTO_INVAL_DATA", + CAP_READDIRPLUS: "READDIRPLUS", + CAP_READDIRPLUS_AUTO: "READDIRPLUS_AUTO", + CAP_ASYNC_DIO: "ASYNC_DIO", + CAP_WRITEBACK_CACHE: "WRITEBACK_CACHE", + CAP_NO_OPEN_SUPPORT: "NO_OPEN_SUPPORT", + CAP_PARALLEL_DIROPS: "PARALLEL_DIROPS", + CAP_POSIX_ACL: "POSIX_ACL", + CAP_HANDLE_KILLPRIV: "HANDLE_KILLPRIV", + CAP_ABORT_ERROR: "ABORT_ERROR", + CAP_MAX_PAGES: "MAX_PAGES", + CAP_CACHE_SYMLINKS: "CACHE_SYMLINKS", + CAP_SECURITY_CTX: "SECURITY_CTX", + CAP_HAS_INODE_DAX: "HAS_INODE_DAX", + CAP_CREATE_SUPP_GROUP: "CREATE_SUPP_GROUP", + CAP_HAS_EXPIRE_ONLY: "HAS_EXPIRE_ONLY", + CAP_DIRECT_IO_RELAX: "DIRECT_IO_RELAX", }) releaseFlagNames = newFlagNames(map[int64]string{ RELEASE_FLUSH: "FLUSH", }) openFlagNames = newFlagNames(map[int64]string{ - int64(os.O_WRONLY): "WRONLY", - int64(os.O_RDWR): "RDWR", - int64(os.O_APPEND): "APPEND", - int64(syscall.O_ASYNC): "ASYNC", - int64(os.O_CREATE): "CREAT", - int64(os.O_EXCL): "EXCL", - int64(syscall.O_NOCTTY): "NOCTTY", - int64(syscall.O_NONBLOCK): "NONBLOCK", - int64(os.O_SYNC): "SYNC", - int64(os.O_TRUNC): "TRUNC", - + int64(os.O_WRONLY): "WRONLY", + int64(os.O_RDWR): "RDWR", + int64(os.O_APPEND): "APPEND", + int64(syscall.O_ASYNC): "ASYNC", + int64(os.O_CREATE): "CREAT", + int64(os.O_EXCL): "EXCL", + int64(syscall.O_NOCTTY): "NOCTTY", + int64(syscall.O_NONBLOCK): "NONBLOCK", + int64(os.O_SYNC): "SYNC", + int64(os.O_TRUNC): "TRUNC", + 0x8000: "LARGEFILE", int64(syscall.O_CLOEXEC): "CLOEXEC", int64(syscall.O_DIRECTORY): "DIRECTORY", }) @@ -209,7 +212,7 @@ func (in *OpenOut) string() string { func (in *InitIn) string() string { return fmt.Sprintf("{%d.%d Ra %d %s}", in.Major, in.Minor, in.MaxReadAhead, - flagString(initFlagNames, int64(in.Flags), "")) + flagString(initFlagNames, int64(in.Flags)|(int64(in.Flags2)<<32), "")) } func (o *InitOut) string() string { diff --git a/fuse/print_darwin.go b/fuse/print_darwin.go index 73243c228..a9af949da 100644 --- a/fuse/print_darwin.go +++ b/fuse/print_darwin.go @@ -9,6 +9,11 @@ import ( ) func init() { + initFlagNames.set(CAP_NODE_RWLOCK, "NODE_RWLOCK") + initFlagNames.set(CAP_RENAME_SWAP, "RENAME_SWAP") + initFlagNames.set(CAP_RENAME_EXCL, "RENAME_EXCL") + initFlagNames.set(CAP_ALLOCATE, "ALLOCATE") + initFlagNames.set(CAP_EXCHANGE_DATA, "EXCHANGE_DATA") initFlagNames.set(CAP_XTIMES, "XTIMES") initFlagNames.set(CAP_VOL_RENAME, "VOL_RENAME") initFlagNames.set(CAP_CASE_INSENSITIVE, "CASE_INSENSITIVE") diff --git a/fuse/print_linux.go b/fuse/print_linux.go index a7f3f44c1..955b732bd 100644 --- a/fuse/print_linux.go +++ b/fuse/print_linux.go @@ -13,6 +13,14 @@ func init() { openFlagNames.set(syscall.O_DIRECT, "DIRECT") openFlagNames.set(syscall.O_LARGEFILE, "LARGEFILE") openFlagNames.set(syscall_O_NOATIME, "NOATIME") + initFlagNames.set(CAP_NO_OPENDIR_SUPPORT, "NO_OPENDIR_SUPPORT") + initFlagNames.set(CAP_EXPLICIT_INVAL_DATA, "EXPLICIT_INVAL_DATA") + initFlagNames.set(CAP_MAP_ALIGNMENT, "MAP_ALIGNMENT") + initFlagNames.set(CAP_SUBMOUNTS, "SUBMOUNTS") + initFlagNames.set(CAP_HANDLE_KILLPRIV_V2, "HANDLE_KILLPRIV_V2") + initFlagNames.set(CAP_SETXATTR_EXT, "SETXATTR_EXT") + initFlagNames.set(CAP_INIT_EXT, "INIT_EXT") + initFlagNames.set(CAP_INIT_RESERVED, "INIT_RESERVED") } func (a *Attr) string() string { diff --git a/fuse/request.go b/fuse/request.go index 16be52767..d59833be8 100644 --- a/fuse/request.go +++ b/fuse/request.go @@ -172,7 +172,7 @@ func (r *request) parseHeader() Status { return OK } -func (r *request) parse() { +func (r *request) parse(kernelSettings InitIn) { r.arg = r.inputBuf[:] r.handler = getHandler(r.inHeader.Opcode) if r.handler == nil { @@ -181,7 +181,15 @@ func (r *request) parse() { return } - if len(r.arg) < int(r.handler.InputSize) { + inSz := int(r.handler.InputSize) + if r.inHeader.Opcode == _OP_RENAME && kernelSettings.supportsRenameSwap() { + inSz = int(unsafe.Sizeof(RenameIn{})) + } + if r.inHeader.Opcode == _OP_INIT && inSz > len(r.arg) { + // Minor version 36 extended the size of InitIn struct + inSz = len(r.arg) + } + if len(r.arg) < inSz { log.Printf("Short read for %v: %v", operationName(r.inHeader.Opcode), r.arg) r.status = EIO return @@ -189,7 +197,7 @@ func (r *request) parse() { if r.handler.InputSize > 0 { r.inData = unsafe.Pointer(&r.arg[0]) - r.arg = r.arg[r.handler.InputSize:] + r.arg = r.arg[inSz:] } else { r.arg = r.arg[unsafe.Sizeof(InHeader{}):] } diff --git a/fuse/request_darwin.go b/fuse/request_darwin.go index 6caddaed3..a356fda72 100644 --- a/fuse/request_darwin.go +++ b/fuse/request_darwin.go @@ -9,5 +9,5 @@ const outputHeaderSize = 200 const ( _FUSE_KERNEL_VERSION = 7 _MINIMUM_MINOR_VERSION = 12 - _OUR_MINOR_VERSION = 12 + _OUR_MINOR_VERSION = 19 ) diff --git a/fuse/server.go b/fuse/server.go index a63d21dca..87a7e1c07 100644 --- a/fuse/server.go +++ b/fuse/server.go @@ -165,7 +165,9 @@ func NewServer(fs RawFileSystem, mountPoint string, opts *MountOptions) (*Server } } o := *opts - + if o.Logger == nil { + o.Logger = log.Default() + } if o.MaxWrite < 0 { o.MaxWrite = 0 } @@ -323,13 +325,15 @@ func (ms *Server) readRequest(exitIdle bool) (req *request, code Status) { ms.reqReaders++ ms.reqMu.Unlock() - req = ms.reqPool.Get().(*request) - dest := ms.readPool.Get().([]byte) + reqIface := ms.reqPool.Get() + req = reqIface.(*request) + destIface := ms.readPool.Get() + dest := destIface.([]byte) n, err := ms.systemRead(dest) if err != nil { code = ToStatus(err) - ms.reqPool.Put(req) + ms.reqPool.Put(reqIface) ms.reqMu.Lock() ms.reqReaders-- ms.reqMu.Unlock() @@ -350,8 +354,7 @@ func (ms *Server) readRequest(exitIdle bool) (req *request, code Status) { req.inflightIndex = len(ms.reqInflight) ms.reqInflight = append(ms.reqInflight, req) if !gobbled { - ms.readPool.Put(dest) - dest = nil + ms.readPool.Put(destIface) } ms.reqReaders-- if !ms.singleReader && ms.reqReaders <= 0 { @@ -478,11 +481,11 @@ exit: case ENODEV: // unmount if ms.opts.Debug { - log.Printf("received ENODEV (unmount request), thread exiting") + ms.opts.Logger.Printf("received ENODEV (unmount request), thread exiting") } break exit default: // some other error? - log.Printf("Failed to read from fuse conn: %v", errNo) + ms.opts.Logger.Printf("Failed to read from fuse conn: %v", errNo) break exit } @@ -500,20 +503,20 @@ func (ms *Server) handleRequest(req *request) Status { defer ms.requestProcessingMu.Unlock() } - req.parse() + req.parse(ms.kernelSettings) if req.handler == nil { req.status = ENOSYS } if req.status.Ok() && ms.opts.Debug { - log.Println(req.InputDebug()) + ms.opts.Logger.Println(req.InputDebug()) } if req.inHeader.NodeId == pollHackInode || req.inHeader.NodeId == FUSE_ROOT_ID && len(req.filenames) > 0 && req.filenames[0] == pollHackName { doPollHackLookup(ms, req) } else if req.status.Ok() && req.handler.Func == nil { - log.Printf("Unimplemented opcode %v", operationName(req.inHeader.Opcode)) + ms.opts.Logger.Printf("Unimplemented opcode %v", operationName(req.inHeader.Opcode)) req.status = ENOSYS } else if req.status.Ok() { req.handler.Func(ms, req) @@ -529,7 +532,7 @@ func (ms *Server) handleRequest(req *request) Status { // kernel. This is a normal if the referred request already has // completed. if ms.opts.Debug || !(req.inHeader.Opcode == _OP_INTERRUPT && errNo == ENOENT) { - log.Printf("writer: Write/Writev failed, err: %v. opcode: %v", + ms.opts.Logger.Printf("writer: Write/Writev failed, err: %v. opcode: %v", errNo, operationName(req.inHeader.Opcode)) } @@ -575,7 +578,7 @@ func (ms *Server) write(req *request) Status { header := req.serializeHeader(req.flatDataSize()) if ms.opts.Debug { - log.Println(req.OutputDebug()) + ms.opts.Logger.Println(req.OutputDebug()) } if header == nil { @@ -612,7 +615,7 @@ func (ms *Server) InodeNotify(node uint64, off int64, length int64) Status { ms.writeMu.Unlock() if ms.opts.Debug { - log.Println("Response: INODE_NOTIFY", result) + ms.opts.Logger.Println("Response: INODE_NOTIFY", result) } return result } @@ -671,7 +674,7 @@ func (ms *Server) inodeNotifyStoreCache32(node uint64, offset int64, data []byte ms.writeMu.Unlock() if ms.opts.Debug { - log.Printf("Response: INODE_NOTIFY_STORE_CACHE: %v", result) + ms.opts.Logger.Printf("Response: INODE_NOTIFY_STORE_CACHE: %v", result) } return result } @@ -763,7 +766,7 @@ func (ms *Server) inodeRetrieveCache1(node uint64, offset int64, dest []byte) (n ms.writeMu.Unlock() if ms.opts.Debug { - log.Printf("Response: NOTIFY_RETRIEVE_CACHE: %v", result) + ms.opts.Logger.Printf("Response: NOTIFY_RETRIEVE_CACHE: %v", result) } if result != OK { ms.retrieveMu.Lock() @@ -777,7 +780,7 @@ func (ms *Server) inodeRetrieveCache1(node uint64, offset int64, dest []byte) (n // unexpected NotifyReply with our notifyUnique, then // retrieveNext wraps, makes full cycle, and another // retrieve request is made with the same notifyUnique. - log.Printf("W: INODE_RETRIEVE_CACHE: request with notifyUnique=%d mutated", q.NotifyUnique) + ms.opts.Logger.Printf("W: INODE_RETRIEVE_CACHE: request with notifyUnique=%d mutated", q.NotifyUnique) } ms.retrieveMu.Unlock() return 0, result @@ -838,7 +841,7 @@ func (ms *Server) DeleteNotify(parent uint64, child uint64, name string) Status ms.writeMu.Unlock() if ms.opts.Debug { - log.Printf("Response: DELETE_NOTIFY: %v", result) + ms.opts.Logger.Printf("Response: DELETE_NOTIFY: %v", result) } return result } @@ -874,7 +877,7 @@ func (ms *Server) EntryNotify(parent uint64, name string) Status { ms.writeMu.Unlock() if ms.opts.Debug { - log.Printf("Response: ENTRY_NOTIFY: %v", result) + ms.opts.Logger.Printf("Response: ENTRY_NOTIFY: %v", result) } return result } @@ -901,6 +904,12 @@ func (in *InitIn) SupportsNotify(notifyType int) bool { return false } +// supportsRenameSwap returns whether the kernel supports the +// renamex_np(2) syscall. This is only supported on OS X. +func (in *InitIn) supportsRenameSwap() bool { + return in.Flags&CAP_RENAME_SWAP != 0 +} + // WaitMount waits for the first request to be served. Use this to // avoid racing between accessing the (empty or not yet mounted) // mountpoint, and the OS trying to setup the user-space mount. diff --git a/fuse/server_linux.go b/fuse/server_linux.go index a48426f08..9e09fd3a2 100644 --- a/fuse/server_linux.go +++ b/fuse/server_linux.go @@ -5,7 +5,6 @@ package fuse import ( - "log" "syscall" ) @@ -25,7 +24,7 @@ func (ms *Server) systemWrite(req *request, header []byte) Status { req.readResult.Done() return OK } - log.Println("trySplice:", err) + ms.opts.Logger.Println("trySplice:", err) } sz := req.flatDataSize() diff --git a/fuse/test/file_lock_test.go b/fuse/test/file_lock_test.go index 873ddc6cd..398f21c88 100644 --- a/fuse/test/file_lock_test.go +++ b/fuse/test/file_lock_test.go @@ -32,6 +32,17 @@ func TestFlockExclusive(t *testing.T) { contents := []byte{1, 2, 3} tc.WriteFile(tc.origFile, []byte(contents), 0700) + for { + f, err := os.Open("/dev/null") + if err != nil { + t.Fatalf("Open(/dev/null): %v", err) + } + defer f.Close() + if f.Fd() > 3 { + break + } + } + f, err := os.OpenFile(tc.mountFile, os.O_WRONLY, 0) if err != nil { t.Fatalf("OpenFile(%q): %v", tc.mountFile, err) diff --git a/fuse/types.go b/fuse/types.go index cbcf51ed0..38010a653 100644 --- a/fuse/types.go +++ b/fuse/types.go @@ -126,17 +126,18 @@ type Owner struct { } const ( // SetAttrIn.Valid - FATTR_MODE = (1 << 0) - FATTR_UID = (1 << 1) - FATTR_GID = (1 << 2) - FATTR_SIZE = (1 << 3) - FATTR_ATIME = (1 << 4) - FATTR_MTIME = (1 << 5) - FATTR_FH = (1 << 6) - FATTR_ATIME_NOW = (1 << 7) - FATTR_MTIME_NOW = (1 << 8) - FATTR_LOCKOWNER = (1 << 9) - FATTR_CTIME = (1 << 10) + FATTR_MODE = (1 << 0) + FATTR_UID = (1 << 1) + FATTR_GID = (1 << 2) + FATTR_SIZE = (1 << 3) + FATTR_ATIME = (1 << 4) + FATTR_MTIME = (1 << 5) + FATTR_FH = (1 << 6) + FATTR_ATIME_NOW = (1 << 7) + FATTR_MTIME_NOW = (1 << 8) + FATTR_LOCKOWNER = (1 << 9) + FATTR_CTIME = (1 << 10) + FATTR_KILL_SUIDGID = (1 << 11) ) type SetAttrInCommon struct { @@ -251,11 +252,13 @@ type OpenIn struct { const ( // OpenOut.Flags - FOPEN_DIRECT_IO = (1 << 0) - FOPEN_KEEP_CACHE = (1 << 1) - FOPEN_NONSEEKABLE = (1 << 2) - FOPEN_CACHE_DIR = (1 << 3) - FOPEN_STREAM = (1 << 4) + FOPEN_DIRECT_IO = (1 << 0) + FOPEN_KEEP_CACHE = (1 << 1) + FOPEN_NONSEEKABLE = (1 << 2) + FOPEN_CACHE_DIR = (1 << 3) + FOPEN_STREAM = (1 << 4) + FOPEN_NOFLUSH = (1 << 5) + FOPEN_PARALLEL_DIRECT_WRITES = (1 << 6) ) type OpenOut struct { @@ -273,32 +276,37 @@ type OpenOut struct { // * https://github.com/libfuse/libfuse/blob/master/include/fuse_common.h // This file has CAP_HANDLE_KILLPRIV and CAP_POSIX_ACL reversed! const ( - CAP_ASYNC_READ = (1 << 0) - CAP_POSIX_LOCKS = (1 << 1) - CAP_FILE_OPS = (1 << 2) - CAP_ATOMIC_O_TRUNC = (1 << 3) - CAP_EXPORT_SUPPORT = (1 << 4) - CAP_BIG_WRITES = (1 << 5) - CAP_DONT_MASK = (1 << 6) - CAP_SPLICE_WRITE = (1 << 7) - CAP_SPLICE_MOVE = (1 << 8) - CAP_SPLICE_READ = (1 << 9) - CAP_FLOCK_LOCKS = (1 << 10) - CAP_IOCTL_DIR = (1 << 11) - CAP_AUTO_INVAL_DATA = (1 << 12) - CAP_READDIRPLUS = (1 << 13) - CAP_READDIRPLUS_AUTO = (1 << 14) - CAP_ASYNC_DIO = (1 << 15) - CAP_WRITEBACK_CACHE = (1 << 16) - CAP_NO_OPEN_SUPPORT = (1 << 17) - CAP_PARALLEL_DIROPS = (1 << 18) - CAP_HANDLE_KILLPRIV = (1 << 19) - CAP_POSIX_ACL = (1 << 20) - CAP_ABORT_ERROR = (1 << 21) - CAP_MAX_PAGES = (1 << 22) - CAP_CACHE_SYMLINKS = (1 << 23) - CAP_NO_OPENDIR_SUPPORT = (1 << 24) - CAP_EXPLICIT_INVAL_DATA = (1 << 25) + CAP_ASYNC_READ = (1 << 0) + CAP_POSIX_LOCKS = (1 << 1) + CAP_FILE_OPS = (1 << 2) + CAP_ATOMIC_O_TRUNC = (1 << 3) + CAP_EXPORT_SUPPORT = (1 << 4) + CAP_BIG_WRITES = (1 << 5) + CAP_DONT_MASK = (1 << 6) + CAP_SPLICE_WRITE = (1 << 7) + CAP_SPLICE_MOVE = (1 << 8) + CAP_SPLICE_READ = (1 << 9) + CAP_FLOCK_LOCKS = (1 << 10) + CAP_IOCTL_DIR = (1 << 11) + CAP_AUTO_INVAL_DATA = (1 << 12) + CAP_READDIRPLUS = (1 << 13) + CAP_READDIRPLUS_AUTO = (1 << 14) + CAP_ASYNC_DIO = (1 << 15) + CAP_WRITEBACK_CACHE = (1 << 16) + CAP_NO_OPEN_SUPPORT = (1 << 17) + CAP_PARALLEL_DIROPS = (1 << 18) + CAP_HANDLE_KILLPRIV = (1 << 19) + CAP_POSIX_ACL = (1 << 20) + CAP_ABORT_ERROR = (1 << 21) + CAP_MAX_PAGES = (1 << 22) + CAP_CACHE_SYMLINKS = (1 << 23) + + /* bits 32..63 get shifted down 32 bits into the Flags2 field */ + CAP_SECURITY_CTX = (1 << 32) + CAP_HAS_INODE_DAX = (1 << 33) + CAP_CREATE_SUPP_GROUP = (1 << 34) + CAP_HAS_EXPIRE_ONLY = (1 << 35) + CAP_DIRECT_IO_RELAX = (1 << 36) ) type InitIn struct { @@ -308,6 +316,8 @@ type InitIn struct { Minor uint32 MaxReadAhead uint32 Flags uint32 + Flags2 uint32 + Unused [11]uint32 } type InitOut struct { @@ -321,7 +331,8 @@ type InitOut struct { TimeGran uint32 MaxPages uint16 Padding uint16 - Unused [8]uint32 + Flags2 uint32 + Unused [7]uint32 } type _CuseInitIn struct { @@ -645,8 +656,9 @@ const ( ) const ( - WRITE_CACHE = (1 << 0) - WRITE_LOCKOWNER = (1 << 1) + WRITE_CACHE = (1 << 0) + WRITE_LOCKOWNER = (1 << 1) + WRITE_KILL_SUIDGID = (1 << 2) ) type FallocateIn struct { diff --git a/fuse/types_darwin.go b/fuse/types_darwin.go index c50dd693c..fdaa21f46 100644 --- a/fuse/types_darwin.go +++ b/fuse/types_darwin.go @@ -145,9 +145,17 @@ type GetXAttrIn struct { } const ( + CAP_NODE_RWLOCK = (1 << 24) + CAP_RENAME_SWAP = (1 << 25) + CAP_RENAME_EXCL = (1 << 26) + CAP_ALLOCATE = (1 << 27) + CAP_EXCHANGE_DATA = (1 << 28) CAP_CASE_INSENSITIVE = (1 << 29) CAP_VOL_RENAME = (1 << 30) CAP_XTIMES = (1 << 31) + + // CAP_EXPLICIT_INVAL_DATA is not supported on Darwin. + CAP_EXPLICIT_INVAL_DATA = 0x0 ) type GetxtimesOut struct { diff --git a/fuse/types_linux.go b/fuse/types_linux.go index b1e088f52..4cae76540 100644 --- a/fuse/types_linux.go +++ b/fuse/types_linux.go @@ -15,6 +15,24 @@ const ( EREMOTEIO = Status(syscall.EREMOTEIO) ) +// To be set in InitIn/InitOut.Flags. +// +// This flags conflict with https://github.com/macfuse/library/blob/master/include/fuse_common.h +// and should be used only on Linux. +const ( + CAP_NO_OPENDIR_SUPPORT = (1 << 24) + CAP_EXPLICIT_INVAL_DATA = (1 << 25) + CAP_MAP_ALIGNMENT = (1 << 26) + CAP_SUBMOUNTS = (1 << 27) + CAP_HANDLE_KILLPRIV_V2 = (1 << 28) + CAP_SETXATTR_EXT = (1 << 29) + CAP_INIT_EXT = (1 << 30) + CAP_INIT_RESERVED = (1 << 31) + + // CAP_RENAME_SWAP only exists on OSX. + CAP_RENAME_SWAP = 0x0 +) + type Attr struct { Ino uint64 Size uint64 diff --git a/internal/renameat/renameat.go b/internal/renameat/renameat.go new file mode 100644 index 000000000..edf6c6ea3 --- /dev/null +++ b/internal/renameat/renameat.go @@ -0,0 +1,8 @@ +package renameat + +// Renameat is a wrapper around renameat syscall. +// On Linux, it is a wrapper around renameat2(2). +// On Darwin, it is a wrapper around renameatx_np(2). +func Renameat(olddirfd int, oldpath string, newdirfd int, newpath string, flags uint) (err error) { + return renameat(olddirfd, oldpath, newdirfd, newpath, flags) +} diff --git a/internal/renameat/renameat_darwin.go b/internal/renameat/renameat_darwin.go new file mode 100644 index 000000000..b6a8cc64e --- /dev/null +++ b/internal/renameat/renameat_darwin.go @@ -0,0 +1,38 @@ +package renameat + +import ( + "syscall" + "unsafe" +) + +const ( + SYS_RENAMEATX_NP = 488 + RENAME_SWAP = 0x2 + RENAME_EXCHANGE = RENAME_SWAP +) + +func renameat(olddirfd int, oldpath string, newdirfd int, newpath string, flags uint) error { + oldpathCString, err := syscall.BytePtrFromString(oldpath) + if err != nil { + return err + } + newpathCString, err := syscall.BytePtrFromString(newpath) + if err != nil { + return err + } + + _, _, errno := syscall.Syscall6( + SYS_RENAMEATX_NP, + uintptr(olddirfd), + uintptr(unsafe.Pointer(oldpathCString)), + uintptr(newdirfd), + uintptr(unsafe.Pointer(newpathCString)), + uintptr(flags), + 0, + ) + + if errno != 0 { + return errno + } + return nil +} diff --git a/internal/renameat/renameat_linux.go b/internal/renameat/renameat_linux.go new file mode 100644 index 000000000..3d528e37c --- /dev/null +++ b/internal/renameat/renameat_linux.go @@ -0,0 +1,11 @@ +package renameat + +import "golang.org/x/sys/unix" + +const ( + RENAME_EXCHANGE = unix.RENAME_EXCHANGE +) + +func renameat(olddirfd int, oldpath string, newdirfd int, newpath string, flags uint) (err error) { + return unix.Renameat2(olddirfd, oldpath, newdirfd, newpath, flags) +} diff --git a/posixtest/posixtest_test.go b/posixtest/posixtest_test.go new file mode 100644 index 000000000..2dc739be6 --- /dev/null +++ b/posixtest/posixtest_test.go @@ -0,0 +1,20 @@ +// Copyright 2023 the Go-FUSE Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package posixtest + +import "testing" + +func TestAll(t *testing.T) { + for k, fn := range All { + if k == "FcntlFlockLocksFile" { + // TODO - fix this test. + continue + } + t.Run(k, func(t *testing.T) { + dir := t.TempDir() + fn(t, dir) + }) + } +} diff --git a/posixtest/test.go b/posixtest/test.go index 8fc03ba7b..10b06a498 100644 --- a/posixtest/test.go +++ b/posixtest/test.go @@ -7,6 +7,7 @@ package posixtest import ( "bytes" + "errors" "fmt" "io/ioutil" "os" @@ -34,6 +35,7 @@ var All = map[string]func(*testing.T, string){ "Link": Link, "LinkUnlinkRename": LinkUnlinkRename, "LseekHoleSeeksToEOF": LseekHoleSeeksToEOF, + "LseekEnxioCheck": LseekEnxioCheck, "RenameOverwriteDestNoExist": RenameOverwriteDestNoExist, "RenameOverwriteDestExist": RenameOverwriteDestExist, "RenameOpenDir": RenameOpenDir, @@ -720,3 +722,48 @@ func LseekHoleSeeksToEOF(t *testing.T, mnt string) { t.Errorf("got offset %d, want %d", off, len(content)) } } + +func LseekEnxioCheck(t *testing.T, mnt string) { + fn := filepath.Join(mnt, "file.bin") + content := bytes.Repeat([]byte("abcxyz\n"), 1024) + if err := ioutil.WriteFile(fn, content, 0644); err != nil { + t.Fatalf("WriteFile: %v", err) + } + + fd, err := syscall.Open(fn, syscall.O_RDONLY, 0644) + if err != nil { + t.Fatalf("Open: %v", err) + } + defer syscall.Close(fd) + + testCases := []struct { + name string + offset int64 + whence int + }{ + { + name: "Lseek SEEK_DATA where offset is at EOF returns ENXIO", + offset: int64(len(content)), + whence: unix.SEEK_DATA, + }, + { + name: "Lseek SEEK_DATA where offset greater than EOF returns ENXIO", + offset: int64(len(content)) + 1, + whence: unix.SEEK_DATA, + }, + { + name: "Lseek SEEK_HOLE where offset is greater than EOF returns ENXIO", + offset: int64(len(content)) + 1, + whence: unix.SEEK_HOLE, + }, + } + + for _, tc := range testCases { + _, err := unix.Seek(fd, tc.offset, tc.whence) + if err != nil { + if !errors.Is(err, syscall.ENXIO) { + t.Errorf("Failed test case: %s; got %v, want %v", tc.name, err, syscall.ENXIO) + } + } + } +} diff --git a/splice/pair_linux.go b/splice/pair_linux.go index f8923e307..fafdebfc9 100644 --- a/splice/pair_linux.go +++ b/splice/pair_linux.go @@ -40,7 +40,7 @@ func (p *Pair) WriteTo(fd uintptr, n int) (int, error) { const _SPLICE_F_NONBLOCK = 0x2 func (p *Pair) discard() { - _, err := syscall.Splice(p.r, nil, int(devNullFD), nil, int(p.size), _SPLICE_F_NONBLOCK) + _, err := syscall.Splice(p.r, nil, devNullFD(), nil, int(p.size), _SPLICE_F_NONBLOCK) if err == syscall.EAGAIN { // all good. } else if err != nil { diff --git a/splice/splice.go b/splice/splice.go index cbb20b4e0..d5ca215cc 100644 --- a/splice/splice.go +++ b/splice/splice.go @@ -11,6 +11,7 @@ import ( "io/ioutil" "log" "os" + "sync" "syscall" ) @@ -30,8 +31,10 @@ func MaxPipeSize() int { // Since Linux 2.6.11, the pipe capacity is 65536 bytes. const DefaultPipeSize = 16 * 4096 -// We empty pipes by splicing to /dev/null. -var devNullFD uintptr +var ( + devNullFDOnce sync.Once + devNullFDValue int +) func init() { content, err := ioutil.ReadFile("/proc/sys/fs/pipe-max-size") @@ -51,13 +54,18 @@ func init() { resizable = resizable && (errNo == 0) r.Close() w.Close() +} - fd, err := syscall.Open("/dev/null", os.O_WRONLY, 0) - if err != nil { - log.Panicf("splice: %v", err) - } - - devNullFD = uintptr(fd) +// We empty pipes by splicing to /dev/null. +func devNullFD() int { + devNullFDOnce.Do(func() { + fd, err := syscall.Open("/dev/null", os.O_WRONLY, 0) + if err != nil { + panic(fmt.Sprintf("failed to open /dev/null: %s", err)) + } + devNullFDValue = fd + }) + return devNullFDValue } // copy & paste from syscall.