Fix crashes on database save

* Add saving mutex to database class to prevent re-entrant saving
* Prevent saving multiple times to the same file if the database is not marked as modified
* Prevent locking the database while saving. This also prevents closing the application and database tab while saving.
* FileWatcher: only perform async checksum calculations when triggered by timer (prevents random GUI freezes)
* Re-attempt database lock when requested during save operation
* Prevent database tabs from closing before all databases are locked on quit
This commit is contained in:
Jonathan White 2020-03-05 22:59:07 -05:00
parent 6bce5836f9
commit 7ac292e09b
12 changed files with 122 additions and 40 deletions

View File

@ -121,6 +121,7 @@ int Create::execute(const QStringList& arguments)
QSharedPointer<Database> db(new Database); QSharedPointer<Database> db(new Database);
db->setKey(key); db->setKey(key);
db->setInitialized(true);
if (decryptionTime != 0) { if (decryptionTime != 0) {
auto kdf = db->kdf(); auto kdf = db->kdf();

View File

@ -84,6 +84,7 @@ int Import::execute(const QStringList& arguments)
Database db; Database db;
db.setKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2)); db.setKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2));
db.setKey(key); db.setKey(key);
db.setInitialized(true);
if (!db.import(xmlExportPath, &errorMessage)) { if (!db.import(xmlExportPath, &errorMessage)) {
errorTextStream << QObject::tr("Unable to import XML database: %1").arg(errorMessage) << endl; errorTextStream << QObject::tr("Unable to import XML database: %1").arg(errorMessage) << endl;

View File

@ -18,6 +18,7 @@
#include "Database.h" #include "Database.h"
#include "core/AsyncTask.h"
#include "core/Clock.h" #include "core/Clock.h"
#include "core/FileWatcher.h" #include "core/FileWatcher.h"
#include "core/Group.h" #include "core/Group.h"
@ -159,6 +160,15 @@ bool Database::open(const QString& filePath, QSharedPointer<const CompositeKey>
return true; return true;
} }
bool Database::isSaving()
{
bool locked = m_saveMutex.tryLock();
if (locked) {
m_saveMutex.unlock();
}
return !locked;
}
/** /**
* Save the database to the current file path. It is an error to call this function * Save the database to the current file path. It is an error to call this function
* if no file path has been defined. * if no file path has been defined.
@ -201,6 +211,25 @@ bool Database::save(QString* error, bool atomic, bool backup)
*/ */
bool Database::saveAs(const QString& filePath, QString* error, bool atomic, bool backup) bool Database::saveAs(const QString& filePath, QString* error, bool atomic, bool backup)
{ {
// Disallow overlapping save operations
if (isSaving()) {
if (error) {
*error = tr("Database save is already in progress.");
}
return false;
}
// Never save an uninitialized database
if (!m_initialized) {
if (error) {
*error = tr("Could not save, database has not been initialized!");
}
return false;
}
// Prevent destructive operations while saving
QMutexLocker locker(&m_saveMutex);
if (filePath == m_data.filePath) { if (filePath == m_data.filePath) {
// Disallow saving to the same file if read-only // Disallow saving to the same file if read-only
if (m_data.isReadOnly) { if (m_data.isReadOnly) {
@ -226,7 +255,7 @@ bool Database::saveAs(const QString& filePath, QString* error, bool atomic, bool
m_fileWatcher->stop(); m_fileWatcher->stop();
auto& canonicalFilePath = QFileInfo::exists(filePath) ? QFileInfo(filePath).canonicalFilePath() : filePath; auto& canonicalFilePath = QFileInfo::exists(filePath) ? QFileInfo(filePath).canonicalFilePath() : filePath;
bool ok = performSave(canonicalFilePath, error, atomic, backup); bool ok = AsyncTask::runAndWaitForFuture([&] { return performSave(canonicalFilePath, error, atomic, backup); });
if (ok) { if (ok) {
markAsClean(); markAsClean();
setFilePath(filePath); setFilePath(filePath);
@ -389,6 +418,9 @@ bool Database::import(const QString& xmlExportPath, QString* error)
void Database::releaseData() void Database::releaseData()
{ {
// Prevent data release while saving
QMutexLocker locker(&m_saveMutex);
s_uuidMap.remove(m_uuid); s_uuidMap.remove(m_uuid);
m_uuid = QUuid(); m_uuid = QUuid();
@ -397,22 +429,20 @@ void Database::releaseData()
} }
m_data.clear(); m_data.clear();
m_metadata->clear();
if (m_rootGroup && m_rootGroup->parent() == this) { if (m_rootGroup && m_rootGroup->parent() == this) {
delete m_rootGroup; delete m_rootGroup;
} }
if (m_metadata) {
delete m_metadata; m_fileWatcher->stop();
}
if (m_fileWatcher) {
delete m_fileWatcher;
}
m_deletedObjects.clear(); m_deletedObjects.clear();
m_commonUsernames.clear(); m_commonUsernames.clear();
m_initialized = false; m_initialized = false;
m_modified = false; m_modified = false;
m_modifiedTimer.stop();
} }
/** /**

View File

@ -21,6 +21,7 @@
#include <QDateTime> #include <QDateTime>
#include <QHash> #include <QHash>
#include <QMutex>
#include <QPointer> #include <QPointer>
#include <QScopedPointer> #include <QScopedPointer>
#include <QTimer> #include <QTimer>
@ -85,6 +86,7 @@ public:
void setEmitModified(bool value); void setEmitModified(bool value);
bool isReadOnly() const; bool isReadOnly() const;
void setReadOnly(bool readOnly); void setReadOnly(bool readOnly);
bool isSaving();
QUuid uuid() const; QUuid uuid() const;
QString filePath() const; QString filePath() const;
@ -208,6 +210,7 @@ private:
QPointer<Group> m_rootGroup; QPointer<Group> m_rootGroup;
QList<DeletedObject> m_deletedObjects; QList<DeletedObject> m_deletedObjects;
QTimer m_modifiedTimer; QTimer m_modifiedTimer;
QMutex m_saveMutex;
QPointer<FileWatcher> m_fileWatcher; QPointer<FileWatcher> m_fileWatcher;
bool m_initialized = false; bool m_initialized = false;
bool m_modified = false; bool m_modified = false;

View File

@ -43,6 +43,11 @@ FileWatcher::FileWatcher(QObject* parent)
m_fileIgnoreDelayTimer.setSingleShot(true); m_fileIgnoreDelayTimer.setSingleShot(true);
} }
FileWatcher::~FileWatcher()
{
stop();
}
void FileWatcher::start(const QString& filePath, int checksumIntervalSeconds, int checksumSizeKibibytes) void FileWatcher::start(const QString& filePath, int checksumIntervalSeconds, int checksumSizeKibibytes)
{ {
stop(); stop();
@ -120,8 +125,7 @@ void FileWatcher::checkFileChanged()
// Prevent reentrance // Prevent reentrance
m_ignoreFileChange = true; m_ignoreFileChange = true;
// Only trigger the change notice if there is a checksum mismatch auto checksum = AsyncTask::runAndWaitForFuture([this]() -> QByteArray { return calculateChecksum(); });
auto checksum = calculateChecksum();
if (checksum != m_fileChecksum) { if (checksum != m_fileChecksum) {
m_fileChecksum = checksum; m_fileChecksum = checksum;
m_fileChangeDelayTimer.start(0); m_fileChangeDelayTimer.start(0);
@ -132,21 +136,19 @@ void FileWatcher::checkFileChanged()
QByteArray FileWatcher::calculateChecksum() QByteArray FileWatcher::calculateChecksum()
{ {
return AsyncTask::runAndWaitForFuture([this]() -> QByteArray { QFile file(m_filePath);
QFile file(m_filePath); if (file.open(QFile::ReadOnly)) {
if (file.open(QFile::ReadOnly)) { QCryptographicHash hash(QCryptographicHash::Sha256);
QCryptographicHash hash(QCryptographicHash::Sha256); if (m_fileChecksumSizeBytes > 0) {
if (m_fileChecksumSizeBytes > 0) { hash.addData(file.read(m_fileChecksumSizeBytes));
hash.addData(file.read(m_fileChecksumSizeBytes)); } else {
} else { hash.addData(&file);
hash.addData(&file);
}
return hash.result();
} }
// If we fail to open the file return the last known checksum, this return hash.result();
// prevents unnecessary merge requests on intermittent network shares }
return m_fileChecksum; // If we fail to open the file return the last known checksum, this
}); // prevents unnecessary merge requests on intermittent network shares
return m_fileChecksum;
} }
BulkFileWatcher::BulkFileWatcher(QObject* parent) BulkFileWatcher::BulkFileWatcher(QObject* parent)

View File

@ -29,6 +29,7 @@ class FileWatcher : public QObject
public: public:
explicit FileWatcher(QObject* parent = nullptr); explicit FileWatcher(QObject* parent = nullptr);
~FileWatcher() override;
void start(const QString& path, int checksumIntervalSeconds = 0, int checksumSizeKibibytes = -1); void start(const QString& path, int checksumIntervalSeconds = 0, int checksumSizeKibibytes = -1);
void stop(); void stop();

View File

@ -31,7 +31,13 @@ Metadata::Metadata(QObject* parent)
, m_customData(new CustomData(this)) , m_customData(new CustomData(this))
, m_updateDatetime(true) , m_updateDatetime(true)
{ {
m_data.generator = "KeePassXC"; init();
connect(m_customData, SIGNAL(customDataModified()), SIGNAL(metadataModified()));
}
void Metadata::init()
{
m_data.generator = QStringLiteral("KeePassXC");
m_data.maintenanceHistoryDays = 365; m_data.maintenanceHistoryDays = 365;
m_data.masterKeyChangeRec = -1; m_data.masterKeyChangeRec = -1;
m_data.masterKeyChangeForce = -1; m_data.masterKeyChangeForce = -1;
@ -52,8 +58,17 @@ Metadata::Metadata(QObject* parent)
m_entryTemplatesGroupChanged = now; m_entryTemplatesGroupChanged = now;
m_masterKeyChanged = now; m_masterKeyChanged = now;
m_settingsChanged = now; m_settingsChanged = now;
}
connect(m_customData, SIGNAL(customDataModified()), this, SIGNAL(metadataModified())); void Metadata::clear()
{
init();
m_customIcons.clear();
m_customIconCacheKeys.clear();
m_customIconScaledCacheKeys.clear();
m_customIconsOrder.clear();
m_customIconsHashes.clear();
m_customData->clear();
} }
template <class P, class V> bool Metadata::set(P& property, const V& value) template <class P, class V> bool Metadata::set(P& property, const V& value)

View File

@ -62,6 +62,9 @@ public:
bool protectNotes; bool protectNotes;
}; };
void init();
void clear();
QString generator() const; QString generator() const;
QString name() const; QString name() const;
QDateTime nameChanged() const; QDateTime nameChanged() const;

View File

@ -353,13 +353,17 @@ bool DatabaseTabWidget::closeDatabaseTab(DatabaseWidget* dbWidget)
*/ */
bool DatabaseTabWidget::closeAllDatabaseTabs() bool DatabaseTabWidget::closeAllDatabaseTabs()
{ {
while (count() > 0) { // Attempt to lock all databases first to prevent closing only a portion of tabs
if (!closeDatabaseTab(0)) { if (lockDatabases()) {
return false; while (count() > 0) {
if (!closeDatabaseTab(0)) {
return false;
}
} }
return true;
} }
return true; return false;
} }
bool DatabaseTabWidget::saveDatabase(int index) bool DatabaseTabWidget::saveDatabase(int index)
@ -597,15 +601,26 @@ DatabaseWidget* DatabaseTabWidget::currentDatabaseWidget()
return qobject_cast<DatabaseWidget*>(currentWidget()); return qobject_cast<DatabaseWidget*>(currentWidget());
} }
void DatabaseTabWidget::lockDatabases() /**
* Attempt to lock all open databases
*
* @return return true if all databases are locked
*/
bool DatabaseTabWidget::lockDatabases()
{ {
int numLocked = 0;
for (int i = 0, c = count(); i < c; ++i) { for (int i = 0, c = count(); i < c; ++i) {
auto dbWidget = databaseWidgetFromIndex(i); auto dbWidget = databaseWidgetFromIndex(i);
if (dbWidget->lock() && dbWidget->database()->filePath().isEmpty()) { if (dbWidget->lock()) {
// If we locked a database without a file close the tab ++numLocked;
closeDatabaseTab(dbWidget); if (dbWidget->database()->filePath().isEmpty()) {
// If we locked a database without a file close the tab
closeDatabaseTab(dbWidget);
}
} }
} }
return numLocked == count();
} }
/** /**

View File

@ -71,7 +71,7 @@ public slots:
void exportToCsv(); void exportToCsv();
void exportToHtml(); void exportToHtml();
void lockDatabases(); bool lockDatabases();
void closeDatabaseFromSender(); void closeDatabaseFromSender();
void unlockDatabaseInDialog(DatabaseWidget* dbWidget, DatabaseOpenDialog::Intent intent); void unlockDatabaseInDialog(DatabaseWidget* dbWidget, DatabaseOpenDialog::Intent intent);
void unlockDatabaseInDialog(DatabaseWidget* dbWidget, DatabaseOpenDialog::Intent intent, const QString& filePath); void unlockDatabaseInDialog(DatabaseWidget* dbWidget, DatabaseOpenDialog::Intent intent, const QString& filePath);

View File

@ -255,15 +255,15 @@ QSharedPointer<Database> DatabaseWidget::database() const
DatabaseWidget::Mode DatabaseWidget::currentMode() const DatabaseWidget::Mode DatabaseWidget::currentMode() const
{ {
if (currentWidget() == nullptr) { if (currentWidget() == nullptr) {
return DatabaseWidget::Mode::None; return Mode::None;
} else if (currentWidget() == m_mainWidget) { } else if (currentWidget() == m_mainWidget) {
return DatabaseWidget::Mode::ViewMode; return Mode::ViewMode;
} else if (currentWidget() == m_databaseOpenWidget || currentWidget() == m_keepass1OpenWidget) { } else if (currentWidget() == m_databaseOpenWidget || currentWidget() == m_keepass1OpenWidget) {
return DatabaseWidget::Mode::LockedMode; return Mode::LockedMode;
} else if (currentWidget() == m_csvImportWizard) { } else if (currentWidget() == m_csvImportWizard) {
return DatabaseWidget::Mode::ImportMode; return Mode::ImportMode;
} else { } else {
return DatabaseWidget::Mode::EditMode; return Mode::EditMode;
} }
} }
@ -272,6 +272,11 @@ bool DatabaseWidget::isLocked() const
return currentMode() == Mode::LockedMode; return currentMode() == Mode::LockedMode;
} }
bool DatabaseWidget::isSaving() const
{
return m_db->isSaving();
}
bool DatabaseWidget::isSearchActive() const bool DatabaseWidget::isSearchActive() const
{ {
return m_entryView->inSearchMode(); return m_entryView->inSearchMode();
@ -1380,6 +1385,12 @@ bool DatabaseWidget::lock()
return true; return true;
} }
// Don't try to lock the database while saving, this will cause a deadlock
if (m_db->isSaving()) {
QTimer::singleShot(200, this, SLOT(lock()));
return false;
}
emit databaseLockRequested(); emit databaseLockRequested();
clipboard()->clearCopiedText(); clipboard()->clearCopiedText();
@ -1660,7 +1671,6 @@ bool DatabaseWidget::save()
auto focusWidget = qApp->focusWidget(); auto focusWidget = qApp->focusWidget();
// TODO: Make this async
// Lock out interactions // Lock out interactions
m_entryView->setDisabled(true); m_entryView->setDisabled(true);
m_groupView->setDisabled(true); m_groupView->setDisabled(true);

View File

@ -80,6 +80,7 @@ public:
DatabaseWidget::Mode currentMode() const; DatabaseWidget::Mode currentMode() const;
bool isLocked() const; bool isLocked() const;
bool isSaving() const;
bool isSearchActive() const; bool isSearchActive() const;
bool isEntryEditActive() const; bool isEntryEditActive() const;
bool isGroupEditActive() const; bool isGroupEditActive() const;