From 682e642cfa556c2d72c4050bfb8e38febdec49b3 Mon Sep 17 00:00:00 2001
From: Sebastian Lackner
Date: Wed, 16 Jan 2019 15:38:32 +0100
Subject: fusefrontend: Rework the Utimens handling on macOS.

For Linux, everything effectively stays the same. For both path-based and
fd-based Utimens() calls, we use unix.UtimesNanoAt(). To avoid introducing
a separate syscall wrapper for futimens() (as done in go-fuse, for example),
we instead use the /proc/self/fd - trick.

On macOS, this changes quite a lot:

* Path-based Utimens() calls were previously completely broken, since
  unix.UtimensNanoAt() ignores the passed file descriptor. Note that this
  cannot be fixed easily since there IS no appropriate syscall available on
  macOS prior to High Sierra (10.13). We emulate this case by using
  Fchdir() + setattrlist().

* Fd-based Utimens() calls were previously translated to f.GetAttr() (to
  fill any empty parameters) and syscall.Futimes(), which does not does
  support nanosecond precision. Both issues can be fixed by switching to
  fsetattrlist().

Fixes https://github.com/rfjakob/gocryptfs/issues/350
---
 internal/fusefrontend/file.go        |  6 +--
 internal/fusefrontend/fs.go          |  5 +--
 internal/syscallcompat/sys_darwin.go | 87 ++++++++++++++++++++++++++++++++++++
 internal/syscallcompat/sys_linux.go  | 23 ++++++++++
 4 files changed, 113 insertions(+), 8 deletions(-)

(limited to 'internal')

diff --git a/internal/fusefrontend/file.go b/internal/fusefrontend/file.go
index a0b82ba..531274a 100644
--- a/internal/fusefrontend/file.go
+++ b/internal/fusefrontend/file.go
@@ -44,8 +44,6 @@ type File struct {
 	qIno openfiletable.QIno
 	// Entry in the open file table
 	fileTableEntry *openfiletable.Entry
-	// go-fuse nodefs.loopbackFile
-	loopbackFile nodefs.File
 	// Store where the last byte was written
 	lastWrittenOffset int64
 	// The opCount is used to judge whether "lastWrittenOffset" is still
@@ -75,7 +73,6 @@ func NewFile(fd *os.File, fs *FS) (*File, fuse.Status) {
 		contentEnc:     fs.contentEnc,
 		qIno:           qi,
 		fileTableEntry: e,
-		loopbackFile:   nodefs.NewLoopbackFile(fd),
 		fs:             fs,
 		File:           nodefs.NewDefaultFile(),
 	}, fuse.OK
@@ -474,5 +471,6 @@ func (f *File) GetAttr(a *fuse.Attr) fuse.Status {
 func (f *File) Utimens(a *time.Time, m *time.Time) fuse.Status {
 	f.fdLock.RLock()
 	defer f.fdLock.RUnlock()
-	return f.loopbackFile.Utimens(a, m)
+	err := syscallcompat.FutimesNano(f.intFd(), a, m)
+	return fuse.ToStatus(err)
 }
diff --git a/internal/fusefrontend/fs.go b/internal/fusefrontend/fs.go
index 4c8af2d..096e663 100644
--- a/internal/fusefrontend/fs.go
+++ b/internal/fusefrontend/fs.go
@@ -372,10 +372,7 @@ func (fs *FS) Utimens(path string, a *time.Time, m *time.Time, context *fuse.Con
 		return fuse.ToStatus(err)
 	}
 	defer syscall.Close(dirfd)
-	ts := make([]unix.Timespec, 2)
-	ts[0] = unix.Timespec(fuse.UtimeToTimespec(a))
-	ts[1] = unix.Timespec(fuse.UtimeToTimespec(m))
-	err = unix.UtimesNanoAt(dirfd, cName, ts, unix.AT_SYMLINK_NOFOLLOW)
+	err = syscallcompat.UtimesNanoAtNofollow(dirfd, cName, a, m)
 	return fuse.ToStatus(err)
 }
 
diff --git a/internal/syscallcompat/sys_darwin.go b/internal/syscallcompat/sys_darwin.go
index ebda80f..699e38c 100644
--- a/internal/syscallcompat/sys_darwin.go
+++ b/internal/syscallcompat/sys_darwin.go
@@ -2,8 +2,11 @@ package syscallcompat
 
 import (
 	"log"
+	"path/filepath"
 	"runtime"
 	"syscall"
+	"time"
+	"unsafe"
 
 	"golang.org/x/sys/unix"
 
@@ -33,6 +36,24 @@ func pthread_setugid_np(uid uint32, gid uint32) (err error) {
 	return
 }
 
+// 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) {
+	_, _, e1 := syscall.Syscall6(syscall.SYS_FSETATTRLIST, uintptr(fd), uintptr(list), uintptr(buf), uintptr(size), uintptr(options), 0)
+	if e1 != 0 {
+		err = e1
+	}
+	return
+}
+
+// Setattrlist already has a syscall wrapper, but it is not exported.
+func setattrlist(path *byte, list unsafe.Pointer, buf unsafe.Pointer, size uintptr, options int) (err error) {
+	_, _, e1 := syscall.Syscall6(syscall.SYS_SETATTRLIST, uintptr(unsafe.Pointer(path)), uintptr(list), uintptr(buf), uintptr(size), uintptr(options), 0)
+	if e1 != 0 {
+		err = e1
+	}
+	return
+}
+
 // Sorry, fallocate is not available on OSX at all and
 // fcntl F_PREALLOCATE is not accessible from Go.
 // See https://github.com/rfjakob/gocryptfs/issues/18 if you want to help.
@@ -125,6 +146,72 @@ func MkdiratUser(dirfd int, path string, mode uint32, context *fuse.Context) (er
 	return Mkdirat(dirfd, path, mode)
 }
 
+type attrList struct {
+	bitmapCount uint16
+	_           uint16
+	CommonAttr  uint32
+	VolAttr     uint32
+	DirAttr     uint32
+	FileAttr    uint32
+	Forkattr    uint32
+}
+
+func timesToAttrList(a *time.Time, m *time.Time) (attrList attrList, attributes [2]unix.Timespec) {
+	attrList.bitmapCount = unix.ATTR_BIT_MAP_COUNT
+	attrList.CommonAttr = 0
+	i := 0
+	if m != nil {
+		attributes[i] = unix.Timespec(fuse.UtimeToTimespec(m))
+		attrList.CommonAttr |= unix.ATTR_CMN_MODTIME
+		i += 1
+	}
+	if a != nil {
+		attributes[i] = unix.Timespec(fuse.UtimeToTimespec(a))
+		attrList.CommonAttr |= unix.ATTR_CMN_ACCTIME
+		i += 1
+	}
+	return attrList, attributes
+}
+
+// FutimesNano syscall.
+func FutimesNano(fd int, a *time.Time, m *time.Time) (err error) {
+	attrList, attributes := timesToAttrList(a, m)
+	return fsetattrlist(fd, unsafe.Pointer(&attrList), unsafe.Pointer(&attributes),
+		unsafe.Sizeof(attributes), 0)
+}
+
+// UtimesNanoAtNofollow is like UtimesNanoAt but never follows symlinks.
+//
+// Unfortunately we cannot use unix.UtimesNanoAt since it is broken and just
+// ignores the provided 'dirfd'. In addition, it also lacks handling of 'nil'
+// pointers (used to preserve one of both timestamps).
+func UtimesNanoAtNofollow(dirfd int, path string, a *time.Time, m *time.Time) (err error) {
+	if !filepath.IsAbs(path) {
+		chdirMutex.Lock()
+		defer chdirMutex.Unlock()
+		var cwd int
+		cwd, err = syscall.Open(".", syscall.O_RDONLY, 0)
+		if err != nil {
+			return err
+		}
+		defer syscall.Close(cwd)
+		err = syscall.Fchdir(dirfd)
+		if err != nil {
+			return err
+		}
+		defer syscall.Fchdir(cwd)
+	}
+
+	_p0, err := syscall.BytePtrFromString(path)
+	if err != nil {
+		return err
+	}
+
+	attrList, attributes := timesToAttrList(a, m)
+	return setattrlist(_p0, unsafe.Pointer(&attrList), unsafe.Pointer(&attributes),
+		unsafe.Sizeof(attributes), unix.FSOPT_NOFOLLOW)
+}
+
 func Getdents(fd int) ([]fuse.DirEntry, error) {
 	return emulateGetdents(fd)
 }
diff --git a/internal/syscallcompat/sys_linux.go b/internal/syscallcompat/sys_linux.go
index 625e136..92d8183 100644
--- a/internal/syscallcompat/sys_linux.go
+++ b/internal/syscallcompat/sys_linux.go
@@ -6,6 +6,7 @@ import (
 	"runtime"
 	"sync"
 	"syscall"
+	"time"
 
 	"golang.org/x/sys/unix"
 
@@ -191,6 +192,28 @@ func MkdiratUser(dirfd int, path string, mode uint32, context *fuse.Context) (er
 	return Mkdirat(dirfd, path, mode)
 }
 
+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))
+	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.
+func UtimesNanoAtNofollow(dirfd int, path string, a *time.Time, m *time.Time) (err error) {
+	ts := timesToTimespec(a, m)
+	return unix.UtimesNanoAt(dirfd, path, ts, unix.AT_SYMLINK_NOFOLLOW)
+}
+
 // Getdents syscall.
 func Getdents(fd int) ([]fuse.DirEntry, error) {
 	return getdents(fd)
-- 
cgit v1.2.3