From 3062de6187990f9b4f669ecd9dffdd48ee0d778f Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Tue, 11 Jul 2017 23:19:58 +0200 Subject: [PATCH] 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 ++++++++++++++++++++++++++++++++++--- tests/defaults/main_test.go | 30 ++++++++++++++++++ 2 files changed, 89 insertions(+), 4 deletions(-) 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) { diff --git a/tests/defaults/main_test.go b/tests/defaults/main_test.go index ca11b43..5c6bb84 100644 --- a/tests/defaults/main_test.go +++ b/tests/defaults/main_test.go @@ -4,6 +4,7 @@ package defaults import ( "bytes" "io" + "io/ioutil" "os" "os/exec" "runtime" @@ -161,3 +162,32 @@ func TestXfs124(t *testing.T) { wg.Wait() } + +func TestWrite0200File(t *testing.T) { + fn := test_helpers.DefaultPlainDir + "/TestWrite0200File" + err := ioutil.WriteFile(fn, nil, 0200) + if err != nil { + t.Fatalf("creating empty file failed: %v", err) + } + fd, err := os.OpenFile(fn, os.O_WRONLY, 0) + if err != nil { + t.Fatal(err) + } + fi, err := fd.Stat() + if err != nil { + t.Fatal(err) + } + perms := fi.Mode().Perm() + if perms != 0200 { + t.Fatal("wrong initial permissions") + } + defer fd.Close() + _, err = fd.Write(make([]byte, 10)) + if err != nil { + t.Fatal(err) + } + perms = fi.Mode().Perm() + if perms != 0200 { + t.Fatal("wrong restored permissions") + } +}