From 474f268a8e04a4d2d66aa7e36b3b72cc1c1ef769 Mon Sep 17 00:00:00 2001 From: Marc Mutz Date: Mon, 01 Jun 2026 17:49:07 +0200 Subject: [PATCH] QDomNode: fix unbounded nesting depth on destruction and clear() In both ~QDomNodePrivate() and QDomNodePrivate::clear(), the code simply walked the list of chldren (->first, ->next), and deleted each one in turn. This recurses into ~QDomNodePrivate() and uses stack space proportional to the height of the tree. Since the latter is user-controlled, this is a an easy DoS, so fix the implementation to use iteration instead of recursion. Note that it suffices to fix ~QDomNodePrivate(), as clear() only recurses via the dtor, too. Found by yours truly, confirmed by Claude Opus 4.7. Fix vibe-coded with Opus 4.8. The code is well-commented, so I won't spill a lot of ink in the commit message, but the general idea is to perform a post-order depth-first-search where visitation means deleting. Since we visited all children before we visit their parent, we only ever delete empty nodes, which won't recurse. In particular, this doesn't change the order or deletion from that of the old code, when calculated as end of dtor (of course, since we remove recursion, the entering order of dtors changes). Add tests that exhaust the stack even on Linux (8MiB) before the fix and execute in less than 100ms after. While writing the test, found QTBUG-147190... :( The tests are tiny bit DRYer than I'd otherwise shoot for, since I plan to make the toByteArray() deep-nesting check use the same framework. Amends the start of the public history. Fixes: QTBUG-147191 Pick-to: 6.8 6.5 5.15 Change-Id: I154a1db71518b037c296fc5f993599f106296e81 Reviewed-by: Axel Spoerl Reviewed-by: Ivan Solovev (cherry picked from commit 647b221ca885739a69ece5695b7dcddcf3227558) Reviewed-by: Qt Cherry-pick Bot (cherry picked from commit ea71bb878603a0f1dd954d1a473c92967d6b2aec) --- diff --git a/src/xml/dom/qdom.cpp b/src/xml/dom/qdom.cpp index c58a1e3..9ff8da2 100644 --- a/src/xml/dom/qdom.cpp +++ b/src/xml/dom/qdom.cpp @@ -1086,16 +1086,21 @@ QDomNodePrivate::~QDomNodePrivate() { - QDomNodePrivate* p = first; - QDomNodePrivate* n; + QDomNodePrivate *p = this; - while (p) { - n = p->next; - if (!p->ref.deref()) - delete p; - else - p->setNoParent(); - p = n; + // post-order depth-first-search; visitation is deletion (avoids recursion) + while (true) { + if (QDomNodePrivate *c = p->first) { + p->first = c->next; // peel firstChild off p + if (c->ref.deref()) + c->setNoParent(); // survivor: detach, don't descend + else + p = c; // descend; c's parent() remembers p + } else { // p ran out of children (= is a leaf now) + if (p == this) + break; // we're done, don't `delete this` + delete std::exchange(p, p->parent()); // deletes and ascends + } } } diff --git a/tests/auto/xml/dom/qdom/tst_qdom.cpp b/tests/auto/xml/dom/qdom/tst_qdom.cpp index 2c62e6f..0c68221 100644 --- a/tests/auto/xml/dom/qdom/tst_qdom.cpp +++ b/tests/auto/xml/dom/qdom/tst_qdom.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -126,6 +127,8 @@ void testDomListComparison_data(); void testDomListComparison(); void noCrashOnDeepNesting() const; + void noCrashOnDeepNestingDtor() const { noCrashOnDeepNesting_impl(DeepNestingOp::Dtor); } + void noCrashOnDeepNestingClear() const { noCrashOnDeepNesting_impl(DeepNestingOp::Clear); } void cleanupTestCase() const; @@ -138,6 +141,12 @@ static QString onNullWarning(const char *const functionName); static bool isDeepEqual(const QDomNode &n1, const QDomNode &n2); static bool isFakeXMLDeclaration(const QDomNode &node); + enum class DeepNestingOp { + Dtor, + Clear, + }; + static constexpr size_t DeepNestingDepth = 250'000; + static void noCrashOnDeepNesting_impl(DeepNestingOp op); QList m_testCodecs; }; @@ -2716,6 +2725,25 @@ QT_TEST_EQUALITY_OPS(lhs, rhs, result); } +static QDomDocument makeNested(size_t depth) +{ + QDomDocument doc; + if (!depth) + return doc; + + // Need to build it bottom-up (QTBUG-147190)... + + QDomElement a = doc.createElement("a"); + for (size_t i = 1; i < depth; ++i) { + QDomElement parent = doc.createElement("a"); + parent.appendChild(std::exchange(a, parent)); + } + + doc.appendChild(a); // only now associate it with the doc (QTBUG-147190) + + return doc; +} + // The fix of QTBUG-131151 crash void tst_QDom::noCrashOnDeepNesting() const { @@ -2732,5 +2760,35 @@ file.close(); } +// QTBUG-146936: dtor +void tst_QDom::noCrashOnDeepNesting_impl(DeepNestingOp op) +{ + const auto print = [](const QElapsedTimer &timer, const char *operation) { + std::chrono::duration secs = timer.durationElapsed(); + qDebug("%s %llu-deep document in %fs", operation, + qulonglong(DeepNestingDepth), secs.count()); + }; + + QElapsedTimer timer; + timer.start(); + std::optional doc = makeNested(DeepNestingDepth); + print(timer, "created"); + + timer.restart(); + const char *operation; + // the actual test is that it doesn't run off the stack and crashes (QTBUG-146936): + switch (op) { + case DeepNestingOp::Dtor: + operation = "destroyed"; + doc.reset(); + break; + case DeepNestingOp::Clear: + operation = "cleared"; + doc->clear(); + break; + } + print(timer, operation); +} + QTEST_MAIN(tst_QDom) #include "tst_qdom.moc"