// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT //go:build freebsd package nfsfs import ( "os" "strings" "syscall" "unsafe" ) // xattrNamespace is the only namespace this backend serves: RFC 8276 // section 3.3 names the attributes with their namespace, and the local // mapping of the FreeBSD server is the extattr user namespace. const xattrNamespace = "user." // extattrNamespaceUser is EXTATTR_NAMESPACE_USER of sys/sys/extattr.h, // the numeric namespace the extattr family of system calls takes. The // syscall package of FreeBSD exports the traps of the family but not // this constant. const extattrNamespaceUser = 0x1 // xattrPath resolves a handle for the extattr family: the registered path // must still name the handle's device, inode and kind. A symlink is // refused the way the kernel refuses the user namespace on one, so the // path calls never follow a link out of the export. func (l *Local) xattrPath(h Handle) (string, error) { _, path, fi, err := l.revalidate(h) if err != nil { return "", err } if fi.Mode()&os.ModeSymlink != 0 { return "", syscall.EPERM } return path, nil } // GetXattr reads one named attribute of the object. func (l *Local) GetXattr(h Handle, name string, max int) ([]byte, error) { path, err := l.xattrPath(h) if err != nil { return nil, err } attr, ok := strings.CutPrefix(name, xattrNamespace) if !ok { return nil, ErrNoXattr } buf := make([]byte, maxOr(max, 256)) for { n, err := extattrGetFile(path, attr, buf) if err == syscall.ERANGE { if len(buf) > 1<<20 { return nil, syscall.ERANGE } buf = make([]byte, len(buf)*2) continue } if err == syscall.ENOATTR { return nil, ErrNoXattr } if err != nil { return nil, err } return buf[:n], nil } } // SetXattr writes one named attribute under the RFC 8276 mode. The // extattr interface of FreeBSD carries no create and replace flags, so // the two strict modes ask for the attribute first and write it after: // a create of an existing name answers EEXIST and a replace of a // missing one ENOATTR, the same answers the flags of Linux produce. func (l *Local) SetXattr(h Handle, name string, value []byte, mode uint32) error { path, err := l.xattrPath(h) if err != nil { return err } attr, ok := strings.CutPrefix(name, xattrNamespace) if !ok { return syscall.EOPNOTSUPP } switch mode { case XattrModeCreate, XattrModeReplace: _, err := extattrGetFile(path, attr, nil) switch { case err == nil && mode == XattrModeCreate: return syscall.EEXIST case err == syscall.ENOATTR && mode == XattrModeReplace: return syscall.ENOATTR case err != nil && err != syscall.ENOATTR: return err } } return extattrSetFile(path, attr, value) } // ListXattr names the user namespace attributes of the object. The list // of extattr_list_file is a sequence of one length byte and name pairs // with no separators, extattr(2), and its names carry no namespace, so // each name is dressed with the namespace prefix the protocol speaks. func (l *Local) ListXattr(h Handle, max int) ([]string, error) { path, err := l.xattrPath(h) if err != nil { return nil, err } buf := make([]byte, maxOr(max, 1024)) for { n, err := extattrListFile(path, buf) if err == syscall.ERANGE { if len(buf) > 1<<20 { return nil, syscall.ERANGE } buf = make([]byte, len(buf)*2) continue } if err != nil { return nil, err } buf = buf[:n] break } var names []string for len(buf) > 0 { size := int(buf[0]) if size+1 > len(buf) { break } names = append(names, xattrNamespace+string(buf[1:1+size])) buf = buf[1+size:] } return names, nil } // RemoveXattr deletes one named attribute. func (l *Local) RemoveXattr(h Handle, name string) error { path, err := l.xattrPath(h) if err != nil { return err } attr, ok := strings.CutPrefix(name, xattrNamespace) if !ok { return ErrNoXattr } if err := extattrDeleteFile(path, attr); err == syscall.ENOATTR { return ErrNoXattr } else if err != nil { return err } return nil } // maxOr replaces a zero budget with the given default. func maxOr(max, def int) int { if max == 0 || max > 1<<20 { return def } return max } // extattrGetFile reads the attribute attr of path into buf, or names its // size when buf is nil, extattr(2). func extattrGetFile(path, attr string, buf []byte) (int, error) { name, err := syscall.ByteSliceFromString(attr) if err != nil { return 0, err } // SAFETY: the kernel reads the buffer for the length of the call // only, and data stays alive through the unsafe pointer until the // system call returns. var data unsafe.Pointer if len(buf) > 0 { data = unsafe.Pointer(&buf[0]) } n, _, errno := syscall.Syscall6(syscall.SYS_EXTATTR_GET_FILE, uintptr(unsafe.Pointer(syscall.StringBytePtr(path))), extattrNamespaceUser, uintptr(unsafe.Pointer(&name[0])), uintptr(data), uintptr(len(buf)), 0) if errno != 0 { return 0, errno } return int(n), nil } // extattrSetFile writes value into the attribute attr of path, // extattr(2). func extattrSetFile(path, attr string, value []byte) error { name, err := syscall.ByteSliceFromString(attr) if err != nil { return err } // SAFETY: the kernel reads the buffer for the length of the call // only, and value stays alive through the unsafe pointer until the // system call returns. var data unsafe.Pointer if len(value) > 0 { data = unsafe.Pointer(&value[0]) } _, _, errno := syscall.Syscall6(syscall.SYS_EXTATTR_SET_FILE, uintptr(unsafe.Pointer(syscall.StringBytePtr(path))), extattrNamespaceUser, uintptr(unsafe.Pointer(&name[0])), uintptr(data), uintptr(len(value)), 0) if errno != 0 { return errno } return nil } // extattrListFile names the user namespace attributes of path into buf, // or names the size of the list when buf is nil, extattr(2). func extattrListFile(path string, buf []byte) (int, error) { // SAFETY: the kernel writes the buffer for the length of the call // only, and buf stays alive through the unsafe pointer until the // system call returns. var data unsafe.Pointer if len(buf) > 0 { data = unsafe.Pointer(&buf[0]) } n, _, errno := syscall.Syscall6(syscall.SYS_EXTATTR_LIST_FILE, uintptr(unsafe.Pointer(syscall.StringBytePtr(path))), extattrNamespaceUser, uintptr(data), uintptr(len(buf)), 0, 0) if errno != 0 { return 0, errno } return int(n), nil } // extattrDeleteFile takes the attribute attr off path, extattr(2). func extattrDeleteFile(path, attr string) error { name, err := syscall.ByteSliceFromString(attr) if err != nil { return err } _, _, errno := syscall.Syscall(syscall.SYS_EXTATTR_DELETE_FILE, uintptr(unsafe.Pointer(syscall.StringBytePtr(path))), extattrNamespaceUser, uintptr(unsafe.Pointer(&name[0]))) if errno != 0 { return errno } return nil }