fusefronted: disallow writes running concurrently with reads
As uncovered by xfstests generic/465, concurrent reads and writes could lead to this, doRead 3015532: corrupt block #1039: stupidgcm: message authentication failed, as the read could pick up a block that has not yet been completely written - write() is not atomic! Now writes take ContentLock exclusively, while reads take it shared, meaning that multiple reads can run in parallel with each other, but not with a write. This also simplifies the file header locking.
This commit is contained in:
parent
c70df522d2
commit
f316f1b2df
@ -145,39 +145,34 @@ func (f *File) createHeader() (fileID []byte, err error) {
|
||||
//
|
||||
// Called by Read() for normal reading,
|
||||
// by Write() and Truncate() via doWrite() for Read-Modify-Write.
|
||||
//
|
||||
// doWrite() uses nolock=true because it makes sure the ID is in the cache and
|
||||
// HeaderLock is locked before calling doRead.
|
||||
func (f *File) doRead(dst []byte, off uint64, length uint64, nolock bool) ([]byte, fuse.Status) {
|
||||
if !nolock {
|
||||
// Make sure we have the file ID.
|
||||
f.fileTableEntry.HeaderLock.RLock()
|
||||
if f.fileTableEntry.ID == nil {
|
||||
f.fileTableEntry.HeaderLock.RUnlock()
|
||||
// Yes, somebody else may take the lock before we can. This will get
|
||||
// the header read twice, but causes no harm otherwise.
|
||||
f.fileTableEntry.HeaderLock.Lock()
|
||||
tmpID, err := f.readFileID()
|
||||
func (f *File) doRead(dst []byte, off uint64, length uint64) ([]byte, fuse.Status) {
|
||||
// Get the file ID, either from the open file table, or from disk.
|
||||
var fileID []byte
|
||||
f.fileTableEntry.IDLock.Lock()
|
||||
if f.fileTableEntry.ID != nil {
|
||||
// Use the cached value in the file table
|
||||
fileID = f.fileTableEntry.ID
|
||||
} else {
|
||||
// Not cached, we have to read it from disk.
|
||||
var err error
|
||||
fileID, err = f.readFileID()
|
||||
if err != nil {
|
||||
f.fileTableEntry.IDLock.Unlock()
|
||||
if err == io.EOF {
|
||||
f.fileTableEntry.HeaderLock.Unlock()
|
||||
// Empty file
|
||||
return nil, fuse.OK
|
||||
}
|
||||
if err != nil {
|
||||
f.fileTableEntry.HeaderLock.Unlock()
|
||||
tlog.Warn.Printf("doRead %d: corrupt header: %v", f.qIno.Ino, err)
|
||||
return nil, fuse.EIO
|
||||
}
|
||||
f.fileTableEntry.ID = tmpID
|
||||
// Downgrade the lock.
|
||||
f.fileTableEntry.HeaderLock.Unlock()
|
||||
// The file ID may change in here. This does no harm because we
|
||||
// re-read it after the RLock().
|
||||
f.fileTableEntry.HeaderLock.RLock()
|
||||
}
|
||||
// Save into the file table
|
||||
f.fileTableEntry.ID = fileID
|
||||
}
|
||||
fileID := f.fileTableEntry.ID
|
||||
f.fileTableEntry.IDLock.Unlock()
|
||||
if fileID == nil {
|
||||
log.Panicf("filedID=%v, nolock=%v", fileID, nolock)
|
||||
log.Panicf("fileID=%v", fileID)
|
||||
}
|
||||
// Read the backing ciphertext in one go
|
||||
blocks := f.contentEnc.ExplodePlainRange(off, length)
|
||||
@ -189,10 +184,6 @@ func (f *File) doRead(dst []byte, off uint64, length uint64, nolock bool) ([]byt
|
||||
ciphertext := f.fs.contentEnc.CReqPool.Get()
|
||||
ciphertext = ciphertext[:int(alignedLength)]
|
||||
n, err := f.fd.ReadAt(ciphertext, int64(alignedOffset))
|
||||
// We don't care if the file ID changes after we have read the data. Drop the lock.
|
||||
if !nolock {
|
||||
f.fileTableEntry.HeaderLock.RUnlock()
|
||||
}
|
||||
if err != nil && err != io.EOF {
|
||||
tlog.Warn.Printf("read: ReadAt: %s", err.Error())
|
||||
return nil, fuse.ToStatus(err)
|
||||
@ -251,11 +242,14 @@ func (f *File) Read(buf []byte, off int64) (resultData fuse.ReadResult, code fus
|
||||
f.fdLock.RLock()
|
||||
defer f.fdLock.RUnlock()
|
||||
|
||||
f.fileTableEntry.ContentLock.RLock()
|
||||
defer f.fileTableEntry.ContentLock.RUnlock()
|
||||
|
||||
tlog.Debug.Printf("ino%d: FUSE Read: offset=%d length=%d", f.qIno.Ino, len(buf), off)
|
||||
if f.fs.args.SerializeReads {
|
||||
serialize_reads.Wait(off, len(buf))
|
||||
}
|
||||
out, status := f.doRead(buf[:0], uint64(off), uint64(len(buf)), false)
|
||||
out, status := f.doRead(buf[:0], uint64(off), uint64(len(buf)))
|
||||
if f.fs.args.SerializeReads {
|
||||
serialize_reads.Done()
|
||||
}
|
||||
@ -277,31 +271,25 @@ func (f *File) Read(buf []byte, off int64) (resultData fuse.ReadResult, code fus
|
||||
// Empty writes do nothing and are allowed.
|
||||
func (f *File) doWrite(data []byte, off int64) (uint32, fuse.Status) {
|
||||
fileWasEmpty := false
|
||||
// If the file ID is not cached, read it from disk
|
||||
if f.fileTableEntry.ID == nil {
|
||||
// Block other readers while we mess with the file header. Other writers
|
||||
// are blocked by ContentLock already.
|
||||
f.fileTableEntry.HeaderLock.Lock()
|
||||
tmpID, err := f.readFileID()
|
||||
// Get the file ID, create a new one if it does not exist yet.
|
||||
var fileID []byte
|
||||
// The caller has exclusively locked ContentLock, which blocks all other
|
||||
// readers and writers. No need to take IDLock.
|
||||
if f.fileTableEntry.ID != nil {
|
||||
fileID = f.fileTableEntry.ID
|
||||
} else {
|
||||
// If the file ID is not cached, read it from disk
|
||||
var err error
|
||||
fileID, err = f.readFileID()
|
||||
// Write a new file header if the file is empty
|
||||
if err == io.EOF {
|
||||
tmpID, err = f.createHeader()
|
||||
fileID, err = f.createHeader()
|
||||
fileWasEmpty = true
|
||||
}
|
||||
if err != nil {
|
||||
f.fileTableEntry.HeaderLock.Unlock()
|
||||
return 0, fuse.ToStatus(err)
|
||||
}
|
||||
f.fileTableEntry.ID = tmpID
|
||||
if fileWasEmpty {
|
||||
// The file was empty and we wrote a new file header. Keep the lock
|
||||
// as we might have to kill the file header again if the data write
|
||||
// fails.
|
||||
defer f.fileTableEntry.HeaderLock.Unlock()
|
||||
} else {
|
||||
// We won't touch the header again, drop the lock immediately.
|
||||
f.fileTableEntry.HeaderLock.Unlock()
|
||||
}
|
||||
f.fileTableEntry.ID = fileID
|
||||
}
|
||||
// Handle payload data
|
||||
dataBuf := bytes.NewBuffer(data)
|
||||
@ -312,7 +300,7 @@ func (f *File) doWrite(data []byte, off int64) (uint32, fuse.Status) {
|
||||
// Incomplete block -> Read-Modify-Write
|
||||
if b.IsPartial() {
|
||||
// Read
|
||||
oldData, status := f.doRead(nil, b.BlockPlainOff(), f.contentEnc.PlainBS(), true)
|
||||
oldData, status := f.doRead(nil, b.BlockPlainOff(), f.contentEnc.PlainBS())
|
||||
if status != fuse.OK {
|
||||
tlog.Warn.Printf("ino%d fh%d: RMW read failed: %s", f.qIno.Ino, f.intFd(), status.String())
|
||||
return 0, status
|
||||
@ -382,9 +370,7 @@ func (f *File) Write(data []byte, off int64) (uint32, fuse.Status) {
|
||||
f.fdLock.RLock()
|
||||
defer f.fdLock.RUnlock()
|
||||
if f.released {
|
||||
// The file descriptor has been closed concurrently, which also means
|
||||
// the wlock has been freed. Exit here so we don't crash trying to access
|
||||
// it.
|
||||
// The file descriptor has been closed concurrently
|
||||
tlog.Warn.Printf("ino%d fh%d: Write on released file", f.qIno.Ino, f.intFd())
|
||||
return 0, fuse.EBADF
|
||||
}
|
||||
|
@ -111,9 +111,7 @@ func (f *File) Truncate(newSize uint64) fuse.Status {
|
||||
return fuse.ToStatus(err)
|
||||
}
|
||||
// Truncate to zero kills the file header
|
||||
f.fileTableEntry.HeaderLock.Lock()
|
||||
f.fileTableEntry.ID = nil
|
||||
f.fileTableEntry.HeaderLock.Unlock()
|
||||
return fuse.OK
|
||||
}
|
||||
// We need the old file size to determine if we are growing or shrinking
|
||||
@ -144,7 +142,7 @@ func (f *File) Truncate(newSize uint64) fuse.Status {
|
||||
var data []byte
|
||||
if lastBlockLen > 0 {
|
||||
var status fuse.Status
|
||||
data, status = f.doRead(nil, plainOff, lastBlockLen, false)
|
||||
data, status = f.doRead(nil, plainOff, lastBlockLen)
|
||||
if status != fuse.OK {
|
||||
tlog.Warn.Printf("Truncate: shrink doRead returned error: %v", err)
|
||||
return status
|
||||
@ -206,8 +204,6 @@ func (f *File) truncateGrowFile(oldPlainSz uint64, newPlainSz uint64) fuse.Statu
|
||||
if newPlainSz%f.contentEnc.PlainBS() == 0 {
|
||||
// The file was empty, so it did not have a header. Create one.
|
||||
if oldPlainSz == 0 {
|
||||
f.fileTableEntry.HeaderLock.Lock()
|
||||
defer f.fileTableEntry.HeaderLock.Unlock()
|
||||
id, err := f.createHeader()
|
||||
if err != nil {
|
||||
return fuse.ToStatus(err)
|
||||
|
@ -57,18 +57,16 @@ type table struct {
|
||||
|
||||
// Entry is an entry in the open file table
|
||||
type Entry struct {
|
||||
// Reference count
|
||||
// Reference count. Protected by the table lock.
|
||||
refCount int
|
||||
// ContentLock guards the file content from concurrent writes. Every writer
|
||||
// ContentLock protects on-disk content from concurrent writes. Every writer
|
||||
// must take this lock before modifying the file content.
|
||||
ContentLock countingMutex
|
||||
// HeaderLock guards the file ID (in this struct) and the file header (on
|
||||
// disk). Take HeaderLock.RLock() to make sure the file ID does not change
|
||||
// behind your back. If you modify the file ID, you must take
|
||||
// HeaderLock.Lock().
|
||||
HeaderLock sync.RWMutex
|
||||
// ID is the file ID in the file header.
|
||||
ID []byte
|
||||
// IDLock must be taken before reading or writing the ID field in this struct,
|
||||
// unless you have an exclusive lock on ContentLock.
|
||||
IDLock sync.Mutex
|
||||
}
|
||||
|
||||
// Register creates an open file table entry for "qi" (or incrementes the
|
||||
@ -101,15 +99,15 @@ func Unregister(qi QIno) {
|
||||
|
||||
// countingMutex incrementes t.writeLockCount on each Lock() call.
|
||||
type countingMutex struct {
|
||||
sync.Mutex
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func (c *countingMutex) Lock() {
|
||||
c.Mutex.Lock()
|
||||
c.RWMutex.Lock()
|
||||
atomic.AddUint64(&t.writeOpCount, 1)
|
||||
}
|
||||
|
||||
// WriteOpCount returns the write lock counter value. This value is encremented
|
||||
// WriteOpCount returns the write lock counter value. This value is incremented
|
||||
// each time writeLock.Lock() on a file table entry is called.
|
||||
func WriteOpCount() uint64 {
|
||||
return atomic.LoadUint64(&t.writeOpCount)
|
||||
|
Loading…
Reference in New Issue
Block a user