Skip to content

Commit 2598674

Browse files
committed
QTextMarkdownImporter: Fix heap-buffer-overflow
After finding the end marker `---`, the code expected more characters beyond: typically at least a trailing newline. But QStringView::sliced() crashes if asked for a substring that starts at or beyond the end. Now it's restructured into a separate splitFrontMatter() function, and we're stricter, tolerating only `---\n` or `---\r\n` as marker lines. So the code is easier to prove correct, and we don't need to check characters between the end of the marker and the end of the line (to allow inadvertent whitespace, for example). If the markers are not valid, the Markdown parser will see them as thematic breaks, as it would have done if we were not extracting the Front Matter beforehand. Amends e10c9b5 and bffddc6 Credit to OSS-Fuzz which found this as issue 42533775. [ChangeLog][QtGui][Text] Fixed a heap buffer overflow in QTextMarkdownImporter. The first marker for Front Matter must begin at the first character of a Markdown document, and both markers must be exactly ---\n or ---\r\n. Done-with: Marc Mutz <[email protected]> Fixes: QTBUG-135284 Pick-to: dev 6.9 6.8 Change-Id: I66412d21ecc0c4eabde443d70865ed2abad86d89 Reviewed-by: Marc Mutz <[email protected]>
1 parent b5e47fa commit 2598674

File tree

7 files changed

+84
-17
lines changed

7 files changed

+84
-17
lines changed

src/gui/text/qtextmarkdownimporter.cpp

+49-14
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ Q_STATIC_LOGGING_CATEGORY(lcMD, "qt.text.markdown")
2727
static const QChar qtmi_Newline = u'\n';
2828
static const QChar qtmi_Space = u' ';
2929

30-
static constexpr auto markerString() noexcept { return "---"_L1; }
30+
static constexpr auto lfMarkerString() noexcept { return "---\n"_L1; }
31+
static constexpr auto crlfMarkerString() noexcept { return "---r\n"_L1; }
3132

3233
// TODO maybe eliminate the margins after all views recognize BlockQuoteLevel, CSS can format it, etc.
3334
static const int qtmi_BlockQuoteIndent =
@@ -119,6 +120,47 @@ QTextMarkdownImporter::QTextMarkdownImporter(QTextDocument *doc, QTextDocument::
119120
{
120121
}
121122

123+
/*! \internal
124+
Split any Front Matter from the Markdown document \a md.
125+
Returns a pair of QStringViews: if \a md begins with qualifying Front Matter
126+
(according to the specification at https://jekyllrb.com/docs/front-matter/ ),
127+
put it into the \c frontMatter view, omitting both markers; and put the remaining
128+
Markdown into \c rest. If no Front Matter is found, return all of \a md in \c rest.
129+
*/
130+
static auto splitFrontMatter(QStringView md)
131+
{
132+
struct R {
133+
QStringView frontMatter, rest;
134+
explicit operator bool() const noexcept { return !frontMatter.isEmpty(); }
135+
};
136+
137+
const auto NotFound = R{{}, md};
138+
139+
/* Front Matter must start with '---\n' or '---\r\n' on the very first line,
140+
and Front Matter must end with another such line.
141+
If that is not the case, we return NotFound: then the whole document is
142+
to be passed on to the Markdown parser, in which '---\n' is interpreted
143+
as a "thematic break" (like <hr/> in HTML). */
144+
QLatin1StringView marker;
145+
if (md.startsWith(lfMarkerString()))
146+
marker = lfMarkerString();
147+
else if (md.startsWith(crlfMarkerString()))
148+
marker = crlfMarkerString();
149+
else
150+
return NotFound;
151+
152+
const auto frontMatterStart = marker.size();
153+
const auto endMarkerPos = md.indexOf(marker, frontMatterStart);
154+
155+
if (endMarkerPos < 0 || md[endMarkerPos - 1] != QChar::LineFeed)
156+
return NotFound;
157+
158+
Q_ASSERT(frontMatterStart < md.size());
159+
Q_ASSERT(endMarkerPos < md.size());
160+
const auto frontMatter = md.sliced(frontMatterStart, endMarkerPos - frontMatterStart);
161+
return R{frontMatter, md.sliced(endMarkerPos + marker.size())};
162+
}
163+
122164
void QTextMarkdownImporter::import(const QString &markdown)
123165
{
124166
MD_PARSER callbacks = {
@@ -143,21 +185,14 @@ void QTextMarkdownImporter::import(const QString &markdown)
143185
qCDebug(lcMD) << "default font" << defaultFont << "mono font" << m_monoFont;
144186
QStringView md = markdown;
145187

146-
if (m_features.testFlag(QTextMarkdownImporter::FeatureFrontMatter) && md.startsWith(markerString())) {
147-
qsizetype endMarkerPos = md.indexOf(markerString(), markerString().size() + 1);
148-
if (endMarkerPos > 4) {
149-
qsizetype firstLinePos = 4; // first line of yaml
150-
while (md.at(firstLinePos) == '\n'_L1 || md.at(firstLinePos) == '\r'_L1)
151-
++firstLinePos;
152-
auto frontMatter = md.sliced(firstLinePos, endMarkerPos - firstLinePos);
153-
firstLinePos = endMarkerPos + 4; // first line of markdown after yaml
154-
while (md.size() > firstLinePos && (md.at(firstLinePos) == '\n'_L1 || md.at(firstLinePos) == '\r'_L1))
155-
++firstLinePos;
156-
md = md.sliced(firstLinePos);
157-
doc->setMetaInformation(QTextDocument::FrontMatter, frontMatter.toString());
158-
qCDebug(lcMD) << "extracted FrontMatter: size" << frontMatter.size();
188+
if (m_features.testFlag(QTextMarkdownImporter::FeatureFrontMatter)) {
189+
if (const auto split = splitFrontMatter(md)) {
190+
doc->setMetaInformation(QTextDocument::FrontMatter, split.frontMatter.toString());
191+
qCDebug(lcMD) << "extracted FrontMatter: size" << split.frontMatter.size();
192+
md = split.rest;
159193
}
160194
}
195+
161196
const auto mdUtf8 = md.toUtf8();
162197
m_cursor.beginEditBlock();
163198
md_parse(mdUtf8.constData(), MD_SIZE(mdUtf8.size()), &callbacks, this);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
name: "Pluto"---
3+
Pluto may not be a planet. And this document does not contain Front Matter.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
name: "Sloppy"
3+
---
4+
This document has trailing whitespace after its second Front Matter marker.
5+
Therefore the marker does not qualify, and the document does not have Front Matter.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
name: "Aborted YAML"
3+
description: "The ending marker does not end with a newline, so it's invalid."
4+
---
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
--- ---
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
name: "Venus"
3+
discoverer: "Galileo Galilei"
4+
title: "A description of the planet Venus"
5+
keywords:
6+
- planets
7+
- solar system
8+
- astronomy
9+
---
10+
*Venus* is the second planet from the Sun, orbiting it every 224.7 Earth days.

tests/auto/gui/text/qtextmarkdownimporter/tst_qtextmarkdownimporter.cpp

+12-3
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,7 @@ void tst_QTextMarkdownImporter::pathological_data()
548548
QTest::addColumn<QString>("warning");
549549
QTest::newRow("fuzz20450") << "attempted to insert into a list that no longer exists";
550550
QTest::newRow("fuzz20580") << "";
551+
QTest::newRow("oss-fuzz-42533775") << ""; // caused a heap-buffer-overflow
551552
}
552553

553554
void tst_QTextMarkdownImporter::pathological() // avoid crashing on crazy input
@@ -644,15 +645,21 @@ void tst_QTextMarkdownImporter::fencedCodeBlocks()
644645
void tst_QTextMarkdownImporter::frontMatter_data()
645646
{
646647
QTest::addColumn<QString>("inputFile");
648+
QTest::addColumn<int>("expectedFrontMatterSize");
647649
QTest::addColumn<int>("expectedBlockCount");
648650

649-
QTest::newRow("yaml + markdown") << QFINDTESTDATA("data/yaml.md") << 1;
650-
QTest::newRow("yaml only") << QFINDTESTDATA("data/yaml-only.md") << 0;
651+
QTest::newRow("yaml + markdown") << QFINDTESTDATA("data/yaml.md") << 140 << 1;
652+
QTest::newRow("yaml + markdown with CRLFs") << QFINDTESTDATA("data/yaml-crlf.md") << 140 << 1;
653+
QTest::newRow("yaml only") << QFINDTESTDATA("data/yaml-only.md") << 59 << 0;
654+
QTest::newRow("malformed 1") << QFINDTESTDATA("data/front-marker-malformed1.md") << 0 << 1;
655+
QTest::newRow("malformed 2") << QFINDTESTDATA("data/front-marker-malformed2.md") << 0 << 2;
656+
QTest::newRow("malformed 3") << QFINDTESTDATA("data/front-marker-malformed3.md") << 0 << 1;
651657
}
652658

653659
void tst_QTextMarkdownImporter::frontMatter()
654660
{
655661
QFETCH(QString, inputFile);
662+
QFETCH(int, expectedFrontMatterSize);
656663
QFETCH(int, expectedBlockCount);
657664

658665
QFile f(inputFile);
@@ -672,7 +679,9 @@ void tst_QTextMarkdownImporter::frontMatter()
672679
++blockCount;
673680
}
674681
QCOMPARE(blockCount, expectedBlockCount); // yaml is not part of the markdown text
675-
QCOMPARE(doc.metaInformation(QTextDocument::FrontMatter), yaml); // without fences
682+
if (expectedFrontMatterSize)
683+
QCOMPARE(doc.metaInformation(QTextDocument::FrontMatter), yaml); // without fences
684+
QCOMPARE(doc.metaInformation(QTextDocument::FrontMatter).size(), expectedFrontMatterSize);
676685
}
677686

678687
void tst_QTextMarkdownImporter::toRawText_data()

0 commit comments

Comments
 (0)