From a0e278f86f7157be39df9d71b43f6dbaae4d72b8 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Mon, 3 Aug 2026 21:55:59 -0700 Subject: [PATCH] mount: forward extended attributes on windows (#10554) weed/mount implements all four xattr operations and the filer stores the values, but the Windows adapter overrode none of them, so cgofuse's defaults answered every call with 'not implemented'. WinFsp advertises extended attribute support either way, because cgofuse registers the callbacks unconditionally, so applications were told the volume has them and then refused on every use. Attributes written from Linux were invisible from Windows. Untested in CI: exercising Windows extended attributes needs the native NtSetEaFile path rather than anything in os or PowerShell. --- weed/mount/syscall_shim_windows_test.go | 16 +++++ weed/mount/winfsp/errno_windows_test.go | 9 +++ weed/mount/winfsp/xattr_windows.go | 90 +++++++++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 weed/mount/syscall_shim_windows_test.go create mode 100644 weed/mount/winfsp/xattr_windows.go diff --git a/weed/mount/syscall_shim_windows_test.go b/weed/mount/syscall_shim_windows_test.go new file mode 100644 index 000000000..100d52557 --- /dev/null +++ b/weed/mount/syscall_shim_windows_test.go @@ -0,0 +1,16 @@ +package mount + +import "testing" + +// The Windows adapter forwards cgofuse's xattr flags to SetXAttr untouched, +// which only holds while both sides number them the same. cgofuse's side is +// pinned in weed/mount/winfsp; this pins ours, so editing either alone fails +// rather than silently turning a create into a replace. +func TestXattrFlagValues(t *testing.T) { + if xattr_CREATE != 1 { + t.Errorf("xattr_CREATE = %d, want 1", xattr_CREATE) + } + if xattr_REPLACE != 2 { + t.Errorf("xattr_REPLACE = %d, want 2", xattr_REPLACE) + } +} diff --git a/weed/mount/winfsp/errno_windows_test.go b/weed/mount/winfsp/errno_windows_test.go index 500b2cf07..7d553bc5a 100644 --- a/weed/mount/winfsp/errno_windows_test.go +++ b/weed/mount/winfsp/errno_windows_test.go @@ -76,3 +76,12 @@ func TestOpenFlagTranslation(t *testing.T) { t.Errorf("access mode not preserved: %#x", got) } } + +// Setxattr passes cgofuse's flags straight to the raw filesystem, which is +// only correct while the two number them identically. The other side is +// pinned by TestXattrFlagValues in weed/mount, so editing either alone fails. +func TestXattrFlagValues(t *testing.T) { + if cgofuse.XATTR_CREATE != 1 || cgofuse.XATTR_REPLACE != 2 { + t.Fatalf("cgofuse xattr flags moved: create=%d replace=%d", cgofuse.XATTR_CREATE, cgofuse.XATTR_REPLACE) + } +} diff --git a/weed/mount/winfsp/xattr_windows.go b/weed/mount/winfsp/xattr_windows.go new file mode 100644 index 000000000..f25043844 --- /dev/null +++ b/weed/mount/winfsp/xattr_windows.go @@ -0,0 +1,90 @@ +package winfsp + +import ( + "bytes" + + "github.com/seaweedfs/go-fuse/v2/fuse" +) + +// xattrBufferSize bounds one attribute value and one name listing. The mount +// stores extended attributes in the entry itself, so they are small by +// construction; this is the ceiling WinFsp will accept in a single reply. +const xattrBufferSize = 64 * 1024 + +func (w *WinFS) Getxattr(path string, name string) (int, []byte) { + inode, ref, status := w.resolve(path) + if status != fuse.OK { + return toErrno(status), nil + } + defer ref.release() + + dest := make([]byte, xattrBufferSize) + size, status := w.wfs.GetXAttr(never, ptr(w.caller(inode)), name, dest) + if status != fuse.OK { + return toErrno(status), nil + } + return 0, dest[:size] +} + +func (w *WinFS) Setxattr(path string, name string, value []byte, flags int) int { + if w.denied() { + return -eROFS + } + inode, ref, status := w.resolve(path) + if status != fuse.OK { + return toErrno(status) + } + defer ref.release() + + // cgofuse and the raw filesystem number XATTR_CREATE and XATTR_REPLACE the + // same, so the flags pass straight through; TestXattrFlagValues pins that. + in := &fuse.SetXAttrIn{InHeader: w.caller(inode)} + in.Flags = uint32(flags) + in.Size = uint32(len(value)) + return toErrno(w.wfs.SetXAttr(never, in, name, value)) +} + +func (w *WinFS) Removexattr(path string, name string) int { + if w.denied() { + return -eROFS + } + inode, ref, status := w.resolve(path) + if status != fuse.OK { + return toErrno(status) + } + defer ref.release() + return toErrno(w.wfs.RemoveXAttr(never, ptr(w.caller(inode)), name)) +} + +func (w *WinFS) Listxattr(path string, fill func(name string) bool) int { + inode, ref, status := w.resolve(path) + if status != fuse.OK { + return toErrno(status) + } + defer ref.release() + + dest := make([]byte, xattrBufferSize) + size, status := w.wfs.ListXAttr(never, ptr(w.caller(inode)), dest) + if status != fuse.OK { + return toErrno(status) + } + for _, name := range splitXattrNames(dest[:size]) { + if !fill(name) { + break + } + } + return 0 +} + +// splitXattrNames unpacks the NUL-separated listing the raw filesystem +// produces. A trailing NUL terminates the last name rather than starting an +// empty one. +func splitXattrNames(packed []byte) []string { + var names []string + for _, name := range bytes.Split(packed, []byte{0}) { + if len(name) > 0 { + names = append(names, string(name)) + } + } + return names +}