aboutsummaryrefslogtreecommitdiff
path: root/internal/fusefrontend
diff options
context:
space:
mode:
authorJakob Unterwurzacher2023-06-02 14:24:44 +0200
committerJakob Unterwurzacher2023-06-05 14:28:58 +0200
commit964f0c190932e5dc53b05ec69ccda6e8d33a73b6 (patch)
tree219f6c02123f0666c795b05e7f1c381690110c4c /internal/fusefrontend
parent3058b7978fd8dabd3e8565c9be816b1367bd196a (diff)
fusefrontend: sharedstorage: use byte-range lock on file header creation
Multiple hosts creating the same file at the same time could have overwritten each other's file header, leading to data corruption. Fix the race by placing a byte-range lock on the file when creating the file header.
Diffstat (limited to 'internal/fusefrontend')
-rw-r--r--internal/fusefrontend/file.go16
-rw-r--r--internal/fusefrontend/file_lock.go29
2 files changed, 45 insertions, 0 deletions
diff --git a/internal/fusefrontend/file.go b/internal/fusefrontend/file.go
index 8d0ba01..9bd05e6 100644
--- a/internal/fusefrontend/file.go
+++ b/internal/fusefrontend/file.go
@@ -14,6 +14,8 @@ import (
"sync"
"syscall"
+ "golang.org/x/sys/unix"
+
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
@@ -91,6 +93,13 @@ func (f *File) readFileID() ([]byte, error) {
// and not only the header. A header-only file will be considered empty.
// This makes File ID poisoning more difficult.
readLen := contentenc.HeaderLen + 1
+ if f.rootNode.args.SharedStorage {
+ // With -sharedstorage, we consider a header-only file as valid, because
+ // another gocryptfs process may have either:
+ // 1) just created the header, and not written further data yet.
+ // 2) truncated the file down to just the header.
+ readLen = contentenc.HeaderLen
+ }
buf := make([]byte, readLen)
n, err := f.fd.ReadAt(buf, 0)
if err != nil {
@@ -267,18 +276,25 @@ func (f *File) doWrite(data []byte, off int64) (uint32, syscall.Errno) {
//
// If the file ID is not cached, read it from disk
if f.fileTableEntry.ID == nil {
+ if err := f.LockSharedStorage(unix.F_WRLCK, 0, contentenc.HeaderLen); err != nil {
+ return 0, fs.ToErrno(err)
+ }
var err error
fileID, err := f.readFileID()
// Write a new file header if the file is empty
if err == io.EOF {
fileID, err = f.createHeader()
fileWasEmpty = true
+ // Having the unlock three times is ugly. But every other way I tried is even uglier.
+ f.LockSharedStorage(unix.F_UNLCK, 0, contentenc.HeaderLen)
} else if err != nil {
// Other errors mean readFileID() found a corrupt header
tlog.Warn.Printf("doWrite %d: corrupt header: %v", f.qIno.Ino, err)
+ f.LockSharedStorage(unix.F_UNLCK, 0, contentenc.HeaderLen)
return 0, syscall.EIO
}
if err != nil {
+ f.LockSharedStorage(unix.F_UNLCK, 0, contentenc.HeaderLen)
return 0, fs.ToErrno(err)
}
f.fileTableEntry.ID = fileID
diff --git a/internal/fusefrontend/file_lock.go b/internal/fusefrontend/file_lock.go
new file mode 100644
index 0000000..6f92cfe
--- /dev/null
+++ b/internal/fusefrontend/file_lock.go
@@ -0,0 +1,29 @@
+package fusefrontend
+
+import (
+ "golang.org/x/sys/unix"
+
+ "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat"
+)
+
+// SharedStorageLock conveniently wraps F_OFD_SETLKW
+// See https://man7.org/linux/man-pages/man2/fcntl.2.html -> "Open file description locks (non-POSIX)"
+//
+// lkType is one of:
+// * unix.F_RDLCK (shared read lock)
+// * unix.F_WRLCK (exclusive write lock)
+// * unix.F_UNLCK (unlock)
+//
+// This function is a no-op if args.SharedStorage == false.
+func (f *File) LockSharedStorage(lkType int16, lkStart int64, lkLen int64) error {
+ if !f.rootNode.args.SharedStorage {
+ return nil
+ }
+ lk := unix.Flock_t{
+ Type: lkType,
+ Whence: unix.SEEK_SET,
+ Start: lkStart,
+ Len: lkLen,
+ }
+ return unix.FcntlFlock(uintptr(f.intFd()), syscallcompat.F_OFD_SETLKW, &lk)
+}