From 19549c61590c1873468c53e0026a2fbffae428ef Mon Sep 17 00:00:00 2001
From: Daniel Garcia Moreno <daniel.garcia@suse.com>
Date: Fri, 10 Oct 2025 09:38:31 +0200
Subject: [PATCH] Add RelaxNG include limit

This patch adds a default xmlRelaxNGIncludeLimit of 1.000, and that
limit can be modified at runtime with the env variable
RNG_INCLUDE_LIMIT.

Fix https://gitlab.gnome.org/GNOME/libxml2/-/issues/998

CVE: CVE-2026-0989
Upstream-Status: Backport [https://gitlab.gnome.org/GNOME/libxml2/-/commit/19549c61590c1873468c53e0026a2fbffae428ef]
Signed-off-by: Peter Marko <peter.marko@siemens.com>
---
 include/libxml/relaxng.h                 |  4 ++
 relaxng.c                                | 63 ++++++++++++++++++++--
 runtest.c                                | 67 ++++++++++++++++++++++++
 test/relaxng/include/include-limit.rng   |  4 ++
 test/relaxng/include/include-limit_1.rng |  4 ++
 test/relaxng/include/include-limit_2.rng |  4 ++
 test/relaxng/include/include-limit_3.rng |  8 +++
 7 files changed, 150 insertions(+), 4 deletions(-)
 create mode 100644 test/relaxng/include/include-limit.rng
 create mode 100644 test/relaxng/include/include-limit_1.rng
 create mode 100644 test/relaxng/include/include-limit_2.rng
 create mode 100644 test/relaxng/include/include-limit_3.rng

diff --git a/include/libxml/relaxng.h b/include/libxml/relaxng.h
index eafc6604..099dacd8 100644
--- a/include/libxml/relaxng.h
+++ b/include/libxml/relaxng.h
@@ -139,6 +139,10 @@ XMLPUBFUN int
 		    xmlRelaxParserSetFlag	(xmlRelaxNGParserCtxtPtr ctxt,
 						 int flag);
 
+XMLPUBFUN int
+		    xmlRelaxParserSetIncLImit	(xmlRelaxNGParserCtxt *ctxt,
+						 int limit);
+
 XMLPUBFUN void
 		    xmlRelaxNGFreeParserCtxt	(xmlRelaxNGParserCtxtPtr ctxt);
 XMLPUBFUN void
diff --git a/relaxng.c b/relaxng.c
index 1d74ba9f..c0e94a3c 100644
--- a/relaxng.c
+++ b/relaxng.c
@@ -18,6 +18,8 @@
 
 #ifdef LIBXML_RELAXNG_ENABLED
 
+#include <errno.h>
+#include <stdlib.h>
 #include <string.h>
 #include <stdio.h>
 #include <stddef.h>
@@ -43,6 +45,12 @@
 static const xmlChar *xmlRelaxNGNs = (const xmlChar *)
     "http://relaxng.org/ns/structure/1.0";
 
+/*
+ * Default include limit, this can be override with RNG_INCLUDE_LIMIT
+ * env variable
+ */
+static const int _xmlRelaxNGIncludeLimit = 1000;
+
 #define IS_RELAXNG(node, typ)						\
    ((node != NULL) && (node->ns != NULL) &&				\
     (node->type == XML_ELEMENT_NODE) &&					\
@@ -219,6 +227,7 @@ struct _xmlRelaxNGParserCtxt {
     int incNr;                  /* Depth of the include parsing stack */
     int incMax;                 /* Max depth of the parsing stack */
     xmlRelaxNGIncludePtr *incTab;       /* array of incs */
+    int incLimit;               /* Include limit, to avoid stack-overflow on parse */
 
     int idref;                  /* requires idref checking */
 
@@ -1405,6 +1414,23 @@ xmlRelaxParserSetFlag(xmlRelaxNGParserCtxtPtr ctxt, int flags)
     return(0);
 }
 
+/**
+ * Semi private function used to set the include recursion limit to a
+ * parser context. Set to 0 to use the default value.
+ *
+ * @param ctxt  a RelaxNG parser context
+ * @param limit the new include depth limit
+ * @returns 0 if success and -1 in case of error
+ */
+int
+xmlRelaxParserSetIncLImit(xmlRelaxNGParserCtxt *ctxt, int limit)
+{
+    if (ctxt == NULL) return(-1);
+    if (limit < 0) return(-1);
+    ctxt->incLimit = limit;
+    return(0);
+}
+
 /************************************************************************
  *									*
  *			Document functions				*
@@ -1462,7 +1488,7 @@ xmlRelaxReadMemory(xmlRelaxNGParserCtxtPtr ctxt, const char *buf, int size) {
  *
  * Pushes a new include on top of the include stack
  *
- * Returns 0 in case of error, the index in the stack otherwise
+ * Returns -1 in case of error, the index in the stack otherwise
  */
 static int
 xmlRelaxNGIncludePush(xmlRelaxNGParserCtxtPtr ctxt,
@@ -1476,9 +1502,15 @@ xmlRelaxNGIncludePush(xmlRelaxNGParserCtxtPtr ctxt,
                                                sizeof(ctxt->incTab[0]));
         if (ctxt->incTab == NULL) {
             xmlRngPErrMemory(ctxt);
-            return (0);
+            return (-1);
         }
     }
+    if (ctxt->incNr >= ctxt->incLimit) {
+        xmlRngPErr(ctxt, (xmlNodePtr)value->doc, XML_RNGP_PARSE_ERROR,
+                   "xmlRelaxNG: inclusion recursion limit reached\n", NULL, NULL);
+        return(-1);
+    }
+
     if (ctxt->incNr >= ctxt->incMax) {
         ctxt->incMax *= 2;
         ctxt->incTab =
@@ -1487,7 +1519,7 @@ xmlRelaxNGIncludePush(xmlRelaxNGParserCtxtPtr ctxt,
                                                 sizeof(ctxt->incTab[0]));
         if (ctxt->incTab == NULL) {
             xmlRngPErrMemory(ctxt);
-            return (0);
+            return (-1);
         }
     }
     ctxt->incTab[ctxt->incNr] = value;
@@ -1657,7 +1689,9 @@ xmlRelaxNGLoadInclude(xmlRelaxNGParserCtxtPtr ctxt, const xmlChar * URL,
     /*
      * push it on the stack
      */
-    xmlRelaxNGIncludePush(ctxt, ret);
+    if (xmlRelaxNGIncludePush(ctxt, ret) < 0) {
+        return (NULL);
+    }
 
     /*
      * Some preprocessing of the document content, this include recursing
@@ -7381,11 +7415,32 @@ xmlRelaxNGParse(xmlRelaxNGParserCtxtPtr ctxt)
     xmlDocPtr doc;
     xmlNodePtr root;
 
+    const char *include_limit_env = getenv("RNG_INCLUDE_LIMIT");
+
     xmlRelaxNGInitTypes();
 
     if (ctxt == NULL)
         return (NULL);
 
+    if (ctxt->incLimit == 0) {
+        ctxt->incLimit = _xmlRelaxNGIncludeLimit;
+        if (include_limit_env != NULL) {
+            char *strEnd;
+            unsigned long val = 0;
+            errno = 0;
+            val = strtoul(include_limit_env, &strEnd, 10);
+            if (errno != 0 || *strEnd != 0 || val > INT_MAX) {
+                xmlRngPErr(ctxt, NULL, XML_RNGP_PARSE_ERROR,
+                           "xmlRelaxNGParse: invalid RNG_INCLUDE_LIMIT %s\n",
+                           (const xmlChar*)include_limit_env,
+                           NULL);
+                return(NULL);
+            }
+            if (val)
+                ctxt->incLimit = val;
+        }
+    }
+
     /*
      * First step is to parse the input document into an DOM/Infoset
      */
diff --git a/runtest.c b/runtest.c
index 49519aef..45109f0a 100644
--- a/runtest.c
+++ b/runtest.c
@@ -3832,6 +3832,70 @@ rngTest(const char *filename,
     return(ret);
 }
 
+/**
+ * Parse an RNG schemas with a custom RNG_INCLUDE_LIMIT
+ *
+ * @param filename  the schemas file
+ * @param result  the file with expected result
+ * @param err  the file with error messages
+ * @returns 0 in case of success, an error code otherwise
+ */
+static int
+rngIncludeTest(const char *filename,
+               const char *resul ATTRIBUTE_UNUSED,
+               const char *errr ATTRIBUTE_UNUSED,
+               int options ATTRIBUTE_UNUSED) {
+    xmlRelaxNGParserCtxtPtr ctxt;
+    xmlRelaxNGPtr schemas;
+    int ret = 0;
+
+    /* first compile the schemas if possible */
+    ctxt = xmlRelaxNGNewParserCtxt(filename);
+    xmlRelaxNGSetParserStructuredErrors(ctxt, testStructuredErrorHandler,
+                                        NULL);
+
+    /* Should work */
+    schemas = xmlRelaxNGParse(ctxt);
+    if (schemas == NULL) {
+        testErrorHandler(NULL, "Relax-NG schema %s failed to compile\n",
+                         filename);
+        ret = -1;
+        goto done;
+    }
+    xmlRelaxNGFree(schemas);
+    xmlRelaxNGFreeParserCtxt(ctxt);
+
+    ctxt = xmlRelaxNGNewParserCtxt(filename);
+    /* Should fail */
+    xmlRelaxParserSetIncLImit(ctxt, 2);
+    xmlRelaxNGSetParserStructuredErrors(ctxt, testStructuredErrorHandler,
+                                        NULL);
+    schemas = xmlRelaxNGParse(ctxt);
+    if (schemas != NULL) {
+        ret = -1;
+        xmlRelaxNGFree(schemas);
+    }
+    xmlRelaxNGFreeParserCtxt(ctxt);
+
+    ctxt = xmlRelaxNGNewParserCtxt(filename);
+    /* Should work */
+    xmlRelaxParserSetIncLImit(ctxt, 3);
+    xmlRelaxNGSetParserStructuredErrors(ctxt, testStructuredErrorHandler,
+                                        NULL);
+    schemas = xmlRelaxNGParse(ctxt);
+    if (schemas == NULL) {
+        testErrorHandler(NULL, "Relax-NG schema %s failed to compile\n",
+                         filename);
+        ret = -1;
+        goto done;
+    }
+    xmlRelaxNGFree(schemas);
+
+done:
+    xmlRelaxNGFreeParserCtxt(ctxt);
+    return(ret);
+}
+
 #ifdef LIBXML_READER_ENABLED
 /**
  * rngStreamTest:
@@ -5299,6 +5363,9 @@ testDesc testDescriptions[] = {
     { "Relax-NG regression tests" ,
       rngTest, "./test/relaxng/*.rng", NULL, NULL, NULL,
       XML_PARSE_DTDATTR | XML_PARSE_NOENT },
+    { "Relax-NG include limit tests" ,
+      rngIncludeTest, "./test/relaxng/include/include-limit.rng", NULL, NULL, NULL,
+      0 },
 #ifdef LIBXML_READER_ENABLED
     { "Relax-NG streaming regression tests" ,
       rngStreamTest, "./test/relaxng/*.rng", NULL, NULL, NULL,
diff --git a/test/relaxng/include/include-limit.rng b/test/relaxng/include/include-limit.rng
new file mode 100644
index 00000000..51f03942
--- /dev/null
+++ b/test/relaxng/include/include-limit.rng
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0">
+    <include href="include-limit_1.rng"/>
+</grammar>
diff --git a/test/relaxng/include/include-limit_1.rng b/test/relaxng/include/include-limit_1.rng
new file mode 100644
index 00000000..4672da38
--- /dev/null
+++ b/test/relaxng/include/include-limit_1.rng
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0">
+    <include href="include-limit_2.rng"/>
+</grammar>
diff --git a/test/relaxng/include/include-limit_2.rng b/test/relaxng/include/include-limit_2.rng
new file mode 100644
index 00000000..b35ecaa8
--- /dev/null
+++ b/test/relaxng/include/include-limit_2.rng
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0">
+    <include href="include-limit_3.rng"/>
+</grammar>
diff --git a/test/relaxng/include/include-limit_3.rng b/test/relaxng/include/include-limit_3.rng
new file mode 100644
index 00000000..86213c62
--- /dev/null
+++ b/test/relaxng/include/include-limit_3.rng
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0">
+    <start>
+        <element name="root">
+            <empty/>
+        </element>
+    </start>
+</grammar>
