Skip to content

Commit 3062de6

Browse files
committed
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: #125
1 parent 849ec10 commit 3062de6

File tree

2 files changed

+89
-4
lines changed

2 files changed

+89
-4
lines changed

internal/fusefrontend/fs.go

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ type FS struct {
3434
nameTransform *nametransform.NameTransform
3535
// Content encryption helper
3636
contentEnc *contentenc.ContentEnc
37+
// This lock is used by openWriteOnlyFile() to block concurrent opens while
38+
// it relaxes the permissions on a file.
39+
openWriteOnlyLock sync.RWMutex
3740
}
3841

3942
var _ pathfs.FileSystem = &FS{} // Verify that interface is implemented.
@@ -102,27 +105,79 @@ func (fs *FS) Open(path string, flags uint32, context *fuse.Context) (fuseFile n
102105
if fs.isFiltered(path) {
103106
return nil, fuse.EPERM
104107
}
108+
// Taking this lock makes sure we don't race openWriteOnlyFile()
109+
fs.openWriteOnlyLock.RLock()
110+
defer fs.openWriteOnlyLock.RUnlock()
111+
105112
newFlags := fs.mangleOpenFlags(flags)
106113
cPath, err := fs.getBackingPath(path)
107114
if err != nil {
108115
tlog.Debug.Printf("Open: getBackingPath: %v", err)
109116
return nil, fuse.ToStatus(err)
110117
}
111118
tlog.Debug.Printf("Open: %s", cPath)
112-
f, err := os.OpenFile(cPath, newFlags, 0666)
119+
f, err := os.OpenFile(cPath, newFlags, 0)
113120
if err != nil {
114-
err2 := err.(*os.PathError)
115-
if err2.Err == syscall.EMFILE {
121+
sysErr := err.(*os.PathError).Err
122+
if sysErr == syscall.EMFILE {
116123
var lim syscall.Rlimit
117124
syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim)
118125
tlog.Warn.Printf("Open %q: too many open files. Current \"ulimit -n\": %d", cPath, lim.Cur)
119126
}
127+
if sysErr == syscall.EACCES && (int(flags)&os.O_WRONLY > 0) {
128+
return fs.openWriteOnlyFile(cPath, newFlags)
129+
}
120130
return nil, fuse.ToStatus(err)
121131
}
122-
123132
return NewFile(f, fs)
124133
}
125134

135+
// Due to RMW, we always need read permissions on the backing file. This is a
136+
// problem if the file permissions do not allow reading (i.e. 0200 permissions).
137+
// This function works around that problem by chmod'ing the file, obtaining a fd,
138+
// and chmod'ing it back.
139+
func (fs *FS) openWriteOnlyFile(cPath string, newFlags int) (fuseFile nodefs.File, status fuse.Status) {
140+
woFd, err := os.OpenFile(cPath, os.O_WRONLY, 0)
141+
if err != nil {
142+
return nil, fuse.ToStatus(err)
143+
}
144+
defer woFd.Close()
145+
fi, err := woFd.Stat()
146+
if err != nil {
147+
return nil, fuse.ToStatus(err)
148+
}
149+
perms := fi.Mode().Perm()
150+
// Verify that we don't have read permissions
151+
if perms&0400 != 0 {
152+
tlog.Warn.Printf("openWriteOnlyFile: unexpected permissions %#o, returning EPERM", perms)
153+
return nil, fuse.ToStatus(syscall.EPERM)
154+
}
155+
// Upgrade the lock to block other Open()s and downgrade again on return
156+
fs.openWriteOnlyLock.RUnlock()
157+
fs.openWriteOnlyLock.Lock()
158+
defer func() {
159+
fs.openWriteOnlyLock.Unlock()
160+
fs.openWriteOnlyLock.RLock()
161+
}()
162+
// Relax permissions and revert on return
163+
err = woFd.Chmod(perms | 0400)
164+
if err != nil {
165+
tlog.Warn.Printf("openWriteOnlyFile: changing permissions failed: %v", err)
166+
return nil, fuse.ToStatus(err)
167+
}
168+
defer func() {
169+
err2 := woFd.Chmod(perms)
170+
if err2 != nil {
171+
tlog.Warn.Printf("openWriteOnlyFile: reverting permissions failed: %v", err2)
172+
}
173+
}()
174+
rwFd, err := os.OpenFile(cPath, newFlags, 0)
175+
if err != nil {
176+
return nil, fuse.ToStatus(err)
177+
}
178+
return NewFile(rwFd, fs)
179+
}
180+
126181
// Create implements pathfs.Filesystem.
127182
func (fs *FS) Create(path string, flags uint32, mode uint32, context *fuse.Context) (fuseFile nodefs.File, code fuse.Status) {
128183
if fs.isFiltered(path) {

tests/defaults/main_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package defaults
44
import (
55
"bytes"
66
"io"
7+
"io/ioutil"
78
"os"
89
"os/exec"
910
"runtime"
@@ -161,3 +162,32 @@ func TestXfs124(t *testing.T) {
161162

162163
wg.Wait()
163164
}
165+
166+
func TestWrite0200File(t *testing.T) {
167+
fn := test_helpers.DefaultPlainDir + "/TestWrite0200File"
168+
err := ioutil.WriteFile(fn, nil, 0200)
169+
if err != nil {
170+
t.Fatalf("creating empty file failed: %v", err)
171+
}
172+
fd, err := os.OpenFile(fn, os.O_WRONLY, 0)
173+
if err != nil {
174+
t.Fatal(err)
175+
}
176+
fi, err := fd.Stat()
177+
if err != nil {
178+
t.Fatal(err)
179+
}
180+
perms := fi.Mode().Perm()
181+
if perms != 0200 {
182+
t.Fatal("wrong initial permissions")
183+
}
184+
defer fd.Close()
185+
_, err = fd.Write(make([]byte, 10))
186+
if err != nil {
187+
t.Fatal(err)
188+
}
189+
perms = fi.Mode().Perm()
190+
if perms != 0200 {
191+
t.Fatal("wrong restored permissions")
192+
}
193+
}

0 commit comments

Comments
 (0)