mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-01-15 17:27:43 -05:00
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:
parent
6bce5836f9
commit
7ac292e09b
@ -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();
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
|
@ -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();
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user