From 3062de6187990f9b4f669ecd9dffdd48ee0d778f Mon Sep 17 00:00:00 2001
From: Jakob Unterwurzacher
Date: Tue, 11 Jul 2017 23:19:58 +0200
Subject: fusefronted: enable writing to write-only files

Due to RMW, we always need read permissions on the backing file. This is a
problem if the file permissions do not allow reading (i.e. 0200 permissions).
This patch works around that problem by chmod'ing the file, obtaining a fd,
and chmod'ing it back.

Test included.

Issue reported at: https://github.com/rfjakob/gocryptfs/issues/125
---
 internal/fusefrontend/fs.go | 63 ++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 59 insertions(+), 4 deletions(-)

(limited to 'internal')

diff --git a/internal/fusefrontend/fs.go b/internal/fusefrontend/fs.go
index 16707d6..7a23710 100644
--- a/internal/fusefrontend/fs.go
+++ b/internal/fusefrontend/fs.go
@@ -34,6 +34,9 @@ type FS struct {
 	nameTransform *nametransform.NameTransform
 	// Content encryption helper
 	contentEnc *contentenc.ContentEnc
+	// This lock is used by openWriteOnlyFile() to block concurrent opens while
+	// it relaxes the permissions on a file.
+	openWriteOnlyLock sync.RWMutex
 }
 
 var _ pathfs.FileSystem = &FS{} // Verify that interface is implemented.
@@ -102,6 +105,10 @@ func (fs *FS) Open(path string, flags uint32, context *fuse.Context) (fuseFile n
 	if fs.isFiltered(path) {
 		return nil, fuse.EPERM
 	}
+	// Taking this lock makes sure we don't race openWriteOnlyFile()
+	fs.openWriteOnlyLock.RLock()
+	defer fs.openWriteOnlyLock.RUnlock()
+
 	newFlags := fs.mangleOpenFlags(flags)
 	cPath, err := fs.getBackingPath(path)
 	if err != nil {
@@ -109,20 +116,68 @@ func (fs *FS) Open(path string, flags uint32, context *fuse.Context) (fuseFile n
 		return nil, fuse.ToStatus(err)
 	}
 	tlog.Debug.Printf("Open: %s", cPath)
-	f, err := os.OpenFile(cPath, newFlags, 0666)
+	f, err := os.OpenFile(cPath, newFlags, 0)
 	if err != nil {
-		err2 := err.(*os.PathError)
-		if err2.Err == syscall.EMFILE {
+		sysErr := err.(*os.PathError).Err
+		if sysErr == syscall.EMFILE {
 			var lim syscall.Rlimit
 			syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim)
 			tlog.Warn.Printf("Open %q: too many open files. Current \"ulimit -n\": %d", cPath, lim.Cur)
 		}
+		if sysErr == syscall.EACCES && (int(flags)&os.O_WRONLY > 0) {
+			return fs.openWriteOnlyFile(cPath, newFlags)
+		}
 		return nil, fuse.ToStatus(err)
 	}
-
 	return NewFile(f, fs)
 }
 
+// Due to RMW, we always need read permissions on the backing file. This is a
+// problem if the file permissions do not allow reading (i.e. 0200 permissions).
+// This function works around that problem by chmod'ing the file, obtaining a fd,
+// and chmod'ing it back.
+func (fs *FS) openWriteOnlyFile(cPath string, newFlags int) (fuseFile nodefs.File, status fuse.Status) {
+	woFd, err := os.OpenFile(cPath, os.O_WRONLY, 0)
+	if err != nil {
+		return nil, fuse.ToStatus(err)
+	}
+	defer woFd.Close()
+	fi, err := woFd.Stat()
+	if err != nil {
+		return nil, fuse.ToStatus(err)
+	}
+	perms := fi.Mode().Perm()
+	// Verify that we don't have read permissions
+	if perms&0400 != 0 {
+		tlog.Warn.Printf("openWriteOnlyFile: unexpected permissions %#o, returning EPERM", perms)
+		return nil, fuse.ToStatus(syscall.EPERM)
+	}
+	// Upgrade the lock to block other Open()s and downgrade again on return
+	fs.openWriteOnlyLock.RUnlock()
+	fs.openWriteOnlyLock.Lock()
+	defer func() {
+		fs.openWriteOnlyLock.Unlock()
+		fs.openWriteOnlyLock.RLock()
+	}()
+	// Relax permissions and revert on return
+	err = woFd.Chmod(perms | 0400)
+	if err != nil {
+		tlog.Warn.Printf("openWriteOnlyFile: changing permissions failed: %v", err)
+		return nil, fuse.ToStatus(err)
+	}
+	defer func() {
+		err2 := woFd.Chmod(perms)
+		if err2 != nil {
+			tlog.Warn.Printf("openWriteOnlyFile: reverting permissions failed: %v", err2)
+		}
+	}()
+	rwFd, err := os.OpenFile(cPath, newFlags, 0)
+	if err != nil {
+		return nil, fuse.ToStatus(err)
+	}
+	return NewFile(rwFd, fs)
+}
+
 // Create implements pathfs.Filesystem.
 func (fs *FS) Create(path string, flags uint32, mode uint32, context *fuse.Context) (fuseFile nodefs.File, code fuse.Status) {
 	if fs.isFiltered(path) {
-- 
cgit v1.2.3