Origin: upstream, commits
https://code.qt.io/cgit/qt/qtbase.git/commit/?id=
1a423ce4372d18a7
https://code.qt.io/cgit/qt/qtbase.git/commit/?id=
6326bec46a618c72
https://code.qt.io/cgit/qt/qtbase.git/commit/?id=
bdc8dc51380d2ce4
https://code.qt.io/cgit/qt/qtbase.git/commit/?id=
3bc3b8d69a291aa5
.
Based on KDE's backport:
https://invent.kde.org/qt/qt/qtbase/-/merge_requests/263
Last-Update: 2023-07-15
This fixes a crash while parsing an XML file with garbage data, the file
starts with '<' then garbage data:
- The loop in the parse() keeps iterating until it hits "case 262:",
which calls fastScanName()
- fastScanName() iterates over the text buffer scanning for the
attribute name (e.g. "xml:lang"), until it finds ':'
- Consider a Value val, fastScanName() is called on it, it would set
val.prefix to a number > val.len, then it would hit the 4096 condition
and return (returned 0, now it returns the equivalent of
std::null_opt), which means that val.len doesn't get modified, making
it smaller than val.prefix
- The code would try constructing an XmlStringRef with negative length,
which would hit an assert in one of QStringView's constructors
Add an assert to the XmlStringRef constructor.
Add unittest based on the file from the bug report.
Credit to OSS-Fuzz.
Gbp-Pq: Name CVE-2023-37369.diff
return n;
}
-inline int QXmlStreamReaderPrivate::fastScanName(int *prefix)
+// Fast scan an XML attribute name (e.g. "xml:lang").
+inline QXmlStreamReaderPrivate::FastScanNameResult
+QXmlStreamReaderPrivate::fastScanName(Value *val)
{
int n = 0;
uint c;
while ((c = getChar()) != StreamEOF) {
if (n >= 4096) {
// This is too long to be a sensible name, and
- // can exhaust memory
- return 0;
+ // can exhaust memory, or the range of decltype(*prefix)
+ raiseNamePrefixTooLongError();
+ return {};
}
switch (c) {
case '\n':
case '+':
case '*':
putChar(c);
- if (prefix && *prefix == n+1) {
- *prefix = 0;
+ if (val && val->prefix == n + 1) {
+ val->prefix = 0;
putChar(':');
--n;
}
- return n;
+ return FastScanNameResult(n);
case ':':
- if (prefix) {
- if (*prefix == 0) {
- *prefix = n+2;
+ if (val) {
+ if (val->prefix == 0) {
+ val->prefix = n + 2;
} else { // only one colon allowed according to the namespace spec.
putChar(c);
- return n;
+ return FastScanNameResult(n);
}
} else {
putChar(c);
- return n;
+ return FastScanNameResult(n);
}
Q_FALLTHROUGH();
default:
}
}
- if (prefix)
- *prefix = 0;
+ if (val)
+ val->prefix = 0;
int pos = textBuffer.size() - n;
putString(textBuffer, pos);
textBuffer.resize(pos);
- return 0;
+ return FastScanNameResult(0);
}
enum NameChar { NameBeginning, NameNotBeginning, NotName };
raiseError(QXmlStreamReader::NotWellFormedError, message);
}
+void QXmlStreamReaderPrivate::raiseNamePrefixTooLongError()
+{
+ // TODO: add a ImplementationLimitsExceededError and use it instead
+ raiseError(QXmlStreamReader::NotWellFormedError,
+ QXmlStream::tr("Length of XML attribute name exceeds implementation limits (4KiB "
+ "characters)."));
+}
+
void QXmlStreamReaderPrivate::parseError()
{
int fastScanLiteralContent();
int fastScanSpace();
int fastScanContentCharList();
- int fastScanName(int *prefix = nullptr);
+
+ struct FastScanNameResult {
+ FastScanNameResult() : ok(false) {}
+ explicit FastScanNameResult(int len) : addToLen(len), ok(true) { }
+ operator bool() { return ok; }
+ int operator*() { Q_ASSERT(ok); return addToLen; }
+ int addToLen;
+ bool ok;
+ };
+ FastScanNameResult fastScanName(Value *val = nullptr);
inline int fastScanNMTOKEN();
void raiseError(QXmlStreamReader::Error error, const QString& message = QString());
void raiseWellFormedError(const QString &message);
+ void raiseNamePrefixTooLongError();
QXmlStreamEntityResolver *entityResolver;
qname ::= LETTER;
/.
case $rule_number: {
- sym(1).len += fastScanName(&sym(1).prefix);
+ Value &val = sym(1);
+ if (auto res = fastScanName(&val))
+ val.len += *res;
+ else
+ return false;
+
if (atEnd) {
resume($rule_number);
return false;
name ::= LETTER;
/.
case $rule_number:
- sym(1).len += fastScanName();
+ if (auto res = fastScanName())
+ sym(1).len += *res;
+ else
+ return false;
+
if (atEnd) {
resume($rule_number);
return false;
int fastScanLiteralContent();
int fastScanSpace();
int fastScanContentCharList();
- int fastScanName(int *prefix = nullptr);
+
+ struct FastScanNameResult {
+ FastScanNameResult() : ok(false) {}
+ explicit FastScanNameResult(int len) : addToLen(len), ok(true) { }
+ operator bool() { return ok; }
+ int operator*() { Q_ASSERT(ok); return addToLen; }
+ int addToLen;
+ bool ok;
+ };
+ FastScanNameResult fastScanName(Value *val = nullptr);
inline int fastScanNMTOKEN();
void raiseError(QXmlStreamReader::Error error, const QString& message = QString());
void raiseWellFormedError(const QString &message);
+ void raiseNamePrefixTooLongError();
QXmlStreamEntityResolver *entityResolver;
break;
case 262: {
- sym(1).len += fastScanName(&sym(1).prefix);
+ Value &val = sym(1);
+ if (auto res = fastScanName(&val))
+ val.len += *res;
+ else
+ return false;
+
if (atEnd) {
resume(262);
return false;
} break;
case 263:
- sym(1).len += fastScanName();
+ if (auto res = fastScanName())
+ sym(1).len += *res;
+ else
+ return false;
+
if (atEnd) {
resume(263);
return false;
#include "qc14n.h"
+Q_DECLARE_METATYPE(QXmlStreamReader::Error)
Q_DECLARE_METATYPE(QXmlStreamReader::ReadElementTextBehaviour)
static const char *const catalogFile = "XML-Test-Suite/xmlconf/finalCatalog.xml";
void readBack() const;
void roundTrip() const;
void roundTrip_data() const;
+ void test_fastScanName_data() const;
+ void test_fastScanName() const;
void entityExpansionLimit() const;
QCOMPARE(out, in);
}
+void tst_QXmlStream::test_fastScanName_data() const
+{
+ QTest::addColumn<QByteArray>("data");
+ QTest::addColumn<QXmlStreamReader::Error>("errorType");
+
+ // 4096 is the limit in QXmlStreamReaderPrivate::fastScanName()
+
+ QByteArray arr = "<a:" + QByteArray("b").repeated(4096 - 1);
+ QTest::newRow("data1") << arr << QXmlStreamReader::PrematureEndOfDocumentError;
+
+ arr = "<a:" + QByteArray("b").repeated(4096);
+ QTest::newRow("data2") << arr << QXmlStreamReader::NotWellFormedError;
+
+ arr = "<" + QByteArray("a").repeated(4000) + ":" + QByteArray("b").repeated(96);
+ QTest::newRow("data3") << arr << QXmlStreamReader::PrematureEndOfDocumentError;
+
+ arr = "<" + QByteArray("a").repeated(4000) + ":" + QByteArray("b").repeated(96 + 1);
+ QTest::newRow("data4") << arr << QXmlStreamReader::NotWellFormedError;
+
+ arr = "<" + QByteArray("a").repeated(4000 + 1) + ":" + QByteArray("b").repeated(96);
+ QTest::newRow("data5") << arr << QXmlStreamReader::NotWellFormedError;
+}
+
+void tst_QXmlStream::test_fastScanName() const
+{
+ QFETCH(QByteArray, data);
+ QFETCH(QXmlStreamReader::Error, errorType);
+
+ QXmlStreamReader reader(data);
+ QXmlStreamReader::TokenType tokenType;
+ while (!reader.atEnd())
+ tokenType = reader.readNext();
+
+ QCOMPARE(tokenType, QXmlStreamReader::Invalid);
+ QCOMPARE(reader.error(), errorType);
+}
+
#include "tst_qxmlstream.moc"
// vim: et:ts=4:sw=4:sts=4