aboutsummaryrefslogtreecommitdiff
path: root/internal/syscallcompat
diff options
context:
space:
mode:
Diffstat (limited to 'internal/syscallcompat')
-rw-r--r--internal/syscallcompat/asuser.go59
-rw-r--r--internal/syscallcompat/asuser_darwin.go46
-rw-r--r--internal/syscallcompat/asuser_linux.go80
-rw-r--r--internal/syscallcompat/getdents_test.go5
-rw-r--r--internal/syscallcompat/main_test.go3
-rw-r--r--internal/syscallcompat/quirks.go11
-rw-r--r--internal/syscallcompat/quirks_darwin.go2
-rw-r--r--internal/syscallcompat/quirks_linux.go44
-rw-r--r--internal/syscallcompat/rename_exchange_test.go58
-rw-r--r--internal/syscallcompat/sys_common.go74
-rw-r--r--internal/syscallcompat/sys_darwin.go98
-rw-r--r--internal/syscallcompat/sys_linux.go139
-rw-r--r--internal/syscallcompat/thread_credentials_linux.go61
-rw-r--r--internal/syscallcompat/thread_credentials_linux_32.go40
-rw-r--r--internal/syscallcompat/thread_credentials_linux_other.go37
15 files changed, 497 insertions, 260 deletions
diff --git a/internal/syscallcompat/asuser.go b/internal/syscallcompat/asuser.go
new file mode 100644
index 0000000..0c083ec
--- /dev/null
+++ b/internal/syscallcompat/asuser.go
@@ -0,0 +1,59 @@
+package syscallcompat
+
+import (
+ "golang.org/x/sys/unix"
+
+ "github.com/hanwen/go-fuse/v2/fuse"
+)
+
+// OpenatUser runs the Openat syscall in the context of a different user.
+// It switches the current thread to the new user, performs the syscall,
+// and switches back.
+//
+// If `context` is nil, this function behaves like ordinary Openat (no
+// user switching).
+func OpenatUser(dirfd int, path string, flags int, mode uint32, context *fuse.Context) (fd int, err error) {
+ f := func() (int, error) {
+ return Openat(dirfd, path, flags, mode)
+ }
+ return asUser(f, context)
+}
+
+// MknodatUser runs the Mknodat syscall in the context of a different user.
+// If `context` is nil, this function behaves like ordinary Mknodat.
+//
+// See OpenatUser() for how this works.
+func MknodatUser(dirfd int, path string, mode uint32, dev int, context *fuse.Context) (err error) {
+ f := func() (int, error) {
+ err := Mknodat(dirfd, path, mode, dev)
+ return -1, err
+ }
+ _, err = asUser(f, context)
+ return err
+}
+
+// SymlinkatUser runs the Symlinkat syscall in the context of a different user.
+// If `context` is nil, this function behaves like ordinary Symlinkat.
+//
+// See OpenatUser() for how this works.
+func SymlinkatUser(oldpath string, newdirfd int, newpath string, context *fuse.Context) (err error) {
+ f := func() (int, error) {
+ err := unix.Symlinkat(oldpath, newdirfd, newpath)
+ return -1, err
+ }
+ _, err = asUser(f, context)
+ return err
+}
+
+// MkdiratUser runs the Mkdirat syscall in the context of a different user.
+// If `context` is nil, this function behaves like ordinary Mkdirat.
+//
+// See OpenatUser() for how this works.
+func MkdiratUser(dirfd int, path string, mode uint32, context *fuse.Context) (err error) {
+ f := func() (int, error) {
+ err := unix.Mkdirat(dirfd, path, mode)
+ return -1, err
+ }
+ _, err = asUser(f, context)
+ return err
+}
diff --git a/internal/syscallcompat/asuser_darwin.go b/internal/syscallcompat/asuser_darwin.go
new file mode 100644
index 0000000..5da2782
--- /dev/null
+++ b/internal/syscallcompat/asuser_darwin.go
@@ -0,0 +1,46 @@
+package syscallcompat
+
+import (
+ "runtime"
+ "syscall"
+
+ "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.
+func asUser(f func() (int, error), context *fuse.Context) (int, error) {
+ if context == nil {
+ return f()
+ }
+
+ runtime.LockOSThread()
+ defer runtime.UnlockOSThread()
+
+ err := pthread_setugid_np(context.Owner.Uid, context.Owner.Gid)
+ if err != nil {
+ return -1, err
+ }
+
+ const (
+ // KAUTH_UID_NONE and KAUTH_GID_NONE are special values to
+ // revert permissions to the process credentials.
+ KAUTH_UID_NONE = ^uint32(0) - 100
+ KAUTH_GID_NONE = ^uint32(0) - 100
+ )
+
+ defer pthread_setugid_np(KAUTH_UID_NONE, KAUTH_GID_NONE)
+
+ return f()
+}
+
+// Unfortunately pthread_setugid_np does not have a syscall wrapper yet.
+func pthread_setugid_np(uid uint32, gid uint32) (err error) {
+ _, _, e1 := syscall.RawSyscall(syscall.SYS_SETTID, uintptr(uid), uintptr(gid), 0)
+ if e1 != 0 {
+ err = e1
+ }
+ return
+}
diff --git a/internal/syscallcompat/asuser_linux.go b/internal/syscallcompat/asuser_linux.go
new file mode 100644
index 0000000..39e3ff2
--- /dev/null
+++ b/internal/syscallcompat/asuser_linux.go
@@ -0,0 +1,80 @@
+package syscallcompat
+
+import (
+ "fmt"
+ "os"
+ "runtime"
+ "strconv"
+ "strings"
+
+ "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.
+func asUser(f func() (int, error), context *fuse.Context) (int, error) {
+ if context == nil {
+ return f()
+ }
+
+ runtime.LockOSThread()
+ defer runtime.UnlockOSThread()
+
+ // Since go1.16beta1 (commit d1b1145cace8b968307f9311ff611e4bb810710c ,
+ // https://go-review.googlesource.com/c/go/+/210639 )
+ // syscall.{Setgroups,Setregid,Setreuid} affects all threads, which
+ // is exactly what we not want.
+ //
+ // And unix.{Setgroups,Setregid,Setreuid} also changed to this behavoir in
+ // v0.1.0 (commit d0df966e6959f00dc1c74363e537872647352d51 ,
+ // https://go-review.googlesource.com/c/sys/+/428174 ), so we use
+ // our own syscall wrappers.
+
+ err := Setgroups(getSupplementaryGroups(context.Pid))
+ if err != nil {
+ return -1, err
+ }
+ defer SetgroupsPanic(nil)
+
+ err = Setregid(-1, int(context.Owner.Gid))
+ if err != nil {
+ return -1, err
+ }
+ defer SetregidPanic(-1, 0)
+
+ err = Setreuid(-1, int(context.Owner.Uid))
+ if err != nil {
+ return -1, err
+ }
+ defer SetreuidPanic(-1, 0)
+
+ return f()
+}
+
+func getSupplementaryGroups(pid uint32) (gids []int) {
+ procPath := fmt.Sprintf("/proc/%d/task/%d/status", pid, pid)
+ blob, err := os.ReadFile(procPath)
+ if err != nil {
+ return nil
+ }
+
+ lines := strings.Split(string(blob), "\n")
+ for _, line := range lines {
+ if strings.HasPrefix(line, "Groups:") {
+ f := strings.Fields(line[7:])
+ gids = make([]int, len(f))
+ for i := range gids {
+ val, err := strconv.ParseInt(f[i], 10, 32)
+ if err != nil {
+ return nil
+ }
+ gids[i] = int(val)
+ }
+ return gids
+ }
+ }
+
+ return nil
+}
diff --git a/internal/syscallcompat/getdents_test.go b/internal/syscallcompat/getdents_test.go
index eb670d6..c9e6a99 100644
--- a/internal/syscallcompat/getdents_test.go
+++ b/internal/syscallcompat/getdents_test.go
@@ -4,7 +4,6 @@
package syscallcompat
import (
- "io/ioutil"
"os"
"runtime"
"strings"
@@ -49,13 +48,13 @@ func testGetdents(t *testing.T) {
getdentsUnderTest = emulateGetdents
}
// Fill a directory with filenames of length 1 ... 255
- testDir, err := ioutil.TempDir(tmpDir, "TestGetdents")
+ testDir, err := os.MkdirTemp(tmpDir, "TestGetdents")
if err != nil {
t.Fatal(err)
}
for i := 1; i <= unix.NAME_MAX; i++ {
n := strings.Repeat("x", i)
- err = ioutil.WriteFile(testDir+"/"+n, nil, 0600)
+ err = os.WriteFile(testDir+"/"+n, nil, 0600)
if err != nil {
t.Fatal(err)
}
diff --git a/internal/syscallcompat/main_test.go b/internal/syscallcompat/main_test.go
index ddf6bc4..7183f5a 100644
--- a/internal/syscallcompat/main_test.go
+++ b/internal/syscallcompat/main_test.go
@@ -2,7 +2,6 @@ package syscallcompat
import (
"fmt"
- "io/ioutil"
"os"
"testing"
)
@@ -23,7 +22,7 @@ func TestMain(m *testing.M) {
fmt.Println(err)
os.Exit(1)
}
- tmpDir, err = ioutil.TempDir(parent, "syscallcompat")
+ tmpDir, err = os.MkdirTemp(parent, "syscallcompat")
if err != nil {
fmt.Println(err)
os.Exit(1)
diff --git a/internal/syscallcompat/quirks.go b/internal/syscallcompat/quirks.go
index 858f16d..36bcb9f 100644
--- a/internal/syscallcompat/quirks.go
+++ b/internal/syscallcompat/quirks.go
@@ -5,18 +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
- // QuirkNoUserXattr means that user.* xattrs are not supported
- QuirkNoUserXattr
)
-func logQuirk(s string) {
- tlog.Info.Printf(tlog.ColorYellow + "DetectQuirks: " + s + tlog.ColorReset)
+// 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_linux.go b/internal/syscallcompat/quirks_linux.go
index 5ef2d8a..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,14 +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 uint32(st.Type) == unix.TMPFS_MAGIC {
- logQuirk("tmpfs detected, no extended attributes except acls will work.")
+ 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/rename_exchange_test.go b/internal/syscallcompat/rename_exchange_test.go
new file mode 100644
index 0000000..97a95f8
--- /dev/null
+++ b/internal/syscallcompat/rename_exchange_test.go
@@ -0,0 +1,58 @@
+package syscallcompat
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "golang.org/x/sys/unix"
+)
+
+func TestRenameExchange(t *testing.T) {
+ // Create a temporary directory for testing
+ tmpDir, err := os.MkdirTemp("", "renameat2_test")
+ if err != nil {
+ t.Fatalf("Failed to create temp dir: %v", err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ // Test basic exchange functionality
+ file1 := filepath.Join(tmpDir, "file1.txt")
+ file2 := filepath.Join(tmpDir, "file2.txt")
+
+ content1 := []byte("content of file 1")
+ content2 := []byte("content of file 2")
+
+ if err := os.WriteFile(file1, content1, 0644); err != nil {
+ t.Fatalf("Failed to create file1: %v", err)
+ }
+
+ if err := os.WriteFile(file2, content2, 0644); err != nil {
+ t.Fatalf("Failed to create file2: %v", err)
+ }
+
+ // Test RENAME_EXCHANGE - this is the core functionality for issue #914
+ err = Renameat2(unix.AT_FDCWD, file1, unix.AT_FDCWD, file2, RENAME_EXCHANGE)
+ if err != nil {
+ t.Fatalf("RENAME_EXCHANGE failed: %v", err)
+ }
+
+ // Verify that the files have been swapped
+ newContent1, err := os.ReadFile(file1)
+ if err != nil {
+ t.Fatalf("Failed to read file1 after exchange: %v", err)
+ }
+
+ newContent2, err := os.ReadFile(file2)
+ if err != nil {
+ t.Fatalf("Failed to read file2 after exchange: %v", err)
+ }
+
+ if string(newContent1) != string(content2) {
+ t.Errorf("file1 content after exchange. Expected: %s, Got: %s", content2, newContent1)
+ }
+
+ if string(newContent2) != string(content1) {
+ t.Errorf("file2 content after exchange. Expected: %s, Got: %s", content1, newContent2)
+ }
+}
diff --git a/internal/syscallcompat/sys_common.go b/internal/syscallcompat/sys_common.go
index 1aa6a6e..3cb9ffa 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,7 +135,7 @@ 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 {
@@ -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 06f09f0..ef19f24 100644
--- a/internal/syscallcompat/sys_darwin.go
+++ b/internal/syscallcompat/sys_darwin.go
@@ -3,7 +3,6 @@ package syscallcompat
import (
"log"
"path/filepath"
- "runtime"
"syscall"
"time"
"unsafe"
@@ -21,26 +20,18 @@ const (
// O_PATH is only defined on Linux
O_PATH = 0
+ // Same meaning, different name
+ RENAME_NOREPLACE = unix.RENAME_EXCL
+ RENAME_EXCHANGE = unix.RENAME_SWAP
+
// Only exists on Linux. Define here to fix build failure, even though
- // we will never see the flags.
- RENAME_NOREPLACE = 1
- RENAME_EXCHANGE = 2
- RENAME_WHITEOUT = 4
-
- // KAUTH_UID_NONE and KAUTH_GID_NONE are special values to
- // revert permissions to the process credentials.
- KAUTH_UID_NONE = ^uint32(0) - 100
- KAUTH_GID_NONE = ^uint32(0) - 100
-)
+ // we will never see this flag.
+ RENAME_WHITEOUT = 1 << 30
-// Unfortunately pthread_setugid_np does not have a syscall wrapper yet.
-func pthread_setugid_np(uid uint32, gid uint32) (err error) {
- _, _, e1 := syscall.RawSyscall(syscall.SYS_SETTID, uintptr(uid), uintptr(gid), 0)
- if e1 != 0 {
- err = e1
- }
- return
-}
+ // On Darwin we use O_SYMLINK which allows opening a symlink itself.
+ // On Linux, we only have O_NOFOLLOW.
+ OpenatFlagNofollowSymlink = unix.O_SYMLINK
+)
// Unfortunately fsetattrlist does not have a syscall wrapper yet.
func fsetattrlist(fd int, list unsafe.Pointer, buf unsafe.Pointer, size uintptr, options int) (err error) {
@@ -84,74 +75,14 @@ func Dup3(oldfd int, newfd int, flags int) (err error) {
//// Emulated Syscalls (see emulate.go) ////////////////
////////////////////////////////////////////////////////
-func OpenatUser(dirfd int, path string, flags int, mode uint32, context *fuse.Context) (fd int, err error) {
- if context != nil {
- runtime.LockOSThread()
- defer runtime.UnlockOSThread()
-
- err = pthread_setugid_np(context.Owner.Uid, context.Owner.Gid)
- if err != nil {
- return -1, err
- }
- defer pthread_setugid_np(KAUTH_UID_NONE, KAUTH_GID_NONE)
- }
-
- return Openat(dirfd, path, flags, mode)
-}
-
func Mknodat(dirfd int, path string, mode uint32, dev int) (err error) {
return emulateMknodat(dirfd, path, mode, dev)
}
-func MknodatUser(dirfd int, path string, mode uint32, dev int, context *fuse.Context) (err error) {
- if context != nil {
- runtime.LockOSThread()
- defer runtime.UnlockOSThread()
-
- err = pthread_setugid_np(context.Owner.Uid, context.Owner.Gid)
- if err != nil {
- return err
- }
- defer pthread_setugid_np(KAUTH_UID_NONE, KAUTH_GID_NONE)
- }
-
- return Mknodat(dirfd, path, mode, dev)
-}
-
func FchmodatNofollow(dirfd int, path string, mode uint32) (err error) {
return unix.Fchmodat(dirfd, path, mode, unix.AT_SYMLINK_NOFOLLOW)
}
-func SymlinkatUser(oldpath string, newdirfd int, newpath string, context *fuse.Context) (err error) {
- if context != nil {
- runtime.LockOSThread()
- defer runtime.UnlockOSThread()
-
- err = pthread_setugid_np(context.Owner.Uid, context.Owner.Gid)
- if err != nil {
- return err
- }
- defer pthread_setugid_np(KAUTH_UID_NONE, KAUTH_GID_NONE)
- }
-
- return unix.Symlinkat(oldpath, newdirfd, newpath)
-}
-
-func MkdiratUser(dirfd int, path string, mode uint32, context *fuse.Context) (err error) {
- if context != nil {
- runtime.LockOSThread()
- defer runtime.UnlockOSThread()
-
- err = pthread_setugid_np(context.Owner.Uid, context.Owner.Gid)
- if err != nil {
- return err
- }
- defer pthread_setugid_np(KAUTH_UID_NONE, KAUTH_GID_NONE)
- }
-
- return unix.Mkdirat(dirfd, path, mode)
-}
-
type attrList struct {
bitmapCount uint16
_ uint16
@@ -227,7 +158,12 @@ func GetdentsSpecial(fd int) (entries []fuse.DirEntry, entriesSpecial []fuse.Dir
return emulateGetdents(fd)
}
-// Renameat2 does not exist on Darwin, so we call Renameat and ignore the flags.
+// Renameat2 does not exist on Darwin, but RenameatxNp does.
func Renameat2(olddirfd int, oldpath string, newdirfd int, newpath string, flags uint) (err error) {
- return unix.Renameat(olddirfd, oldpath, newdirfd, newpath)
+ // If no flags are set, use tried and true renameat
+ if flags == 0 {
+ return unix.Renameat(olddirfd, oldpath, newdirfd, newpath)
+ }
+ // Let RenameatxNp handle everything else
+ return unix.RenameatxNp(olddirfd, oldpath, newdirfd, newpath, uint32(flags))
}
diff --git a/internal/syscallcompat/sys_linux.go b/internal/syscallcompat/sys_linux.go
index a64b27e..71478af 100644
--- a/internal/syscallcompat/sys_linux.go
+++ b/internal/syscallcompat/sys_linux.go
@@ -3,10 +3,6 @@ package syscallcompat
import (
"fmt"
- "io/ioutil"
- "runtime"
- "strconv"
- "strings"
"sync"
"syscall"
"time"
@@ -32,6 +28,10 @@ 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
)
var preallocWarn sync.Once
@@ -67,104 +67,11 @@ func Fallocate(fd int, mode uint32, off int64, len int64) (err error) {
return syscall.Fallocate(fd, mode, off, len)
}
-func getSupplementaryGroups(pid uint32) (gids []int) {
- procPath := fmt.Sprintf("/proc/%d/task/%d/status", pid, pid)
- blob, err := ioutil.ReadFile(procPath)
- if err != nil {
- return nil
- }
-
- lines := strings.Split(string(blob), "\n")
- for _, line := range lines {
- if strings.HasPrefix(line, "Groups:") {
- f := strings.Fields(line[7:])
- gids = make([]int, len(f))
- for i := range gids {
- val, err := strconv.ParseInt(f[i], 10, 32)
- if err != nil {
- return nil
- }
- gids[i] = int(val)
- }
- return gids
- }
- }
-
- return nil
-}
-
-// asUser runs `f()` under the effective uid, gid, groups specified
-// in `context`.
-//
-// If `context` is nil, `f()` is executed directly without switching user id.
-func asUser(f func() (int, error), context *fuse.Context) (int, error) {
- if context == nil {
- return f()
- }
-
- runtime.LockOSThread()
- defer runtime.UnlockOSThread()
-
- // Since go1.16beta1 (commit d1b1145cace8b968307f9311ff611e4bb810710c ,
- // https://go-review.googlesource.com/c/go/+/210639 )
- // syscall.{Setgroups,Setregid,Setreuid} affects all threads, which
- // is exactly what we not want.
- //
- // We now use unix.{Setgroups,Setregid,Setreuid} instead.
-
- err := unix.Setgroups(getSupplementaryGroups(context.Pid))
- if err != nil {
- return -1, err
- }
- defer unix.Setgroups(nil)
-
- err = unix.Setregid(-1, int(context.Owner.Gid))
- if err != nil {
- return -1, err
- }
- defer unix.Setregid(-1, 0)
-
- err = unix.Setreuid(-1, int(context.Owner.Uid))
- if err != nil {
- return -1, err
- }
- defer unix.Setreuid(-1, 0)
-
- return f()
-}
-
-// OpenatUser runs the Openat syscall in the context of a different user.
-//
-// It switches the current thread to the new user, performs the syscall,
-// and switches back.
-//
-// If `context` is nil, this function behaves like ordinary Openat (no
-// user switching).
-func OpenatUser(dirfd int, path string, flags int, mode uint32, context *fuse.Context) (fd int, err error) {
- f := func() (int, error) {
- return Openat(dirfd, path, flags, mode)
- }
- return asUser(f, context)
-}
-
// Mknodat wraps the Mknodat syscall.
func Mknodat(dirfd int, path string, mode uint32, dev int) (err error) {
return syscall.Mknodat(dirfd, path, mode, dev)
}
-// MknodatUser runs the Mknodat syscall in the context of a different user.
-// If `context` is nil, this function behaves like ordinary Mknodat.
-//
-// See OpenatUser() for how this works.
-func MknodatUser(dirfd int, path string, mode uint32, dev int, context *fuse.Context) (err error) {
- f := func() (int, error) {
- err := Mknodat(dirfd, path, mode, dev)
- return -1, err
- }
- _, err = asUser(f, context)
- 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) {
@@ -205,32 +112,6 @@ func FchmodatNofollow(dirfd int, path string, mode uint32) (err error) {
return syscall.Chmod(procPath, mode)
}
-// SymlinkatUser runs the Symlinkat syscall in the context of a different user.
-// If `context` is nil, this function behaves like ordinary Symlinkat.
-//
-// See OpenatUser() for how this works.
-func SymlinkatUser(oldpath string, newdirfd int, newpath string, context *fuse.Context) (err error) {
- f := func() (int, error) {
- err := unix.Symlinkat(oldpath, newdirfd, newpath)
- return -1, err
- }
- _, err = asUser(f, context)
- return err
-}
-
-// MkdiratUser runs the Mkdirat syscall in the context of a different user.
-// If `context` is nil, this function behaves like ordinary Mkdirat.
-//
-// See OpenatUser() for how this works.
-func MkdiratUser(dirfd int, path string, mode uint32, context *fuse.Context) (err error) {
- f := func() (int, error) {
- err := unix.Mkdirat(dirfd, path, mode)
- return -1, err
- }
- _, err = asUser(f, context)
- return err
-}
-
// 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).
@@ -247,8 +128,16 @@ func LsetxattrUser(path string, attr string, data []byte, flags int, context *fu
func timesToTimespec(a *time.Time, m *time.Time) []unix.Timespec {
ts := make([]unix.Timespec, 2)
- ts[0] = unix.Timespec(fuse.UtimeToTimespec(a))
- ts[1] = unix.Timespec(fuse.UtimeToTimespec(m))
+ 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
}
diff --git a/internal/syscallcompat/thread_credentials_linux.go b/internal/syscallcompat/thread_credentials_linux.go
new file mode 100644
index 0000000..b5ec6cd
--- /dev/null
+++ b/internal/syscallcompat/thread_credentials_linux.go
@@ -0,0 +1,61 @@
+//go:build linux
+
+// golang.org/x/sys/unix commit
+// https://github.com/golang/sys/commit/d0df966e6959f00dc1c74363e537872647352d51
+// changed unix.Setreuid/unix.Setregid functions to affect the whole thread, which is
+// what gocryptfs does NOT want (https://github.com/rfjakob/gocryptfs/issues/893).
+// The functions Setreuid/Setegid are copy-pasted from one commit before
+// (9e1f76180b77a12eb07c82eb8e1ea8a7f8d202e7).
+//
+// Looking at the diff at https://github.com/golang/sys/commit/d0df966e6959f00dc1c74363e537872647352d51
+// we see that only two architectures, 386 and arm, use SYS_SETREUID32/SYS_SETREGID32
+// (see "man 2 setreuid" for why).
+// All the others architectures use SYS_SETREUID/SYS_SETREGID.
+//
+// As of golang.org/x/sys/unix v0.30.0, Setgroups/setgroups is still per-thread, but
+// it is likely that this will change, too. Setgroups/setgroups are copy-pasted from
+// v0.30.0. The SYS_SETGROUPS32/SYS_SETGROUPS split is the same as for Setreuid.
+//
+// Note: _Gid_t is always uint32 on linux, so we can directly use uint32 for setgroups.
+package syscallcompat
+
+import (
+ "log"
+)
+
+// Setgroups is like setgroups(2) but affects only the current thread
+func Setgroups(gids []int) (err error) {
+ if len(gids) == 0 {
+ return setgroups(0, nil)
+ }
+
+ a := make([]uint32, len(gids))
+ for i, v := range gids {
+ a[i] = uint32(v)
+ }
+ return setgroups(len(a), &a[0])
+}
+
+// SetgroupsPanic calls Setgroups and panics on error
+func SetgroupsPanic(gids []int) {
+ err := Setgroups(gids)
+ if err != nil {
+ log.Panic(err)
+ }
+}
+
+// SetregidPanic calls Setregid and panics on error
+func SetregidPanic(rgid int, egid int) {
+ err := Setregid(rgid, egid)
+ if err != nil {
+ log.Panic(err)
+ }
+}
+
+// SetreuidPanic calls Setreuid and panics on error
+func SetreuidPanic(ruid int, euid int) {
+ err := Setreuid(ruid, euid)
+ if err != nil {
+ log.Panic(err)
+ }
+}
diff --git a/internal/syscallcompat/thread_credentials_linux_32.go b/internal/syscallcompat/thread_credentials_linux_32.go
new file mode 100644
index 0000000..69fffca
--- /dev/null
+++ b/internal/syscallcompat/thread_credentials_linux_32.go
@@ -0,0 +1,40 @@
+//go:build (linux && 386) || (linux && arm)
+
+// Linux on i386 and 32-bit ARM has SYS_SETREUID and friends returning 16-bit values.
+// We need to use SYS_SETREUID32 instead.
+
+package syscallcompat
+
+import (
+ "unsafe"
+
+ "golang.org/x/sys/unix"
+)
+
+// See thread_credentials_linux.go for docs
+
+// Setreuid is like setreuid(2) but affects only the current thread
+func Setreuid(ruid int, euid int) (err error) {
+ _, _, e1 := unix.RawSyscall(unix.SYS_SETREUID32, uintptr(ruid), uintptr(euid), 0)
+ if e1 != 0 {
+ err = e1
+ }
+ return
+}
+
+// Setreuid is like setregid(2) but affects only the current thread
+func Setregid(rgid int, egid int) (err error) {
+ _, _, e1 := unix.RawSyscall(unix.SYS_SETREGID32, uintptr(rgid), uintptr(egid), 0)
+ if e1 != 0 {
+ err = e1
+ }
+ return
+}
+
+func setgroups(n int, list *uint32) (err error) {
+ _, _, e1 := unix.RawSyscall(unix.SYS_SETGROUPS32, uintptr(n), uintptr(unsafe.Pointer(list)), 0)
+ if e1 != 0 {
+ err = e1
+ }
+ return
+}
diff --git a/internal/syscallcompat/thread_credentials_linux_other.go b/internal/syscallcompat/thread_credentials_linux_other.go
new file mode 100644
index 0000000..ab11c71
--- /dev/null
+++ b/internal/syscallcompat/thread_credentials_linux_other.go
@@ -0,0 +1,37 @@
+//go:build !((linux && 386) || (linux && arm))
+
+package syscallcompat
+
+import (
+ "unsafe"
+
+ "golang.org/x/sys/unix"
+)
+
+// See thread_credentials_linux.go for docs
+
+// Setreuid is like setreuid(2) but affects only the current thread
+func Setreuid(ruid int, euid int) (err error) {
+ _, _, e1 := unix.RawSyscall(unix.SYS_SETREUID, uintptr(ruid), uintptr(euid), 0)
+ if e1 != 0 {
+ err = e1
+ }
+ return
+}
+
+// Setreuid is like setregid(2) but affects only the current thread
+func Setregid(rgid int, egid int) (err error) {
+ _, _, e1 := unix.RawSyscall(unix.SYS_SETREGID, uintptr(rgid), uintptr(egid), 0)
+ if e1 != 0 {
+ err = e1
+ }
+ return
+}
+
+func setgroups(n int, list *uint32) (err error) {
+ _, _, e1 := unix.RawSyscall(unix.SYS_SETGROUPS, uintptr(n), uintptr(unsafe.Pointer(list)), 0)
+ if e1 != 0 {
+ err = e1
+ }
+ return
+}