From d313a56f48d5865e1886f0840578de1565646738 Mon Sep 17 00:00:00 2001 From: Dmitriy Smotrov Date: Sat, 11 Nov 2023 19:47:25 +0400 Subject: [PATCH] fs: support renameExchange in loopback for darwin Tested on MacOS 14.2 (macfuse 4.5.0) and Ubuntu 20.04.2 (Linux 5.15.0) Change-Id: I3f4f92c2d2aa3f4fc3696b661fa3cb0e3b43282b --- fs/loopback.go | 36 ++++++++++++ fs/loopback_darwin.go | 4 -- fs/loopback_linux.go | 36 ------------ fs/loopback_linux_test.go | 74 ------------------------- fs/loopback_test.go | 83 ++++++++++++++++++++++++++++ fuse/opcode.go | 6 +- fuse/request.go | 5 +- fuse/request_darwin.go | 2 +- fuse/server.go | 8 ++- fuse/types_linux.go | 3 + internal/renameat/renameat.go | 8 +++ internal/renameat/renameat_darwin.go | 38 +++++++++++++ internal/renameat/renameat_linux.go | 11 ++++ 13 files changed, 196 insertions(+), 118 deletions(-) create mode 100644 fs/loopback_test.go create mode 100644 internal/renameat/renameat.go create mode 100644 internal/renameat/renameat_darwin.go create mode 100644 internal/renameat/renameat_linux.go 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/opcode.go b/fuse/opcode.go index 9cfb7b8c7..3297fee6b 100644 --- a/fuse/opcode.go +++ b/fuse/opcode.go @@ -102,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 @@ -443,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, diff --git a/fuse/request.go b/fuse/request.go index 6f65cd287..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 { @@ -182,6 +182,9 @@ func (r *request) parse() { } 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) 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 8b542c70f..e82abc2fa 100644 --- a/fuse/server.go +++ b/fuse/server.go @@ -508,7 +508,7 @@ func (ms *Server) handleRequest(req *request) Status { defer ms.requestProcessingMu.Unlock() } - req.parse() + req.parse(ms.kernelSettings) if req.handler == nil { req.status = ENOSYS } @@ -906,6 +906,12 @@ func (in *InitIn) SupportsNotify(notifyType int) bool { return false } +// supportsRenameSwap returns whether the kernel supports the +// renamex_np(2) syscall. +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/types_linux.go b/fuse/types_linux.go index 92213fe74..53a11158f 100644 --- a/fuse/types_linux.go +++ b/fuse/types_linux.go @@ -28,6 +28,9 @@ const ( CAP_SETXATTR_EXT = (1 << 29) CAP_INIT_EXT = (1 << 30) CAP_INIT_RESERVED = (1 << 31) + + // CAP_RENAME_SWAP is not supported on Linux. + CAP_RENAME_SWAP = 0x0 ) type Attr struct { 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) +}