diff options
| author | Ankush Patel | 2026-02-05 14:42:40 +1300 |
|---|---|---|
| committer | Ankush Patel | 2026-02-14 03:32:14 +1300 |
| commit | 903fc9d077a81d9224de4207d1672c0b1127cf42 (patch) | |
| tree | 05ae39d5ebbe41bb64d41d7e0f03df7dac596dae | |
| parent | 3191c18f67346c95e4dbdfd16b44256ddfe20b4f (diff) | |
Added basic support for FreeBSD.
| -rwxr-xr-x | crossbuild.bash | 6 | ||||
| -rw-r--r-- | internal/fusefrontend/node_xattr_freebsd.go | 72 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/node_xattr_freebsd.go | 43 | ||||
| -rw-r--r-- | internal/syscallcompat/asuser_freebsd.go | 16 | ||||
| -rw-r--r-- | internal/syscallcompat/emulate.go | 2 | ||||
| -rw-r--r-- | internal/syscallcompat/quirks_freebsd.go | 22 | ||||
| -rw-r--r-- | internal/syscallcompat/sys_common.go | 2 | ||||
| -rw-r--r-- | internal/syscallcompat/sys_darwin.go | 2 | ||||
| -rw-r--r-- | internal/syscallcompat/sys_freebsd.go | 201 | ||||
| -rw-r--r-- | internal/syscallcompat/sys_linux.go | 3 | ||||
| -rw-r--r-- | internal/syscallcompat/unix2syscall_freebsd.go | 28 |
11 files changed, 395 insertions, 2 deletions
diff --git a/crossbuild.bash b/crossbuild.bash index ff773ec..685674e 100755 --- a/crossbuild.bash +++ b/crossbuild.bash @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # Build on all supported architectures & operating systems @@ -31,3 +31,7 @@ time GOOS=darwin GOARCH=amd64 compile_tests # MacOS on Apple Silicon M1. GOOS=darwin GOARCH=arm64 build + +# FreeBSD +GOOS=freebsd GOARCH=amd64 build + diff --git a/internal/fusefrontend/node_xattr_freebsd.go b/internal/fusefrontend/node_xattr_freebsd.go new file mode 100644 index 0000000..770bb95 --- /dev/null +++ b/internal/fusefrontend/node_xattr_freebsd.go @@ -0,0 +1,72 @@ +package fusefrontend + +import ( + "fmt" + + "golang.org/x/sys/unix" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + + "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" +) + +// On FreeBSD, ENODATA is returned when an attribute is not found. +const noSuchAttributeError = unix.ENOATTR + +func filterXattrSetFlags(flags int) int { + return flags +} + +func (n *Node) getXAttr(cAttr string) (out []byte, errno unix.Errno) { + dirfd, cName, errno := n.prepareAtSyscallMyself() + if errno != 0 { + return + } + defer unix.Close(dirfd) + + procPath := fmt.Sprintf("/proc/self/fd/%d/%s", dirfd, cName) + cData, err := syscallcompat.Lgetxattr(procPath, cAttr) + if err != nil { + return nil, fs.ToErrno(err) + } + return cData, 0 +} + +func (n *Node) setXAttr(context *fuse.Context, cAttr string, cData []byte, flags uint32) (errno unix.Errno) { + dirfd, cName, errno := n.prepareAtSyscallMyself() + if errno != 0 { + return + } + defer unix.Close(dirfd) + + procPath := fmt.Sprintf("/proc/self/fd/%d/%s", dirfd, cName) + + return fs.ToErrno(syscallcompat.LsetxattrUser(procPath, cAttr, cData, int(flags), context)) +} + +func (n *Node) removeXAttr(cAttr string) (errno unix.Errno) { + dirfd, cName, errno := n.prepareAtSyscallMyself() + if errno != 0 { + return + } + defer unix.Close(dirfd) + + procPath := fmt.Sprintf("/proc/self/fd/%d/%s", dirfd, cName) + return fs.ToErrno(unix.Lremovexattr(procPath, cAttr)) +} + +func (n *Node) listXAttr() (out []string, errno unix.Errno) { + dirfd, cName, errno := n.prepareAtSyscallMyself() + if errno != 0 { + return + } + defer unix.Close(dirfd) + + procPath := fmt.Sprintf("/proc/self/fd/%d/%s", dirfd, cName) + cNames, err := syscallcompat.Llistxattr(procPath) + if err != nil { + return nil, fs.ToErrno(err) + } + return cNames, 0 +} diff --git a/internal/fusefrontend_reverse/node_xattr_freebsd.go b/internal/fusefrontend_reverse/node_xattr_freebsd.go new file mode 100644 index 0000000..0577521 --- /dev/null +++ b/internal/fusefrontend_reverse/node_xattr_freebsd.go @@ -0,0 +1,43 @@ +package fusefrontend_reverse + +import ( + "fmt" + + "golang.org/x/sys/unix" + + "github.com/hanwen/go-fuse/v2/fs" + + "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" +) + +const noSuchAttributeError = unix.ENOATTR + +func (n *Node) getXAttr(cAttr string) (out []byte, errno unix.Errno) { + d, errno := n.prepareAtSyscall("") + if errno != 0 { + return + } + defer unix.Close(d.dirfd) + + procPath := fmt.Sprintf("/proc/self/fd/%d/%s", d.dirfd, d.pName) + pData, err := syscallcompat.Lgetxattr(procPath, cAttr) + if err != nil { + return nil, fs.ToErrno(err) + } + return pData, 0 +} + +func (n *Node) listXAttr() (out []string, errno unix.Errno) { + d, errno := n.prepareAtSyscall("") + if errno != 0 { + return + } + defer unix.Close(d.dirfd) + + procPath := fmt.Sprintf("/proc/self/fd/%d/%s", d.dirfd, d.pName) + pNames, err := syscallcompat.Llistxattr(procPath) + if err != nil { + return nil, fs.ToErrno(err) + } + return pNames, 0 +} diff --git a/internal/syscallcompat/asuser_freebsd.go b/internal/syscallcompat/asuser_freebsd.go new file mode 100644 index 0000000..34ad41f --- /dev/null +++ b/internal/syscallcompat/asuser_freebsd.go @@ -0,0 +1,16 @@ +package syscallcompat + +import ( + "github.com/hanwen/go-fuse/v2/fuse" +) + +// asUser runs `f()` under the effective uid, gid, groups specified +// in `context`. +// +// If `context` is nil, `f()` is executed directly without switching user id. +// +// WARNING this function is not complete, and always runs f() as if context is nil. +// FreeBSD does not support changing uid/gid per thread. +func asUser(f func() (int, error), context *fuse.Context) (int, error) { + return f() +} diff --git a/internal/syscallcompat/emulate.go b/internal/syscallcompat/emulate.go index 91b592b..cdc4d12 100644 --- a/internal/syscallcompat/emulate.go +++ b/internal/syscallcompat/emulate.go @@ -1,3 +1,5 @@ +//go:build darwin + package syscallcompat import ( diff --git a/internal/syscallcompat/quirks_freebsd.go b/internal/syscallcompat/quirks_freebsd.go new file mode 100644 index 0000000..c340cea --- /dev/null +++ b/internal/syscallcompat/quirks_freebsd.go @@ -0,0 +1,22 @@ +package syscallcompat + +import ( + "golang.org/x/sys/unix" + + "github.com/rfjakob/gocryptfs/v2/internal/tlog" +) + +// DetectQuirks decides if there are known quirks on the backing filesystem +// that need to be workarounded. +// +// Tested by tests/root_test.TestBtrfsQuirks +func DetectQuirks(cipherdir string) (q uint64) { + var st unix.Statfs_t + err := unix.Statfs(cipherdir, &st) + if err != nil { + tlog.Warn.Printf("DetectQuirks: Statfs on %q failed: %v", cipherdir, err) + return 0 + } + + return q +} diff --git a/internal/syscallcompat/sys_common.go b/internal/syscallcompat/sys_common.go index 70ee633..e95373a 100644 --- a/internal/syscallcompat/sys_common.go +++ b/internal/syscallcompat/sys_common.go @@ -138,7 +138,7 @@ func getxattrSmartBuf(fn func(buf []byte) (int, error)) ([]byte, error) { buf := make([]byte, GETXATTR_BUFSZ_SMALL) sz, err := fn(buf) // Non-existing xattr - if err == unix.ENODATA { + if err == ENODATA { return nil, err } // Underlying fs does not support security.capabilities (example: tmpfs) diff --git a/internal/syscallcompat/sys_darwin.go b/internal/syscallcompat/sys_darwin.go index 0ebdd3b..248b5a2 100644 --- a/internal/syscallcompat/sys_darwin.go +++ b/internal/syscallcompat/sys_darwin.go @@ -24,6 +24,8 @@ const ( RENAME_NOREPLACE = unix.RENAME_EXCL RENAME_EXCHANGE = unix.RENAME_SWAP + ENODATA = unix.ENODATA + // Only exists on Linux. Define here to fix build failure, even though // we will never see this flag. RENAME_WHITEOUT = 1 << 30 diff --git a/internal/syscallcompat/sys_freebsd.go b/internal/syscallcompat/sys_freebsd.go new file mode 100644 index 0000000..ec2c402 --- /dev/null +++ b/internal/syscallcompat/sys_freebsd.go @@ -0,0 +1,201 @@ +// Package syscallcompat wraps FreeBSD-specific syscalls +package syscallcompat + +import ( + "errors" + "fmt" + "time" + + "golang.org/x/sys/unix" + + "github.com/hanwen/go-fuse/v2/fuse" +) + +const ( + O_DIRECT = unix.O_DIRECT + + // O_PATH is supported on FreeBSD, but is missing from the sys/unix package + // FreeBSD-15.0 /usr/src/sys/sys/fcntl.h:135 + O_PATH = 0x00400000 + + // Only defined on Linux, but we can emulate the functionality on FreeBSD + // in Renameat2() below + RENAME_NOREPLACE = 0x1 + RENAME_EXCHANGE = 0x2 + RENAME_WHITEOUT = 0x4 + + // ENODATA is only defined on Linux, but FreeBSD provides ENOATTR + ENODATA = unix.ENOATTR +) + +// EnospcPrealloc is supposed to preallocate ciphertext space without +// changing the file size. This guarantees that we don't run out of +// space while writing a ciphertext block (that would corrupt the block). +// +// The fallocate syscall isn't supported on FreeBSD with the same semantics +// as Linux, in particular the _FALLOC_FL_KEEP_SIZE mode isn't supported. +func EnospcPrealloc(fd int, off int64, len int64) (err error) { + return nil +} + +// Fallocate wraps the posix_fallocate() syscall. +// Fallocate returns an error if mode is not 0 +func Fallocate(fd int, mode uint32, off int64, len int64) (err error) { + if mode != 0 { + return errors.New("fallocate unsupported mode") + } + _, _, err = unix.Syscall(unix.SYS_POSIX_FALLOCATE, uintptr(fd), uintptr(off), uintptr(len)) + return err +} + +// Mknodat wraps the Mknodat syscall. +func Mknodat(dirfd int, path string, mode uint32, dev int) (err error) { + return unix.Mknodat(dirfd, path, mode, uint64(dev)) +} + +// Dup3 wraps the Dup3 syscall. We want to use Dup3 rather than Dup2 because Dup2 +// is not implemented on arm64. +func Dup3(oldfd int, newfd int, flags int) (err error) { + return unix.Dup3(oldfd, newfd, flags) +} + +// FchmodatNofollow is like Fchmodat but never follows symlinks. +// +// This should be handled by the AT_SYMLINK_NOFOLLOW flag, but Linux +// does not implement it, so we have to perform an elaborate dance +// with O_PATH and /proc/self/fd. +// +// See also: Qemu implemented the same logic as fchmodat_nofollow(): +// https://git.qemu.org/?p=qemu.git;a=blob;f=hw/9pfs/9p-local.c#l335 +func FchmodatNofollow(dirfd int, path string, mode uint32) (err error) { + // Open handle to the filename (but without opening the actual file). + // This succeeds even when we don't have read permissions to the file. + fd, err := unix.Openat(dirfd, path, unix.O_NOFOLLOW|O_PATH, 0) + if err != nil { + return err + } + defer unix.Close(fd) + + // Now we can check the type without the risk of race-conditions. + // Return syscall.ELOOP if it is a symlink. + var st unix.Stat_t + err = unix.Fstat(fd, &st) + if err != nil { + return err + } + if st.Mode&unix.S_IFMT == unix.S_IFLNK { + return unix.ELOOP + } + + // Change mode of the actual file. Fchmod does not work with O_PATH, + // but Chmod via /proc/self/fd works. + procPath := fmt.Sprintf("/proc/self/fd/%d", fd) + return unix.Chmod(procPath, mode) +} + +// LsetxattrUser runs the Lsetxattr syscall in the context of a different user. +// This is useful when setting ACLs, as the result depends on the user running +// the operation (see fuse-xfstests generic/375). +// +// If `context` is nil, this function behaves like ordinary Lsetxattr. +func LsetxattrUser(path string, attr string, data []byte, flags int, context *fuse.Context) (err error) { + f := func() (int, error) { + err := unix.Lsetxattr(path, attr, data, flags) + return -1, err + } + _, err = asUser(f, context) + return err +} + +func timesToTimespec(a *time.Time, m *time.Time) []unix.Timespec { + ts := make([]unix.Timespec, 2) + if a == nil { + ts[0] = unix.Timespec{Nsec: unix.UTIME_OMIT} + } else { + ts[0], _ = unix.TimeToTimespec(*a) + } + if m == nil { + ts[1] = unix.Timespec{Nsec: unix.UTIME_OMIT} + } else { + ts[1], _ = unix.TimeToTimespec(*m) + } + return ts +} + +// FutimesNano syscall. +func FutimesNano(fd int, a *time.Time, m *time.Time) (err error) { + ts := timesToTimespec(a, m) + // To avoid introducing a separate syscall wrapper for futimens() + // (as done in go-fuse, for example), we instead use the /proc/self/fd trick. + procPath := fmt.Sprintf("/proc/self/fd/%d", fd) + return unix.UtimesNanoAt(unix.AT_FDCWD, procPath, ts, 0) +} + +// UtimesNanoAtNofollow is like UtimesNanoAt but never follows symlinks. +// Retries on EINTR. +func UtimesNanoAtNofollow(dirfd int, path string, a *time.Time, m *time.Time) (err error) { + ts := timesToTimespec(a, m) + err = retryEINTR(func() error { + return unix.UtimesNanoAt(dirfd, path, ts, unix.AT_SYMLINK_NOFOLLOW) + }) + return err +} + +// Getdents syscall with "." and ".." filtered out. +func Getdents(fd int) ([]fuse.DirEntry, error) { + entries, _, err := emulateGetdents(fd) + return entries, err +} + +// GetdentsSpecial calls the Getdents syscall, +// with normal entries and "." / ".." split into two slices. +func GetdentsSpecial(fd int) (entries []fuse.DirEntry, entriesSpecial []fuse.DirEntry, err error) { + return emulateGetdents(fd) +} + +// Renameat2 does not exist on Darwin, so we have to wrap it here. +// Retries on EINTR. +func Renameat2(olddirfd int, oldpath string, newdirfd int, newpath string, flags uint) (err error) { + if flags&(RENAME_NOREPLACE|RENAME_EXCHANGE) == RENAME_NOREPLACE|RENAME_EXCHANGE { + return unix.EINVAL + } + if flags&(RENAME_NOREPLACE|RENAME_EXCHANGE) == RENAME_NOREPLACE|RENAME_EXCHANGE { + return unix.EINVAL + } + + if flags&RENAME_NOREPLACE != 0 { + var st unix.Stat_t + err = unix.Fstatat(newdirfd, newpath, &st, 0) + if err == nil { + // Assume newpath is an existing file if we can stat() it. + // On Linux, RENAME_NOREPLACE fails with EEXIST if newpath + // already exists. + return unix.EEXIST + } + } + if flags&RENAME_EXCHANGE != 0 { + // Note that on Linux, RENAME_EXCHANGE can handle oldpath and + // newpath of different file types (e.g. directory and + // symbolic link). On FreeBSD the file types must be the same. + var stold, stnew unix.Stat_t + err = unix.Fstatat(olddirfd, oldpath, &stold, 0) + if err != nil { + // Assume file does not exist if we can't stat() it. + // On Linux, RENAME_EXCHANGE requires both oldpath + // and newpath exist. + return unix.ENOENT + } + err = unix.Fstatat(newdirfd, newpath, &stnew, 0) + if err != nil { + // Assume file does not exist if we can't stat() it. + // On Linux, RENAME_EXCHANGE requires both oldpath + // and newpath exist. + return unix.ENOENT + } + } + if flags&RENAME_WHITEOUT != 0 { + return unix.EINVAL + } + + return unix.Renameat(olddirfd, oldpath, newdirfd, newpath) +} diff --git a/internal/syscallcompat/sys_linux.go b/internal/syscallcompat/sys_linux.go index 19d2c56..faa7ffc 100644 --- a/internal/syscallcompat/sys_linux.go +++ b/internal/syscallcompat/sys_linux.go @@ -28,6 +28,9 @@ const ( RENAME_NOREPLACE = unix.RENAME_NOREPLACE RENAME_WHITEOUT = unix.RENAME_WHITEOUT RENAME_EXCHANGE = unix.RENAME_EXCHANGE + + // Only defined on Linux + ENODATA = unix.ENODATA ) var preallocWarn sync.Once diff --git a/internal/syscallcompat/unix2syscall_freebsd.go b/internal/syscallcompat/unix2syscall_freebsd.go new file mode 100644 index 0000000..fe85cf6 --- /dev/null +++ b/internal/syscallcompat/unix2syscall_freebsd.go @@ -0,0 +1,28 @@ +package syscallcompat + +import ( + "syscall" + + "golang.org/x/sys/unix" +) + +// Unix2syscall converts a unix.Stat_t struct to a syscall.Stat_t struct. +// A direct cast does not work because the padding is named differently in +// unix.Stat_t for some reason ("X__unused" in syscall, "_" in unix). +func Unix2syscall(u unix.Stat_t) syscall.Stat_t { + return syscall.Stat_t{ + Dev: u.Dev, + Ino: u.Ino, + Nlink: u.Nlink, + Mode: u.Mode, + Uid: u.Uid, + Gid: u.Gid, + Rdev: u.Rdev, + Size: u.Size, + Blksize: u.Blksize, + Blocks: u.Blocks, + Atimespec: syscall.NsecToTimespec(unix.TimespecToNsec(u.Atim)), + Mtimespec: syscall.NsecToTimespec(unix.TimespecToNsec(u.Mtim)), + Ctimespec: syscall.NsecToTimespec(unix.TimespecToNsec(u.Ctim)), + } +} |
