aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnkush Patel2026-02-05 14:42:40 +1300
committerAnkush Patel2026-02-14 03:32:14 +1300
commit903fc9d077a81d9224de4207d1672c0b1127cf42 (patch)
tree05ae39d5ebbe41bb64d41d7e0f03df7dac596dae
parent3191c18f67346c95e4dbdfd16b44256ddfe20b4f (diff)
Added basic support for FreeBSD.
-rwxr-xr-xcrossbuild.bash6
-rw-r--r--internal/fusefrontend/node_xattr_freebsd.go72
-rw-r--r--internal/fusefrontend_reverse/node_xattr_freebsd.go43
-rw-r--r--internal/syscallcompat/asuser_freebsd.go16
-rw-r--r--internal/syscallcompat/emulate.go2
-rw-r--r--internal/syscallcompat/quirks_freebsd.go22
-rw-r--r--internal/syscallcompat/sys_common.go2
-rw-r--r--internal/syscallcompat/sys_darwin.go2
-rw-r--r--internal/syscallcompat/sys_freebsd.go201
-rw-r--r--internal/syscallcompat/sys_linux.go3
-rw-r--r--internal/syscallcompat/unix2syscall_freebsd.go28
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)),
+ }
+}