mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-02 16:44:16 +02:00
Fix #1045: crash on selecting an element with the online-installer Qt
Since #983, projectDataBase::newQuery() checked a query with sqlite3_prepare_v2() and sqlite3_stmt_readonly() on the handle of the QSQLITE driver. Those calls go to the libsqlite3 QElectroTech links. The QSQLITE plugin of the Qt online installer does not use that library: it carries its own copy of SQLite, so the handle belongs to another library and the call crashes. #1021 then put newQuery() on every element selection, which is where #1045 hits it. The check now runs the query with PRAGMA query_only set, through the driver. SQLite refuses a write itself, before touching a row, so the CTE prefix #983 closed ("WITH x AS (SELECT 1) DELETE FROM element") stays closed. A refused or failed query comes back empty, because several callers call exec() again on what newQuery() returns, after query_only is off. QElectroTech no longer calls the SQLite C API anywhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,7 +34,6 @@
|
||||
#include <QRegularExpression>
|
||||
#include <QSqlDriver>
|
||||
#include <QSqlError>
|
||||
#include <sqlite3.h>
|
||||
|
||||
|
||||
|
||||
@@ -205,10 +204,9 @@ bool projectDataBase::isReadOnlySelect(const QString &query, QString *error)
|
||||
@param query the SQL text to run -- must be a single read-only
|
||||
SELECT/WITH statement, see isReadOnlySelect()
|
||||
@param error set to a human-readable reason when the query was rejected
|
||||
before ever reaching the database
|
||||
@return a QSqlQuery with query as query and the internal database of
|
||||
this class as database to use, or an unexecuted, harmless QSqlQuery if
|
||||
the query was rejected
|
||||
or failed
|
||||
@return the executed query, on the internal database of this class, or
|
||||
an empty, harmless QSqlQuery if the query was rejected or failed
|
||||
*/
|
||||
QSqlQuery projectDataBase::newQuery(const QString &query, QString *error) {
|
||||
QString reason;
|
||||
@@ -226,24 +224,24 @@ QSqlQuery projectDataBase::newQuery(const QString &query, QString *error) {
|
||||
return QSqlQuery(m_data_base);
|
||||
}
|
||||
|
||||
// Second gate, and the one that actually enforces read-only: SQLite is
|
||||
// asked about the statement it compiled, instead of the text being read
|
||||
// for clues. The first gate cannot see through a CTE prefix --
|
||||
// "WITH x AS (SELECT 1) DELETE FROM element" starts with WITH, contains
|
||||
// no semicolon, and deletes every row. That matters beyond the
|
||||
// custom-query box, because this path is reachable from a file: a
|
||||
// <graphics_table>'s saved <query> is read straight out of the .qet by
|
||||
// ProjectDBModel::fromXml() and executed by fillValue(), so opening or
|
||||
// exporting a project someone else produced would have been enough.
|
||||
if (!QETSql::isSingleReadOnlyStatement(sqliteHandle(&m_data_base), query, &reason)) {
|
||||
// Second gate, and the one that actually enforces read-only: SQLite
|
||||
// runs the statement with query_only set and refuses a write itself,
|
||||
// instead of the text being read for clues. The first gate cannot see
|
||||
// through a CTE prefix -- "WITH x AS (SELECT 1) DELETE FROM element"
|
||||
// starts with WITH, contains no semicolon, and deletes every row. That
|
||||
// matters beyond the custom-query box, because this path is reachable
|
||||
// from a file: a <graphics_table>'s saved <query> is read straight out
|
||||
// of the .qet by ProjectDBModel::fromXml() and executed by fillValue(),
|
||||
// so opening or exporting a project someone else produced would have
|
||||
// been enough.
|
||||
QSqlQuery result = QETSql::execReadOnly(m_data_base, query, &reason);
|
||||
if (!reason.isEmpty()) {
|
||||
qWarning().noquote() << "projectDataBase::newQuery: rejected query:" << reason << "--" << query;
|
||||
if (error) {
|
||||
*error = reason;
|
||||
}
|
||||
return QSqlQuery(m_data_base);
|
||||
}
|
||||
|
||||
return QSqlQuery(query, m_data_base);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1301,23 +1299,6 @@ void projectDataBase::bindDiagramInfoValues(QSqlQuery &query, Diagram *diagram)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::sqliteHandle
|
||||
@param db
|
||||
@return the sqlite3 handler class used internally by db
|
||||
*/
|
||||
sqlite3 *projectDataBase::sqliteHandle(QSqlDatabase *db)
|
||||
{
|
||||
sqlite3 *handle = nullptr;
|
||||
|
||||
QVariant v = db->driver()->handle();
|
||||
if (v.isValid() && qstrcmp(v.typeName(), "sqlite3*") == 0) {
|
||||
handle = *static_cast<sqlite3 **>(v.data());
|
||||
}
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
#ifdef QET_EXPORT_PROJECT_DB
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,7 +29,6 @@ class QETProject;
|
||||
class Diagram;
|
||||
class Conductor;
|
||||
class Terminal;
|
||||
struct sqlite3;
|
||||
|
||||
/**
|
||||
@brief The projectDataBase class
|
||||
@@ -156,12 +155,6 @@ class projectDataBase : public QObject
|
||||
m_cascade_remove_conductor_query,
|
||||
m_cascade_remove_element_query;
|
||||
|
||||
public:
|
||||
// Deliberately outside the QET_EXPORT_PROJECT_DB guard below:
|
||||
// newQuery() needs the raw connection to ask SQLite whether a
|
||||
// query only reads, and that check runs in every build.
|
||||
static sqlite3 *sqliteHandle(QSqlDatabase *db);
|
||||
|
||||
#ifdef QET_EXPORT_PROJECT_DB
|
||||
public:
|
||||
static void exportDb(projectDataBase *db,
|
||||
|
||||
@@ -18,15 +18,14 @@
|
||||
#include "sqlreadonly.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
|
||||
#include <sqlite3.h>
|
||||
#include <QSqlError>
|
||||
|
||||
namespace QETSql {
|
||||
|
||||
/**
|
||||
@brief QETSql::isSingleReadOnlyStatement
|
||||
Ask SQLite itself whether @p query is exactly one statement, and whether
|
||||
that statement only reads.
|
||||
@brief QETSql::execReadOnly
|
||||
Run @p query on @p db with SQLite's query_only pragma set, so that
|
||||
SQLite itself refuses anything that would write.
|
||||
|
||||
Why SQLite is asked rather than the text inspected: a check on the
|
||||
query's first keyword cannot see what the statement actually does.
|
||||
@@ -37,98 +36,75 @@ namespace QETSql {
|
||||
WITH x AS (SELECT 1) DELETE FROM element
|
||||
@endcode
|
||||
|
||||
begins with WITH, contains no semicolon, and deletes every row.
|
||||
sqlite3_stmt_readonly() reports on the statement SQLite compiled, not
|
||||
on how it was spelled, so the same query is correctly refused here
|
||||
while an ordinary WITH ... SELECT still passes.
|
||||
begins with WITH, contains no semicolon, and deletes every row. With
|
||||
query_only set, SQLite fails that statement with SQLITE_READONLY when it
|
||||
tries to start writing, before any row is touched, while an ordinary
|
||||
WITH ... SELECT still runs.
|
||||
|
||||
The statement is compiled and immediately finalised; sqlite3_prepare_v2()
|
||||
does not run it, so nothing is executed to reach this verdict.
|
||||
Why a pragma rather than sqlite3_stmt_readonly(): that needs the
|
||||
driver's native sqlite3 handle passed to the libsqlite3 QElectroTech
|
||||
links. The QSQLITE plugin of the Qt online installer carries its own
|
||||
private copy of SQLite, so that handle belongs to another library and
|
||||
the call crashes (qelectrotech-source-mirror#1045). A pragma goes
|
||||
through the driver, whichever SQLite it uses.
|
||||
|
||||
This is a read-only test, NOT a statement-type allowlist: SQLite
|
||||
considers ATTACH, BEGIN and several PRAGMAs read-only too, because none
|
||||
of them change the contents of the database. Callers that need to
|
||||
restrict which *kind* of statement is acceptable must say so separately
|
||||
-- projectDataBase::newQuery() keeps isReadOnlySelect() in front of this
|
||||
A statement that succeeded here is read-only, so running the returned
|
||||
query again, as several callers do, runs a read-only statement again.
|
||||
A refused one comes back as an empty query with nothing to run again:
|
||||
query_only is only set for the duration of this call.
|
||||
|
||||
Qt's SQLite driver refuses a second statement after the first one, so
|
||||
"SELECT 1; DROP TABLE element" is refused too.
|
||||
|
||||
This is a read-only test, NOT a statement-type allowlist: query_only
|
||||
does not refuse ATTACH, BEGIN or most PRAGMAs, because none of them
|
||||
change the contents of the database. Callers that need to restrict
|
||||
which *kind* of statement is acceptable must say so separately --
|
||||
projectDataBase::newQuery() keeps isReadOnlySelect() in front of this
|
||||
for exactly that reason.
|
||||
|
||||
@param handle the connection the query would run on. A null handle is
|
||||
refused rather than waved through: without it there is nothing to ask,
|
||||
and guessing from the text is the weakness this exists to replace.
|
||||
@param db the connection to run the query on
|
||||
@param query the raw SQL text
|
||||
@param error set to a human-readable reason when this returns false
|
||||
@return true if @p query is a single, read-only statement
|
||||
@param error set to a human-readable reason when the query is refused
|
||||
@return the executed query, or an empty query on @p db if @p query was
|
||||
refused or failed
|
||||
*/
|
||||
bool isSingleReadOnlyStatement(sqlite3 *handle, const QString &query, QString *error)
|
||||
QSqlQuery execReadOnly(const QSqlDatabase &db, const QString &query, QString *error)
|
||||
{
|
||||
if (error) {
|
||||
error->clear();
|
||||
}
|
||||
|
||||
if (!handle) {
|
||||
if (!QSqlQuery(db).exec(QStringLiteral("PRAGMA query_only = ON"))) {
|
||||
if (error) {
|
||||
*error = QCoreApplication::translate("QETSql",
|
||||
"Impossible de vérifier la requête : "
|
||||
"aucune connexion SQLite disponible.");
|
||||
"la base de données ne peut pas être mise en lecture seule.");
|
||||
}
|
||||
return false;
|
||||
return QSqlQuery(db);
|
||||
}
|
||||
|
||||
const QByteArray utf8 = query.toUtf8();
|
||||
sqlite3_stmt *statement = nullptr;
|
||||
const char *tail = nullptr;
|
||||
QSqlQuery result(db);
|
||||
const bool ok = result.exec(query);
|
||||
QSqlQuery(db).exec(QStringLiteral("PRAGMA query_only = OFF"));
|
||||
|
||||
if (sqlite3_prepare_v2(handle, utf8.constData(), utf8.size(),
|
||||
&statement, &tail) != SQLITE_OK)
|
||||
{
|
||||
if (error) {
|
||||
*error = QCoreApplication::translate("QETSql",
|
||||
"Requête SQL invalide : %1")
|
||||
.arg(QString::fromUtf8(sqlite3_errmsg(handle)));
|
||||
}
|
||||
sqlite3_finalize(statement);
|
||||
return false;
|
||||
if (ok) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Whitespace or a bare comment compiles successfully to no statement
|
||||
// at all, and sqlite3_stmt_readonly() must not be handed that.
|
||||
if (!statement) {
|
||||
if (error) {
|
||||
*error = QCoreApplication::translate("QETSql",
|
||||
"La requête ne contient aucune instruction.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool read_only = sqlite3_stmt_readonly(statement) != 0;
|
||||
sqlite3_finalize(statement);
|
||||
|
||||
if (!read_only) {
|
||||
if (error) {
|
||||
if (error) {
|
||||
// SQLITE_READONLY is 8; extended codes keep it in the low byte.
|
||||
if ((result.lastError().nativeErrorCode().toInt() & 0xff) == 8) {
|
||||
*error = QCoreApplication::translate("QETSql",
|
||||
"Seules les requêtes en lecture seule sont autorisées : "
|
||||
"cette requête modifierait la base de données.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// tail points just past the first statement, semicolon included.
|
||||
// Anything left once semicolons and spacing are stripped is a second
|
||||
// statement -- caught structurally here, where "SELECT ';'" is a
|
||||
// perfectly ordinary query rather than a suspicious string.
|
||||
if (tail) {
|
||||
QString rest = QString::fromUtf8(tail);
|
||||
rest.remove(QLatin1Char(';'));
|
||||
if (!rest.trimmed().isEmpty()) {
|
||||
if (error) {
|
||||
*error = QCoreApplication::translate("QETSql",
|
||||
"Une seule requête est autorisée.");
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
*error = QCoreApplication::translate("QETSql",
|
||||
"Requête SQL invalide : %1")
|
||||
.arg(result.lastError().databaseText());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return QSqlQuery(db);
|
||||
}
|
||||
|
||||
} // namespace QETSql
|
||||
|
||||
@@ -18,26 +18,26 @@
|
||||
#ifndef SQLREADONLY_H
|
||||
#define SQLREADONLY_H
|
||||
|
||||
#include <QSqlDatabase>
|
||||
#include <QSqlQuery>
|
||||
#include <QString>
|
||||
|
||||
struct sqlite3;
|
||||
|
||||
/**
|
||||
Deciding whether a piece of SQL only reads.
|
||||
Running a piece of SQL only if it reads.
|
||||
|
||||
Deliberately its own translation unit, depending on nothing but QString
|
||||
and SQLite: it is the enforcement point for every query QElectroTech
|
||||
runs against a project database, including queries that arrive from
|
||||
outside the application (a .qet file's saved report/table query), so it
|
||||
is worth being able to test it in isolation -- see
|
||||
Deliberately its own translation unit, depending on nothing but Qt SQL:
|
||||
it is the enforcement point for every query QElectroTech runs against a
|
||||
project database, including queries that arrive from outside the
|
||||
application (a .qet file's saved report/table query), so it is worth
|
||||
being able to test it in isolation -- see
|
||||
tests/qttest/tst_sqlreadonly.cpp, which links this file and nothing
|
||||
else of QElectroTech.
|
||||
*/
|
||||
namespace QETSql {
|
||||
|
||||
bool isSingleReadOnlyStatement(sqlite3 *handle,
|
||||
const QString &query,
|
||||
QString *error = nullptr);
|
||||
QSqlQuery execReadOnly(const QSqlDatabase &db,
|
||||
const QString &query,
|
||||
QString *error = nullptr);
|
||||
}
|
||||
|
||||
#endif // SQLREADONLY_H
|
||||
|
||||
Reference in New Issue
Block a user