diff --git a/internal/fusefrontend/file.go b/internal/fusefrontend/file.go index 1ae3386..ffc41c6 100644 --- a/internal/fusefrontend/file.go +++ b/internal/fusefrontend/file.go @@ -122,9 +122,9 @@ func (f *File) createHeader() (fileID []byte, err error) { if !f.fs.args.NoPrealloc { err = syscallcompat.EnospcPrealloc(int(f.fd.Fd()), 0, contentenc.HeaderLen) if err != nil { - // If the underlying filesystem is full, it is normal get ENOSPC here. - // Log at Info level instead of Warning. - tlog.Info.Printf("ino%d: createHeader: prealloc failed: %s\n", f.qIno.Ino, err.Error()) + if !syscallcompat.IsENOSPC(err) { + tlog.Warn.Printf("ino%d: createHeader: prealloc failed: %s\n", f.qIno.Ino, err.Error()) + } return nil, err } } @@ -144,33 +144,41 @@ func (f *File) createHeader() (fileID []byte, err error) { // returns the requested part of the plaintext. // // Called by Read() for normal reading, -// by Write() and Truncate() for Read-Modify-Write -func (f *File) doRead(dst []byte, off uint64, length uint64) ([]byte, fuse.Status) { - // 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() - if err == io.EOF { - f.fileTableEntry.HeaderLock.Unlock() - 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(). +// 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() + if err == io.EOF { + f.fileTableEntry.HeaderLock.Unlock() + 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() + } } fileID := f.fileTableEntry.ID + if fileID == nil { + log.Panicf("filedID=%v, nolock=%v", fileID, nolock) + } // Read the backing ciphertext in one go blocks := f.contentEnc.ExplodePlainRange(off, length) alignedOffset, alignedLength := blocks[0].JointCiphertextRange(blocks) @@ -182,7 +190,9 @@ func (f *File) doRead(dst []byte, off uint64, length uint64) ([]byte, fuse.Statu 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. - f.fileTableEntry.HeaderLock.RUnlock() + 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) @@ -245,7 +255,7 @@ func (f *File) Read(buf []byte, off int64) (resultData fuse.ReadResult, code fus if f.fs.args.SerializeReads { serialize_reads.Wait(off, len(buf)) } - out, status := f.doRead(buf[:0], uint64(off), uint64(len(buf))) + out, status := f.doRead(buf[:0], uint64(off), uint64(len(buf)), false) if f.fs.args.SerializeReads { serialize_reads.Done() } @@ -266,6 +276,7 @@ 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 @@ -275,13 +286,22 @@ func (f *File) doWrite(data []byte, off int64) (uint32, fuse.Status) { // Write a new file header if the file is empty if err == io.EOF { tmpID, err = f.createHeader() + fileWasEmpty = true } if err != nil { f.fileTableEntry.HeaderLock.Unlock() return 0, fuse.ToStatus(err) } f.fileTableEntry.ID = tmpID - f.fileTableEntry.HeaderLock.Unlock() + 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() + } } // Handle payload data dataBuf := bytes.NewBuffer(data) @@ -292,7 +312,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()) + oldData, status := f.doRead(nil, b.BlockPlainOff(), f.contentEnc.PlainBS(), true) 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 @@ -315,9 +335,17 @@ func (f *File) doWrite(data []byte, off int64) (uint32, fuse.Status) { if !f.fs.args.NoPrealloc { err = syscallcompat.EnospcPrealloc(int(f.fd.Fd()), cOff, int64(len(ciphertext))) if err != nil { - // If the underlying filesystem is full, it is normal get ENOSPC here. - // Log at Info level instead of Warning. - tlog.Info.Printf("ino%d fh%d: doWrite: prealloc failed: %s", f.qIno.Ino, f.intFd(), err.Error()) + if !syscallcompat.IsENOSPC(err) { + tlog.Warn.Printf("ino%d fh%d: doWrite: prealloc failed: %v", f.qIno.Ino, f.intFd(), err) + } + if fileWasEmpty { + // Kill the file header again + f.fileTableEntry.ID = nil + err2 := syscall.Ftruncate(int(f.fd.Fd()), 0) + if err2 != nil { + tlog.Warn.Printf("ino%d fh%d: doWrite: rollback failed: %v", f.qIno.Ino, f.intFd(), err2) + } + } return 0, fuse.ToStatus(err) } } diff --git a/internal/fusefrontend/file_allocate_truncate.go b/internal/fusefrontend/file_allocate_truncate.go index 74c51a7..81ac8d3 100644 --- a/internal/fusefrontend/file_allocate_truncate.go +++ b/internal/fusefrontend/file_allocate_truncate.go @@ -144,7 +144,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) + data, status = f.doRead(nil, plainOff, lastBlockLen, false) if status != fuse.OK { tlog.Warn.Printf("Truncate: shrink doRead returned error: %v", err) return status diff --git a/internal/fusefrontend/file_holes.go b/internal/fusefrontend/file_holes.go index 04a00ec..3725f56 100644 --- a/internal/fusefrontend/file_holes.go +++ b/internal/fusefrontend/file_holes.go @@ -36,7 +36,6 @@ func (f *File) writePadHole(targetOff int64) fuse.Status { // will contain a file hole in the ciphertext. status := f.zeroPad(plainSize) if status != fuse.OK { - tlog.Warn.Printf("zeroPad returned error %v", status) return status } return fuse.OK diff --git a/internal/nametransform/diriv.go b/internal/nametransform/diriv.go index d67a5fa..1a72d2a 100644 --- a/internal/nametransform/diriv.go +++ b/internal/nametransform/diriv.go @@ -100,7 +100,10 @@ func WriteDirIV(dirfd *os.File, dir string) error { _, err = fd.Write(iv) if err != nil { fd.Close() - tlog.Warn.Printf("WriteDirIV: Write: %v", err) + // It is normal to get ENOSPC here + if !syscallcompat.IsENOSPC(err) { + tlog.Warn.Printf("WriteDirIV: Write: %v", err) + } // Delete incomplete gocryptfs.diriv file syscallcompat.Unlinkat(int(dirfd.Fd()), file, 0) return err diff --git a/internal/syscallcompat/helpers.go b/internal/syscallcompat/helpers.go new file mode 100644 index 0000000..e2a2215 --- /dev/null +++ b/internal/syscallcompat/helpers.go @@ -0,0 +1,21 @@ +package syscallcompat + +import ( + "os" + "syscall" +) + +// IsENOSPC tries to find out if "err" is a (potentially wrapped) ENOSPC error. +func IsENOSPC(err error) bool { + // syscallcompat.EnospcPrealloc returns the naked syscall error + if err == syscall.ENOSPC { + return true + } + // os.File.WriteAt returns &PathError + if err2, ok := err.(*os.PathError); ok { + if err2.Err == syscall.ENOSPC { + return true + } + } + return false +}