Skip to content

Stack Overflow via Unbounded Recursion in XMLNode Destructor #1083

@ahlien

Description

@ahlien

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:

  1. Programmatic API usage: Applications that build document trees via InsertEndChild() loops can create trees of arbitrary depth, bypassing the parse-time limit.
  2. 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

  1. 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.

  2. 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.

  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions