aboutsummaryrefslogtreecommitdiff
path: root/internal/syscallcompat
diff options
context:
space:
mode:
Diffstat (limited to 'internal/syscallcompat')
-rw-r--r--internal/syscallcompat/asuser_freebsd.go24
-rw-r--r--internal/syscallcompat/emulate.go2
-rw-r--r--internal/syscallcompat/emulate_test.go2
-rw-r--r--internal/syscallcompat/quirks.go7
-rw-r--r--internal/syscallcompat/quirks_darwin.go2
-rw-r--r--internal/syscallcompat/quirks_freebsd.go22
-rw-r--r--internal/syscallcompat/quirks_linux.go40
-rw-r--r--internal/syscallcompat/sys_common.go76
-rw-r--r--internal/syscallcompat/sys_darwin.go11
-rw-r--r--internal/syscallcompat/sys_freebsd.go165
-rw-r--r--internal/syscallcompat/sys_linux.go21
-rw-r--r--internal/syscallcompat/unix2syscall.go (renamed from internal/syscallcompat/unix2syscall_darwin.go)2
12 files changed, 331 insertions, 43 deletions
diff --git a/internal/syscallcompat/asuser_freebsd.go b/internal/syscallcompat/asuser_freebsd.go
new file mode 100644
index 0000000..dfa8e18
--- /dev/null
+++ b/internal/syscallcompat/asuser_freebsd.go
@@ -0,0 +1,24 @@
+package syscallcompat
+
+import (
+ "golang.org/x/sys/unix"
+
+ "github.com/hanwen/go-fuse/v2/fuse"
+
+ "github.com/rfjakob/gocryptfs/v2/internal/tlog"
+)
+
+// asUser runs `f()` under the effective uid, gid, groups specified
+// in `context`.
+//
+// If `context` is nil, `f()` is executed directly without switching user id.
+//
+// FreeBSD does not support changing uid/gid per thread. If context is not nil,
+// an error is returned.
+func asUser(f func() (int, error), context *fuse.Context) (int, error) {
+ if context == nil {
+ return f()
+ }
+ tlog.Warn.Printf("asUser: error, only nil context is supported\n")
+ return 0, unix.EOPNOTSUPP
+}
diff --git a/internal/syscallcompat/emulate.go b/internal/syscallcompat/emulate.go
index 91b592b..435c579 100644
--- a/internal/syscallcompat/emulate.go
+++ b/internal/syscallcompat/emulate.go
@@ -1,3 +1,5 @@
+//go:build !freebsd
+
package syscallcompat
import (
diff --git a/internal/syscallcompat/emulate_test.go b/internal/syscallcompat/emulate_test.go
index 16383f2..907ba3a 100644
--- a/internal/syscallcompat/emulate_test.go
+++ b/internal/syscallcompat/emulate_test.go
@@ -1,3 +1,5 @@
+//go:build !freebsd
+
package syscallcompat
import (
diff --git a/internal/syscallcompat/quirks.go b/internal/syscallcompat/quirks.go
index e30d605..36bcb9f 100644
--- a/internal/syscallcompat/quirks.go
+++ b/internal/syscallcompat/quirks.go
@@ -5,16 +5,17 @@ import (
)
const (
- // QuirkBrokenFalloc means the falloc is broken.
+ // QuirkBtrfsBrokenFalloc means the falloc is broken.
// Preallocation on Btrfs is broken ( https://github.com/rfjakob/gocryptfs/issues/395 )
// and slow ( https://github.com/rfjakob/gocryptfs/issues/63 ).
- QuirkBrokenFalloc = uint64(1 << iota)
+ QuirkBtrfsBrokenFalloc = uint64(1 << iota)
// QuirkDuplicateIno1 means that we have duplicate inode numbers.
// On MacOS ExFAT, all empty files share inode number 1:
// https://github.com/rfjakob/gocryptfs/issues/585
QuirkDuplicateIno1
)
-func logQuirk(s string) {
+// LogQuirk prints a yellow message about a detected quirk.
+func LogQuirk(s string) {
tlog.Info.Println(tlog.ColorYellow + "DetectQuirks: " + s + tlog.ColorReset)
}
diff --git a/internal/syscallcompat/quirks_darwin.go b/internal/syscallcompat/quirks_darwin.go
index 4adeea1..c4d5006 100644
--- a/internal/syscallcompat/quirks_darwin.go
+++ b/internal/syscallcompat/quirks_darwin.go
@@ -33,7 +33,7 @@ func DetectQuirks(cipherdir string) (q uint64) {
// On MacOS ExFAT, all empty files share inode number 1:
// https://github.com/rfjakob/gocryptfs/issues/585
if fstypename == FstypenameExfat {
- logQuirk("ExFAT detected, disabling hard links. See https://github.com/rfjakob/gocryptfs/issues/585 for why.")
+ LogQuirk("ExFAT detected, disabling hard links. See https://github.com/rfjakob/gocryptfs/issues/585 for why.")
q |= QuirkDuplicateIno1
}
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/quirks_linux.go b/internal/syscallcompat/quirks_linux.go
index bcdcf07..35f754d 100644
--- a/internal/syscallcompat/quirks_linux.go
+++ b/internal/syscallcompat/quirks_linux.go
@@ -1,11 +1,38 @@
package syscallcompat
import (
+ "syscall"
+
"golang.org/x/sys/unix"
"github.com/rfjakob/gocryptfs/v2/internal/tlog"
)
+// FS_NOCOW_FL is the flag set by "chattr +C" to disable copy-on-write on
+// btrfs. Not exported by golang.org/x/sys/unix, value from linux/fs.h.
+const FS_NOCOW_FL = 0x00800000
+
+// dirHasNoCow checks whether the directory at the given path has the
+// NOCOW (No Copy-on-Write) attribute set (i.e. "chattr +C").
+// When a directory has this attribute, files created within it inherit
+// NOCOW, which makes fallocate work correctly on btrfs because writes
+// go in-place rather than through COW.
+func dirHasNoCow(path string) bool {
+ fd, err := syscall.Open(path, syscall.O_RDONLY|syscall.O_DIRECTORY, 0)
+ if err != nil {
+ tlog.Debug.Printf("dirHasNoCow: Open %q failed: %v", path, err)
+ return false
+ }
+ defer syscall.Close(fd)
+
+ flags, err := unix.IoctlGetInt(fd, unix.FS_IOC_GETFLAGS)
+ if err != nil {
+ tlog.Debug.Printf("dirHasNoCow: FS_IOC_GETFLAGS on %q failed: %v", path, err)
+ return false
+ }
+ return flags&FS_NOCOW_FL != 0
+}
+
// DetectQuirks decides if there are known quirks on the backing filesystem
// that need to be workarounded.
//
@@ -21,10 +48,19 @@ func DetectQuirks(cipherdir string) (q uint64) {
// Preallocation on Btrfs is broken ( https://github.com/rfjakob/gocryptfs/issues/395 )
// and slow ( https://github.com/rfjakob/gocryptfs/issues/63 ).
//
+ // The root cause is that btrfs COW allocates new blocks on write even for
+ // preallocated extents, defeating the purpose of fallocate. However, if the
+ // backing directory has the NOCOW attribute (chattr +C), writes go in-place
+ // and fallocate works correctly.
+ //
// Cast to uint32 avoids compile error on arm: "constant 2435016766 overflows int32"
if uint32(st.Type) == unix.BTRFS_SUPER_MAGIC {
- logQuirk("Btrfs detected, forcing -noprealloc. See https://github.com/rfjakob/gocryptfs/issues/395 for why.")
- q |= QuirkBrokenFalloc
+ if dirHasNoCow(cipherdir) {
+ tlog.Debug.Printf("DetectQuirks: Btrfs detected but cipherdir has NOCOW attribute (chattr +C), fallocate should work correctly")
+ } else {
+ // LogQuirk is called in fusefrontend/root_node.go
+ q |= QuirkBtrfsBrokenFalloc
+ }
}
return q
diff --git a/internal/syscallcompat/sys_common.go b/internal/syscallcompat/sys_common.go
index 1aa6a6e..4f84d98 100644
--- a/internal/syscallcompat/sys_common.go
+++ b/internal/syscallcompat/sys_common.go
@@ -54,10 +54,10 @@ func Openat(dirfd int, path string, flags int, mode uint32) (fd int, err error)
flags |= syscall.O_EXCL
}
} else {
- // If O_CREAT is not used, we should use O_NOFOLLOW
- if flags&syscall.O_NOFOLLOW == 0 {
- tlog.Warn.Printf("Openat: O_NOFOLLOW missing: flags = %#x", flags)
- flags |= syscall.O_NOFOLLOW
+ // If O_CREAT is not used, we should use O_NOFOLLOW or O_SYMLINK
+ if flags&(unix.O_NOFOLLOW|OpenatFlagNofollowSymlink) == 0 {
+ tlog.Warn.Printf("Openat: O_NOFOLLOW/O_SYMLINK missing: flags = %#x", flags)
+ flags |= unix.O_NOFOLLOW
}
}
@@ -112,10 +112,10 @@ const XATTR_SIZE_MAX = 65536
// Make the buffer 1kB bigger so we can detect overflows. Unfortunately,
// slices larger than 64kB are always allocated on the heap.
-const XATTR_BUFSZ = XATTR_SIZE_MAX + 1024
+const GETXATTR_BUFSZ_BIG = XATTR_SIZE_MAX + 1024
// We try with a small buffer first - this one can be allocated on the stack.
-const XATTR_BUFSZ_SMALL = 500
+const GETXATTR_BUFSZ_SMALL = 500
// Fgetxattr is a wrapper around unix.Fgetxattr that handles the buffer sizing.
func Fgetxattr(fd int, attr string) (val []byte, err error) {
@@ -135,10 +135,10 @@ func Lgetxattr(path string, attr string) (val []byte, err error) {
func getxattrSmartBuf(fn func(buf []byte) (int, error)) ([]byte, error) {
// Fastpaths. Important for security.capabilities, which gets queried a lot.
- buf := make([]byte, XATTR_BUFSZ_SMALL)
+ 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)
@@ -159,7 +159,7 @@ func getxattrSmartBuf(fn func(buf []byte) (int, error)) ([]byte, error) {
// We choose the simple approach of buffer that is bigger than the limit on
// Linux, and return an error for everything that is bigger (which can
// only happen on MacOS).
- buf = make([]byte, XATTR_BUFSZ)
+ buf = make([]byte, GETXATTR_BUFSZ_BIG)
sz, err = fn(buf)
if err == syscall.ERANGE {
// Do NOT return ERANGE - the user might retry ad inifinitum!
@@ -182,42 +182,44 @@ out:
// Flistxattr is a wrapper for unix.Flistxattr that handles buffer sizing and
// parsing the returned blob to a string slice.
func Flistxattr(fd int) (attrs []string, err error) {
- // See the buffer sizing comments in getxattrSmartBuf.
- // TODO: smarter buffer sizing?
- buf := make([]byte, XATTR_BUFSZ)
- sz, err := unix.Flistxattr(fd, buf)
- if err == syscall.ERANGE {
- // Do NOT return ERANGE - the user might retry ad inifinitum!
- return nil, syscall.EOVERFLOW
+ listxattrSyscall := func(buf []byte) (int, error) {
+ return unix.Flistxattr(fd, buf)
}
- if err != nil {
- return nil, err
- }
- if sz >= XATTR_SIZE_MAX {
- return nil, syscall.EOVERFLOW
- }
- attrs = parseListxattrBlob(buf[:sz])
- return attrs, nil
+ return listxattrSmartBuf(listxattrSyscall)
}
// Llistxattr is a wrapper for unix.Llistxattr that handles buffer sizing and
// parsing the returned blob to a string slice.
func Llistxattr(path string) (attrs []string, err error) {
- // TODO: smarter buffer sizing?
- buf := make([]byte, XATTR_BUFSZ)
- sz, err := unix.Llistxattr(path, buf)
- if err == syscall.ERANGE {
- // Do NOT return ERANGE - the user might retry ad inifinitum!
- return nil, syscall.EOVERFLOW
+ listxattrSyscall := func(buf []byte) (int, error) {
+ return unix.Llistxattr(path, buf)
}
- if err != nil {
- return nil, err
- }
- if sz >= XATTR_SIZE_MAX {
- return nil, syscall.EOVERFLOW
+ return listxattrSmartBuf(listxattrSyscall)
+}
+
+// listxattrSmartBuf handles smart buffer sizing for Flistxattr and Llistxattr
+func listxattrSmartBuf(listxattrSyscall func([]byte) (int, error)) ([]string, error) {
+ const LISTXATTR_BUFSZ_SMALL = 100
+
+ // Blindly try with the small buffer first
+ buf := make([]byte, LISTXATTR_BUFSZ_SMALL)
+ sz, err := listxattrSyscall(buf)
+ if err == syscall.ERANGE {
+ // Did not fit. Find the actual size
+ sz, err = listxattrSyscall(nil)
+ if err != nil {
+ return nil, err
+ }
+ // ...and allocate the buffer to fit
+ buf = make([]byte, sz)
+ sz, err = listxattrSyscall(buf)
+ if err != nil {
+ // When an xattr got added between the size probe and here,
+ // we could fail with ERANGE. This is ok as the caller will retry.
+ return nil, err
+ }
}
- attrs = parseListxattrBlob(buf[:sz])
- return attrs, nil
+ return parseListxattrBlob(buf[:sz]), nil
}
func parseListxattrBlob(buf []byte) (attrs []string) {
diff --git a/internal/syscallcompat/sys_darwin.go b/internal/syscallcompat/sys_darwin.go
index 0ebdd3b..07eb30a 100644
--- a/internal/syscallcompat/sys_darwin.go
+++ b/internal/syscallcompat/sys_darwin.go
@@ -24,9 +24,20 @@ 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
+
+ // On Darwin we use O_SYMLINK which allows opening a symlink itself.
+ // On Linux, we only have O_NOFOLLOW.
+ OpenatFlagNofollowSymlink = unix.O_SYMLINK
+
+ // F_OFD_SETLKW only exists on Linux. On Darwin, fall back to F_SETLKW as a
+ // flawed replacement.
+ F_OFD_SETLKW = unix.F_SETLKW
+ F_OFD_SETLK = unix.F_SETLK
)
// Unfortunately fsetattrlist does not have a syscall wrapper yet.
diff --git a/internal/syscallcompat/sys_freebsd.go b/internal/syscallcompat/sys_freebsd.go
new file mode 100644
index 0000000..0d28f3e
--- /dev/null
+++ b/internal/syscallcompat/sys_freebsd.go
@@ -0,0 +1,165 @@
+// Package syscallcompat wraps FreeBSD-specific syscalls
+package syscallcompat
+
+import (
+ "time"
+
+ "golang.org/x/sys/unix"
+
+ "github.com/hanwen/go-fuse/v2/fuse"
+
+ "github.com/rfjakob/gocryptfs/v2/internal/tlog"
+)
+
+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
+
+ // On FreeBSD, we only have O_NOFOLLOW.
+ OpenatFlagNofollowSymlink = unix.O_NOFOLLOW
+
+ // For the utimensat syscall on FreeBSD
+ AT_EMPTY_PATH = 0x4000
+
+ // F_OFD_SETLKW only exists on Linux. On Darwin, fall back to F_SETLKW as a
+ // flawed replacement.
+ F_OFD_SETLKW = unix.F_SETLKW
+ F_OFD_SETLK = unix.F_SETLK
+)
+
+// 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 {
+ tlog.Warn.Printf("Fallocate: unsupported mode\n")
+ return unix.EOPNOTSUPP
+ }
+ _, _, 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.
+func FchmodatNofollow(dirfd int, path string, mode uint32) (err error) {
+ return unix.Fchmodat(dirfd, path, mode, unix.AT_SYMLINK_NOFOLLOW)
+}
+
+// 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)
+ return unix.UtimesNanoAt(unix.AT_FDCWD, "", ts, AT_EMPTY_PATH)
+}
+
+// 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 FreeBSD, so we have to wrap it here.
+// Retries on EINTR.
+// The RENAME_EXCHANGE and RENAME_WHITEOUT flags are not supported.
+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 {
+ return unix.EINVAL
+ }
+ 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..ffa5a97 100644
--- a/internal/syscallcompat/sys_linux.go
+++ b/internal/syscallcompat/sys_linux.go
@@ -28,6 +28,18 @@ const (
RENAME_NOREPLACE = unix.RENAME_NOREPLACE
RENAME_WHITEOUT = unix.RENAME_WHITEOUT
RENAME_EXCHANGE = unix.RENAME_EXCHANGE
+
+ // On Darwin we use O_SYMLINK which allows opening a symlink itself.
+ // On Linux, we only have O_NOFOLLOW.
+ OpenatFlagNofollowSymlink = unix.O_NOFOLLOW
+
+ // Only defined on Linux
+ ENODATA = unix.ENODATA
+
+ // F_OFD_SETLKW only exists on Linux. On Darwin, fall back to F_SETLKW as a
+ // flawed replacement.
+ F_OFD_SETLKW = unix.F_OFD_SETLKW
+ F_OFD_SETLK = unix.F_OFD_SETLK
)
var preallocWarn sync.Once
@@ -68,6 +80,15 @@ func Mknodat(dirfd int, path string, mode uint32, dev int) (err error) {
return syscall.Mknodat(dirfd, path, mode, dev)
}
+// Statx wraps the Statx syscall.
+// Retries on EINTR.
+func Statx(dirfd int, path string, flags int, mask int, st *unix.Statx_t) (err error) {
+ err = retryEINTR(func() error {
+ return unix.Statx(dirfd, path, flags, mask, st)
+ })
+ return err
+}
+
// 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) {
diff --git a/internal/syscallcompat/unix2syscall_darwin.go b/internal/syscallcompat/unix2syscall.go
index 5767a27..fa2e8c4 100644
--- a/internal/syscallcompat/unix2syscall_darwin.go
+++ b/internal/syscallcompat/unix2syscall.go
@@ -1,3 +1,5 @@
+//go:build darwin || freebsd
+
package syscallcompat
import (