diff options
Diffstat (limited to 'internal/syscallcompat')
| -rw-r--r-- | internal/syscallcompat/asuser_freebsd.go | 24 | ||||
| -rw-r--r-- | internal/syscallcompat/emulate.go | 2 | ||||
| -rw-r--r-- | internal/syscallcompat/emulate_test.go | 2 | ||||
| -rw-r--r-- | internal/syscallcompat/quirks.go | 7 | ||||
| -rw-r--r-- | internal/syscallcompat/quirks_darwin.go | 2 | ||||
| -rw-r--r-- | internal/syscallcompat/quirks_freebsd.go | 22 | ||||
| -rw-r--r-- | internal/syscallcompat/quirks_linux.go | 40 | ||||
| -rw-r--r-- | internal/syscallcompat/sys_common.go | 76 | ||||
| -rw-r--r-- | internal/syscallcompat/sys_darwin.go | 11 | ||||
| -rw-r--r-- | internal/syscallcompat/sys_freebsd.go | 165 | ||||
| -rw-r--r-- | internal/syscallcompat/sys_linux.go | 21 | ||||
| -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 ( |
