From abe7ee00b8af3f12a2b3ec10a380cae385f143bf Mon Sep 17 00:00:00 2001 From: Dmitriy Smotrov Date: Sat, 11 Nov 2023 19:47:25 +0400 Subject: [PATCH] draft: fs: support renameExchange in loopback for darwin Tested on MacOS 14.2 (macfuse 4.5.0) and Alpine 3.18 in Docker (Linux 6.4) Change-Id: I3f4f92c2d2aa3f4fc3696b661fa3cb0e3b43282b --- fs/loopback.go | 36 ++++++++++++++++++++++++++ fs/loopback_darwin.go | 4 --- fs/loopback_linux.go | 36 -------------------------- fuse/opcode.go | 19 ++++++++++++++ fuse/request_darwin.go | 4 +-- fuse/server.go | 7 ++--- fuse/types_linux.go | 3 +++ internal/renameat/renameat.go | 8 ++++++ internal/renameat/renameat_darwin.go | 38 ++++++++++++++++++++++++++++ internal/renameat/renameat_linux.go | 11 ++++++++ 10 files changed, 121 insertions(+), 45 deletions(-) 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/fuse/opcode.go b/fuse/opcode.go index 9cfb7b8c7..2401675fb 100644 --- a/fuse/opcode.go +++ b/fuse/opcode.go @@ -138,6 +138,19 @@ func doInit(server *Server, req *request) { server.setSplice() } + renameSwap := input.Flags & uint32(CAP_RENAME_SWAP) + if renameSwap != 0 { + server.canRenameSwap = true + + // closed-source macfuse 4.x has broken compatibility with osxfuse 3.x: + // it passes an additional 64-bit field (flags) as in RenameIn, not Rename1In + // macfuse doesn't want change the behaviour back which is motivated by + // not breaking compatibility the second time, look here for details: + // https://github.com/osxfuse/osxfuse/issues/839 + // https://github.com/macfuse/library/blob/eee4f806272fcfba3c8ee662647068f8e3abab72/lib/fuse_lowlevel.c#L1299-L1305 + getHandler(_OP_RENAME).InputSize = unsafe.Sizeof(RenameIn{}) + } + // maxPages is the maximum request size we want the kernel to use, in units of // memory pages (usually 4kiB). Linux v4.19 and older ignore this and always use // 128kiB. @@ -443,6 +456,12 @@ func doSymlink(server *Server, req *request) { } func doRename(server *Server, req *request) { + // see for details: + // https://github.com/macfuse/library/blob/eee4f806272fcfba3c8ee662647068f8e3abab72/lib/fuse_lowlevel.c#L1299-L1305 + if server.canRenameSwap { + doRename2(server, req) + return + } in1 := (*Rename1In)(req.inData) in := RenameIn{ InHeader: in1.InHeader, diff --git a/fuse/request_darwin.go b/fuse/request_darwin.go index 6caddaed3..bb118c9e0 100644 --- a/fuse/request_darwin.go +++ b/fuse/request_darwin.go @@ -8,6 +8,6 @@ const outputHeaderSize = 200 const ( _FUSE_KERNEL_VERSION = 7 - _MINIMUM_MINOR_VERSION = 12 - _OUR_MINOR_VERSION = 12 + _MINIMUM_MINOR_VERSION = 19 + _OUR_MINOR_VERSION = 19 ) diff --git a/fuse/server.go b/fuse/server.go index 8b542c70f..59e58aa5d 100644 --- a/fuse/server.go +++ b/fuse/server.go @@ -73,9 +73,10 @@ type Server struct { retrieveNext uint64 retrieveTab map[uint64]*retrieveCacheRequest // notifyUnique -> retrieve request - singleReader bool - canSplice bool - loops sync.WaitGroup + singleReader bool + canSplice bool + canRenameSwap bool + loops sync.WaitGroup ready chan error 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) +}