From eef1f80f05ced10190c771058ab4dc5e644c1c80 Mon Sep 17 00:00:00 2001 From: AlmaLinux RelEng Bot Date: Mon, 31 Aug 2026 05:01:36 -0400 Subject: [PATCH] import UBI libxml2-2.9.13-14.el9_8.4 --- SOURCES/libxml2-2.9.13-CVE-2026-11979.patch | 117 ++ SOURCES/libxml2-2.9.13-CVE-2026-6653.patch | 1484 +++++++++++++++++++ SPECS/libxml2.spec | 14 +- 3 files changed, 1614 insertions(+), 1 deletion(-) create mode 100644 SOURCES/libxml2-2.9.13-CVE-2026-11979.patch create mode 100644 SOURCES/libxml2-2.9.13-CVE-2026-6653.patch diff --git a/SOURCES/libxml2-2.9.13-CVE-2026-11979.patch b/SOURCES/libxml2-2.9.13-CVE-2026-11979.patch new file mode 100644 index 0000000..bc03bf7 --- /dev/null +++ b/SOURCES/libxml2-2.9.13-CVE-2026-11979.patch @@ -0,0 +1,117 @@ +From e3b080bac15cbf867888ca246200135601cae4bc Mon Sep 17 00:00:00 2001 +From: Daniel Garcia Moreno +Date: Fri, 22 May 2026 12:21:20 +0200 +Subject: [PATCH] xmlcatalog: overflow check for large --shell commands + +Fix https://gitlab.gnome.org/GNOME/libxml2/-/work_items/1124 +--- + test/catalogs/test.sh | 58 +++++++++++++++++++++++++++++++++++++++++++ + xmlcatalog.c | 16 ++++++++++++ + 2 files changed, 74 insertions(+) + create mode 100755 test/catalogs/test.sh + +diff --git a/test/catalogs/test.sh b/test/catalogs/test.sh +new file mode 100755 +index 00000000..84e8b90a +--- /dev/null ++++ b/test/catalogs/test.sh +@@ -0,0 +1,58 @@ ++#!/bin/sh ++ ++echo "## Catalog regression tests" ++ ++if [ -n "$1" ]; then ++ xmlcatalog=$1 ++else ++ xmlcatalog=./xmlcatalog ++fi ++ ++exitcode=0 ++ ++# Test xmlcatalog --shell command line ++# Case 1: Really long argument (470 chars) ++input=""; for i in {1..470}; do input="${input}A"; done ++echo $input | $xmlcatalog --shell test/catalogs/dockbook.xml || exit 1 ++# Case 2: public + long argument ++input="public "; for i in {1..470}; do input="${input}A"; done ++echo $input | $xmlcatalog --shell test/catalogs/dockbook.xml || exit 1 ++# Case 3: public + lots of args ++input="public "; for i in {1..80}; do input="${input} x"; done ++echo $input | $xmlcatalog --shell test/catalogs/dockbook.xml || exit 1 ++ ++for i in test/catalogs/*.script ; do ++ name=$(basename $i .script) ++ xml="./test/catalogs/$name.xml" ++ ++ if [ -f $xml ] ; then ++ if [ ! -f result/catalogs/$name ] ; then ++ echo New test file $name ++ $xmlcatalog --shell $xml < $i 2>&1 > result/catalogs/$name ++ else ++ $xmlcatalog --shell $xml < $i 2>&1 > catalog.out ++ log=$(diff result/catalogs/$name catalog.out) ++ if [ -n "$log" ] ; then ++ echo $name result ++ echo "$log" ++ exitcode=1 ++ fi ++ rm catalog.out ++ fi ++ fi ++done ++ ++# Add and del operations on XML Catalogs ++ ++$xmlcatalog --create --noout mycatalog ++$xmlcatalog --noout --add public Pubid sysid mycatalog ++$xmlcatalog --noout --add public Pubid2 sysid2 mycatalog ++$xmlcatalog --noout --add public Pubid3 sysid3 mycatalog ++diff result/catalogs/mycatalog.full mycatalog ++$xmlcatalog --noout --del sysid mycatalog ++$xmlcatalog --noout --del sysid3 mycatalog ++$xmlcatalog --noout --del sysid2 mycatalog ++diff result/catalogs/mycatalog.empty mycatalog ++rm -f mycatalog ++ ++exit $exitcode +diff --git a/xmlcatalog.c b/xmlcatalog.c +index 7b6f3769..2299dc1c 100644 +--- a/xmlcatalog.c ++++ b/xmlcatalog.c +@@ -118,6 +118,12 @@ static void usershell(void) { + (*cur != '\n') && (*cur != '\r')) { + if (*cur == 0) + break; ++ /* Do not read beyond the command array capacity */ ++ if (i >= (int)sizeof(command) - 2) { ++ printf("Invalid command %s\n", cur); ++ i = 0; ++ break; ++ } + command[i++] = *cur++; + } + command[i] = 0; +@@ -135,6 +141,11 @@ static void usershell(void) { + while ((*cur != '\n') && (*cur != '\r') && (*cur != 0)) { + if (*cur == 0) + break; ++ if (i >= (int)sizeof(arg) - 2) { ++ printf("Invalid arg %s\n", arg); ++ i = 0; ++ break; ++ } + arg[i++] = *cur++; + } + arg[i] = 0; +@@ -147,6 +158,11 @@ static void usershell(void) { + cur = arg; + memset(argv, 0, sizeof(argv)); + while (*cur != 0) { ++ if (i >= (int)sizeof(argv) / (int)sizeof(char*)) { ++ printf("Too much arguments\n"); ++ break; ++ } ++ + while ((*cur == ' ') || (*cur == '\t')) cur++; + if (*cur == '\'') { + cur++; diff --git a/SOURCES/libxml2-2.9.13-CVE-2026-6653.patch b/SOURCES/libxml2-2.9.13-CVE-2026-6653.patch new file mode 100644 index 0000000..917bbc4 --- /dev/null +++ b/SOURCES/libxml2-2.9.13-CVE-2026-6653.patch @@ -0,0 +1,1484 @@ +From 3a7ff570858b488f805f15c854dd637249e706f2 Mon Sep 17 00:00:00 2001 +From: Nick Wellnhofer +Date: Mon, 19 Dec 2022 15:24:53 +0100 +Subject: [PATCH 1/6] entities: Add "flags" member to struct xmlEntity + +This will hold various flags and eventually replace the "checked" +member. +--- + entities.c | 10 +++++----- + include/libxml/entities.h | 1 + + 2 files changed, 6 insertions(+), 5 deletions(-) + +diff --git a/entities.c b/entities.c +index ec1b9a71..a351ab1c 100644 +--- a/entities.c ++++ b/entities.c +@@ -38,35 +38,35 @@ static xmlEntity xmlEntityLt = { + NULL, NULL, NULL, NULL, NULL, NULL, + BAD_CAST "<", BAD_CAST "<", 1, + XML_INTERNAL_PREDEFINED_ENTITY, +- NULL, NULL, NULL, NULL, 0, 1 ++ NULL, NULL, NULL, NULL, 0, 1, 0 + }; + static xmlEntity xmlEntityGt = { + NULL, XML_ENTITY_DECL, BAD_CAST "gt", + NULL, NULL, NULL, NULL, NULL, NULL, + BAD_CAST ">", BAD_CAST ">", 1, + XML_INTERNAL_PREDEFINED_ENTITY, +- NULL, NULL, NULL, NULL, 0, 1 ++ NULL, NULL, NULL, NULL, 0, 1, 0 + }; + static xmlEntity xmlEntityAmp = { + NULL, XML_ENTITY_DECL, BAD_CAST "amp", + NULL, NULL, NULL, NULL, NULL, NULL, + BAD_CAST "&", BAD_CAST "&", 1, + XML_INTERNAL_PREDEFINED_ENTITY, +- NULL, NULL, NULL, NULL, 0, 1 ++ NULL, NULL, NULL, NULL, 0, 1, 0 + }; + static xmlEntity xmlEntityQuot = { + NULL, XML_ENTITY_DECL, BAD_CAST "quot", + NULL, NULL, NULL, NULL, NULL, NULL, + BAD_CAST "\"", BAD_CAST "\"", 1, + XML_INTERNAL_PREDEFINED_ENTITY, +- NULL, NULL, NULL, NULL, 0, 1 ++ NULL, NULL, NULL, NULL, 0, 1, 0 + }; + static xmlEntity xmlEntityApos = { + NULL, XML_ENTITY_DECL, BAD_CAST "apos", + NULL, NULL, NULL, NULL, NULL, NULL, + BAD_CAST "'", BAD_CAST "'", 1, + XML_INTERNAL_PREDEFINED_ENTITY, +- NULL, NULL, NULL, NULL, 0, 1 ++ NULL, NULL, NULL, NULL, 0, 1, 0 + }; + + /** +diff --git a/include/libxml/entities.h b/include/libxml/entities.h +index 47b4573e..ee1ba341 100644 +--- a/include/libxml/entities.h ++++ b/include/libxml/entities.h +@@ -60,6 +60,7 @@ struct _xmlEntity { + /* this is also used to count entities + * references done from that entity + * and if it contains '<' */ ++ int flags; /* various flags */ + }; + + /* + +From bddc1005f41c37aa579527bd1a25d96814407847 Mon Sep 17 00:00:00 2001 +From: Nick Wellnhofer +Date: Mon, 19 Dec 2022 15:26:46 +0100 +Subject: [PATCH 2/6] entities: Add XML_ENT_PARSED flag + +To check whether an entity was already parsed, the code previously +tested whether "checked" was non-zero or "children" was non-null. The +"children" check could be unreliable because an empty entity also +results in an empty (NULL) node list. Use a separate flag to make this +check more reliable. +--- + include/private/entities.h | 18 ++++++++++++++++++ + parser.c | 12 ++++++------ + tree.c | 9 +++++++-- + 3 files changed, 31 insertions(+), 8 deletions(-) + create mode 100644 include/private/entities.h + +diff --git a/include/private/entities.h b/include/private/entities.h +new file mode 100644 +index 00000000..eb187e81 +--- /dev/null ++++ b/include/private/entities.h +@@ -0,0 +1,18 @@ ++#ifndef XML_ENTITIES_H_PRIVATE__ ++#define XML_ENTITIES_H_PRIVATE__ ++ ++#include ++#include ++ ++/* ++ * Entity flags ++ * ++ * XML_ENT_PARSED: The entity was parsed and `children` points to the ++ * content. ++ */ ++#define XML_ENT_PARSED (1<<0) ++ ++XML_HIDDEN xmlChar * ++xmlEncodeAttributeEntities(xmlDocPtr doc, const xmlChar *input); ++ ++#endif /* XML_ENTITIES_H_PRIVATE__ */ +diff --git a/parser.c b/parser.c +index 237715bc..cbd0b67d 100644 +--- a/parser.c ++++ b/parser.c +@@ -86,6 +86,7 @@ + + #include "buf.h" + #include "enc.h" ++#include "private/entities.h" + + struct _xmlStartTag { + const xmlChar *prefix; +@@ -7144,7 +7145,7 @@ xmlParseReference(xmlParserCtxtPtr ctxt) { + if (ent == NULL) return; + if (!ctxt->wellFormed) + return; +- was_checked = ent->checked; ++ was_checked = ent->flags & XML_ENT_PARSED; + + /* special case of predefined entities */ + if ((ent->name == NULL) || +@@ -7170,8 +7171,7 @@ xmlParseReference(xmlParserCtxtPtr ctxt) { + * far more secure as the parser will only process data coming from + * the document entity by default. + */ +- if (((ent->checked == 0) || +- ((ent->children == NULL) && (ctxt->options & XML_PARSE_NOENT))) && ++ if (((ent->flags & XML_ENT_PARSED) == 0) && + ((ent->etype != XML_EXTERNAL_GENERAL_PARSED_ENTITY) || + (ctxt->options & (XML_PARSE_NOENT | XML_PARSE_DTDVALID)))) { + unsigned long oldnbent = ctxt->nbentities, diff; +@@ -7211,6 +7211,7 @@ xmlParseReference(xmlParserCtxtPtr ctxt) { + "invalid entity type found\n", NULL); + } + ++ ent->flags |= XML_ENT_PARSED; + /* + * Store the number of entities needing parsing for this entity + * content and do checkings +@@ -7233,9 +7234,8 @@ xmlParseReference(xmlParserCtxtPtr ctxt) { + } + + if ((ret == XML_ERR_OK) && (list != NULL)) { +- if (((ent->etype == XML_INTERNAL_GENERAL_ENTITY) || +- (ent->etype == XML_EXTERNAL_GENERAL_PARSED_ENTITY))&& +- (ent->children == NULL)) { ++ if ((ent->etype == XML_INTERNAL_GENERAL_ENTITY) || ++ (ent->etype == XML_EXTERNAL_GENERAL_PARSED_ENTITY)) { + ent->children = list; + /* + * Prune it directly in the generated document +diff --git a/tree.c b/tree.c +index 72ec7cae..f9f7a465 100644 +--- a/tree.c ++++ b/tree.c +@@ -49,6 +49,7 @@ + #endif + + #include "buf.h" ++#include "private/entities.h" + #include "save.h" + + int __xmlRegisterCallbacks = 0; +@@ -1420,7 +1421,8 @@ xmlStringLenGetNodeList(const xmlDoc *doc, const xmlChar *value, int len) { + if (val != NULL) xmlFree(val); + goto out; + } +- else if ((ent != NULL) && (ent->children == NULL)) { ++ else if ((ent != NULL) && ++ ((ent->flags & XML_ENT_PARSED) == 0)) { + xmlNodePtr temp; + + /* Set to non-NULL value to avoid recursion. */ +@@ -1428,6 +1430,7 @@ xmlStringLenGetNodeList(const xmlDoc *doc, const xmlChar *value, int len) { + ent->children = xmlStringGetNodeList(doc, + (const xmlChar*)node->content); + ent->owner = 1; ++ ent->flags |= XML_ENT_PARSED; + temp = ent->children; + while (temp) { + temp->parent = (xmlNodePtr)ent; +@@ -1620,7 +1623,8 @@ xmlStringGetNodeList(const xmlDoc *doc, const xmlChar *value) { + if (val != NULL) xmlFree(val); + goto out; + } +- else if ((ent != NULL) && (ent->children == NULL)) { ++ else if ((ent != NULL) && ++ ((ent->flags & XML_ENT_PARSED) == 0)) { + xmlNodePtr temp; + + /* Set to non-NULL value to avoid recursion. */ +@@ -1628,6 +1632,7 @@ xmlStringGetNodeList(const xmlDoc *doc, const xmlChar *value) { + ent->children = xmlStringGetNodeList(doc, + (const xmlChar*)node->content); + ent->owner = 1; ++ ent->flags |= XML_ENT_PARSED; + temp = ent->children; + while (temp) { + temp->parent = (xmlNodePtr)ent; + +From 3e81f7fe1366b97971f810b90a48f0ac8bcb5d3c Mon Sep 17 00:00:00 2001 +From: Nick Wellnhofer +Date: Mon, 19 Dec 2022 15:59:49 +0100 +Subject: [PATCH 3/6] entities: Use flags to store '<' check results + +Instead of abusing the LSB of the "checked" member, store the result +of testing for occurrence of '<' character in "flags". + +Also use the flags in xmlParseStringEntityRef instead of rescanning +every time. +--- + include/private/entities.h | 2 ++ + parser.c | 68 +++++++++++++++++--------------------- + 2 files changed, 32 insertions(+), 38 deletions(-) + +diff --git a/include/private/entities.h b/include/private/entities.h +index eb187e81..88c9adc9 100644 +--- a/include/private/entities.h ++++ b/include/private/entities.h +@@ -11,6 +11,8 @@ + * content. + */ + #define XML_ENT_PARSED (1<<0) ++#define XML_ENT_CHECKED_LT (1<<1) ++#define XML_ENT_CONTAINS_LT (1<<2) + + XML_HIDDEN xmlChar * + xmlEncodeAttributeEntities(xmlDocPtr doc, const xmlChar *input); +diff --git a/parser.c b/parser.c +index cbd0b67d..2e565428 100644 +--- a/parser.c ++++ b/parser.c +@@ -157,7 +157,7 @@ xmlParserEntityCheck(xmlParserCtxtPtr ctxt, size_t size, + if ((ent != NULL) && (ent->etype != XML_INTERNAL_PREDEFINED_ENTITY) && + (ent->content != NULL) && (ent->checked == 0) && + (ctxt->errNo != XML_ERR_ENTITY_LOOP)) { +- unsigned long oldnbent = ctxt->nbentities, diff; ++ unsigned long oldnbent = ctxt->nbentities; + xmlChar *rep; + + ent->checked = 1; +@@ -170,13 +170,8 @@ xmlParserEntityCheck(xmlParserCtxtPtr ctxt, size_t size, + ent->content[0] = 0; + } + +- diff = ctxt->nbentities - oldnbent + 1; +- if (diff > INT_MAX / 2) +- diff = INT_MAX / 2; +- ent->checked = diff * 2; ++ ent->checked = ctxt->nbentities - oldnbent + 1; + if (rep != NULL) { +- if (xmlStrchr(rep, '<')) +- ent->checked |= 1; + xmlFree(rep); + rep = NULL; + } +@@ -243,7 +238,7 @@ xmlParserEntityCheck(xmlParserCtxtPtr ctxt, size_t size, + /* + * use the number of parsed entities in the replacement + */ +- size = ent->checked / 2; ++ size = ent->checked; + + /* + * The amount of data parsed counting entities size only once +@@ -2706,7 +2701,7 @@ xmlStringLenDecodeEntities(xmlParserCtxtPtr ctxt, const xmlChar *str, int len, + ent = xmlParseStringEntityRef(ctxt, &str); + xmlParserEntityCheck(ctxt, 0, ent, 0); + if (ent != NULL) +- ctxt->nbentities += ent->checked / 2; ++ ctxt->nbentities += ent->checked; + if ((ent != NULL) && + (ent->etype == XML_INTERNAL_PREDEFINED_ENTITY)) { + if (ent->content != NULL) { +@@ -2759,7 +2754,7 @@ xmlStringLenDecodeEntities(xmlParserCtxtPtr ctxt, const xmlChar *str, int len, + ent = xmlParseStringPEReference(ctxt, &str); + xmlParserEntityCheck(ctxt, 0, ent, 0); + if (ent != NULL) +- ctxt->nbentities += ent->checked / 2; ++ ctxt->nbentities += ent->checked; + if (ent != NULL) { + if (ent->content == NULL) { + /* +@@ -4057,20 +4052,15 @@ xmlParseAttValueComplex(xmlParserCtxtPtr ctxt, int *attlen, int normalize) { + */ + if ((ent->etype != XML_INTERNAL_PREDEFINED_ENTITY) && + (ent->content != NULL) && (ent->checked == 0)) { +- unsigned long oldnbent = ctxt->nbentities, diff; ++ unsigned long oldnbent = ctxt->nbentities; + + ++ctxt->depth; + rep = xmlStringDecodeEntities(ctxt, ent->content, + XML_SUBSTITUTE_REF, 0, 0, 0); + --ctxt->depth; + +- diff = ctxt->nbentities - oldnbent + 1; +- if (diff > INT_MAX / 2) +- diff = INT_MAX / 2; +- ent->checked = diff * 2; ++ ent->checked = ctxt->nbentities - oldnbent + 1; + if (rep != NULL) { +- if (xmlStrchr(rep, '<')) +- ent->checked |= 1; + xmlFree(rep); + rep = NULL; + } else { +@@ -7174,7 +7164,7 @@ xmlParseReference(xmlParserCtxtPtr ctxt) { + if (((ent->flags & XML_ENT_PARSED) == 0) && + ((ent->etype != XML_EXTERNAL_GENERAL_PARSED_ENTITY) || + (ctxt->options & (XML_PARSE_NOENT | XML_PARSE_DTDVALID)))) { +- unsigned long oldnbent = ctxt->nbentities, diff; ++ unsigned long oldnbent = ctxt->nbentities; + + /* + * This is a bit hackish but this seems the best +@@ -7216,12 +7206,7 @@ xmlParseReference(xmlParserCtxtPtr ctxt) { + * Store the number of entities needing parsing for this entity + * content and do checkings + */ +- diff = ctxt->nbentities - oldnbent + 1; +- if (diff > INT_MAX / 2) +- diff = INT_MAX / 2; +- ent->checked = diff * 2; +- if ((ent->content != NULL) && (xmlStrchr(ent->content, '<'))) +- ent->checked |= 1; ++ ent->checked = ctxt->nbentities - oldnbent + 1; + if (ret == XML_ERR_ENTITY_LOOP) { + xmlFatalErr(ctxt, XML_ERR_ENTITY_LOOP, NULL); + xmlHaltParser(ctxt); +@@ -7285,12 +7270,12 @@ xmlParseReference(xmlParserCtxtPtr ctxt) { + list = NULL; + } + if (ent->checked == 0) +- ent->checked = 2; ++ ent->checked = 1; + + /* Prevent entity from being parsed and expanded twice (Bug 760367). */ + was_checked = 0; +- } else if (ent->checked != 1) { +- ctxt->nbentities += ent->checked / 2; ++ } else { ++ ctxt->nbentities += ent->checked; + } + + /* +@@ -7654,13 +7639,16 @@ xmlParseEntityRef(xmlParserCtxtPtr ctxt) { + * not contain a <. + */ + else if ((ctxt->instate == XML_PARSER_ATTRIBUTE_VALUE) && +- (ent != NULL) && + (ent->etype != XML_INTERNAL_PREDEFINED_ENTITY)) { +- if (((ent->checked & 1) || (ent->checked == 0)) && +- (ent->content != NULL) && (xmlStrchr(ent->content, '<'))) { +- xmlFatalErrMsgStr(ctxt, XML_ERR_LT_IN_ATTRIBUTE, +- "'<' in entity '%s' is not allowed in attributes values\n", name); ++ if ((ent->flags & XML_ENT_CHECKED_LT) == 0) { ++ if ((ent->content != NULL) && (xmlStrchr(ent->content, '<'))) ++ ent->flags |= XML_ENT_CONTAINS_LT; ++ ent->flags |= XML_ENT_CHECKED_LT; + } ++ if (ent->flags & XML_ENT_CONTAINS_LT) ++ xmlFatalErrMsgStr(ctxt, XML_ERR_LT_IN_ATTRIBUTE, ++ "'<' in entity '%s' is not allowed in attributes " ++ "values\n", name); + } + + /* +@@ -7848,12 +7836,16 @@ xmlParseStringEntityRef(xmlParserCtxtPtr ctxt, const xmlChar ** str) { + * not contain a <. + */ + else if ((ctxt->instate == XML_PARSER_ATTRIBUTE_VALUE) && +- (ent != NULL) && (ent->content != NULL) && +- (ent->etype != XML_INTERNAL_PREDEFINED_ENTITY) && +- (xmlStrchr(ent->content, '<'))) { +- xmlFatalErrMsgStr(ctxt, XML_ERR_LT_IN_ATTRIBUTE, +- "'<' in entity '%s' is not allowed in attributes values\n", +- name); ++ (ent->etype != XML_INTERNAL_PREDEFINED_ENTITY)) { ++ if ((ent->flags & XML_ENT_CHECKED_LT) == 0) { ++ if ((ent->content != NULL) && (xmlStrchr(ent->content, '<'))) ++ ent->flags |= XML_ENT_CONTAINS_LT; ++ ent->flags |= XML_ENT_CHECKED_LT; ++ } ++ if (ent->flags & XML_ENT_CONTAINS_LT) ++ xmlFatalErrMsgStr(ctxt, XML_ERR_LT_IN_ATTRIBUTE, ++ "'<' in entity '%s' is not allowed in attributes " ++ "values\n", name); + } + + /* + +From ec6cb0ac37f139397c00c896dd1f8ce6bf45ddd5 Mon Sep 17 00:00:00 2001 +From: Nick Wellnhofer +Date: Mon, 19 Dec 2022 18:39:45 +0100 +Subject: [PATCH 4/6] entities: Rework entity amplification checks + +This commit implements robust detection of entity amplification attacks, +better known as the "billion laughs" attack. + +We now limit the size of the document after substitution of entities to +10 times the size before expansion. This guarantees linear behavior by +definition. There already was a similar check before, but the accounting +of "sizeentities" (size of external entities) and "sizeentcopy" (size of +all copies created by entity references) wasn't accurate. + +We also need saturation arithmetic since we're historically limited to +"unsigned long" which is 32-bit on many platforms. + +A maximum of 10 MB of substitutions is always allowed. This should make +use cases like DITA work which have caused problems in the past. + +The old checks based on the number of entities were removed. This is +accounted for by adding a fixed cost to each entity reference. + +Entity amplification checks are now enabled even if XML_PARSE_HUGE is +set. This option is mainly used to allow larger text nodes. Most users +were unaware that it also disabled entity expansion checks. + +Some of the limits might be adjusted later. If this change turns out to +affect legitimate use cases, we can add a separate parser option to +disable the checks. + +Fixes #294. +Fixes #345. +--- + SAX2.c | 14 ++ + entities.c | 10 +- + include/libxml/entities.h | 1 + + include/libxml/parser.h | 7 +- + parser.c | 459 ++++++++++++++++++++------------------ + parserInternals.c | 8 +- + testrecurse.c | 4 +- + 7 files changed, 276 insertions(+), 227 deletions(-) + +diff --git a/SAX2.c b/SAX2.c +index 18c747d1..c5f1bb68 100644 +--- a/SAX2.c ++++ b/SAX2.c +@@ -413,6 +413,8 @@ xmlSAX2ExternalSubset(void *ctx, const xmlChar *name, + xmlCharEncoding enc; + int oldcharset; + const xmlChar *oldencoding; ++ unsigned long consumed; ++ size_t buffered; + + /* + * Ask the Entity resolver to load the damn thing +@@ -481,6 +483,18 @@ xmlSAX2ExternalSubset(void *ctx, const xmlChar *name, + + while (ctxt->inputNr > 1) + xmlPopInput(ctxt); ++ ++ consumed = ctxt->input->consumed; ++ buffered = ctxt->input->cur - ctxt->input->base; ++ if (buffered > ULONG_MAX - consumed) ++ consumed = ULONG_MAX; ++ else ++ consumed += buffered; ++ if (consumed > ULONG_MAX - ctxt->sizeentities) ++ ctxt->sizeentities = ULONG_MAX; ++ else ++ ctxt->sizeentities += consumed; ++ + xmlFreeInputStream(ctxt->input); + xmlFree(ctxt->inputTab); + +diff --git a/entities.c b/entities.c +index a351ab1c..8f0e1670 100644 +--- a/entities.c ++++ b/entities.c +@@ -38,35 +38,35 @@ static xmlEntity xmlEntityLt = { + NULL, NULL, NULL, NULL, NULL, NULL, + BAD_CAST "<", BAD_CAST "<", 1, + XML_INTERNAL_PREDEFINED_ENTITY, +- NULL, NULL, NULL, NULL, 0, 1, 0 ++ NULL, NULL, NULL, NULL, 0, 1, 0, 0 + }; + static xmlEntity xmlEntityGt = { + NULL, XML_ENTITY_DECL, BAD_CAST "gt", + NULL, NULL, NULL, NULL, NULL, NULL, + BAD_CAST ">", BAD_CAST ">", 1, + XML_INTERNAL_PREDEFINED_ENTITY, +- NULL, NULL, NULL, NULL, 0, 1, 0 ++ NULL, NULL, NULL, NULL, 0, 1, 0, 0 + }; + static xmlEntity xmlEntityAmp = { + NULL, XML_ENTITY_DECL, BAD_CAST "amp", + NULL, NULL, NULL, NULL, NULL, NULL, + BAD_CAST "&", BAD_CAST "&", 1, + XML_INTERNAL_PREDEFINED_ENTITY, +- NULL, NULL, NULL, NULL, 0, 1, 0 ++ NULL, NULL, NULL, NULL, 0, 1, 0, 0 + }; + static xmlEntity xmlEntityQuot = { + NULL, XML_ENTITY_DECL, BAD_CAST "quot", + NULL, NULL, NULL, NULL, NULL, NULL, + BAD_CAST "\"", BAD_CAST "\"", 1, + XML_INTERNAL_PREDEFINED_ENTITY, +- NULL, NULL, NULL, NULL, 0, 1, 0 ++ NULL, NULL, NULL, NULL, 0, 1, 0, 0 + }; + static xmlEntity xmlEntityApos = { + NULL, XML_ENTITY_DECL, BAD_CAST "apos", + NULL, NULL, NULL, NULL, NULL, NULL, + BAD_CAST "'", BAD_CAST "'", 1, + XML_INTERNAL_PREDEFINED_ENTITY, +- NULL, NULL, NULL, NULL, 0, 1, 0 ++ NULL, NULL, NULL, NULL, 0, 1, 0, 0 + }; + + /** +diff --git a/include/libxml/entities.h b/include/libxml/entities.h +index ee1ba341..ecb4358e 100644 +--- a/include/libxml/entities.h ++++ b/include/libxml/entities.h +@@ -61,6 +61,7 @@ struct _xmlEntity { + * references done from that entity + * and if it contains '<' */ + int flags; /* various flags */ ++ unsigned long expandedSize; /* expanded size */ + }; + + /* +diff --git a/include/libxml/parser.h b/include/libxml/parser.h +index 0ba1c387..e66de9c0 100644 +--- a/include/libxml/parser.h ++++ b/include/libxml/parser.h +@@ -63,17 +63,14 @@ struct _xmlParserInput { + int length; /* length if known */ + int line; /* Current line */ + int col; /* Current column */ +- /* +- * NOTE: consumed is only tested for equality in the parser code, +- * so even if there is an overflow this should not give troubles +- * for parsing very large instances. +- */ + unsigned long consumed; /* How many xmlChars already consumed */ + xmlParserInputDeallocate free; /* function to deallocate the base */ + const xmlChar *encoding; /* the encoding string for entity */ + const xmlChar *version; /* the version string for entity */ + int standalone; /* Was that entity marked standalone */ + int id; /* an unique identifier for the entity */ ++ unsigned long parentConsumed; /* consumed bytes from parents */ ++ xmlEntityPtr entity; /* entity, if any */ + }; + + /** +diff --git a/parser.c b/parser.c +index 2e565428..b36eae6b 100644 +--- a/parser.c ++++ b/parser.c +@@ -95,9 +95,6 @@ struct _xmlStartTag { + int nsNr; + }; + +-static void +-xmlFatalErr(xmlParserCtxtPtr ctxt, xmlParserErrors error, const char *info); +- + static xmlParserCtxtPtr + xmlCreateEntityParserCtxtInternal(const xmlChar *URL, const xmlChar *ID, + const xmlChar *base, xmlParserCtxtPtr pctx); +@@ -129,144 +126,7 @@ xmlParseElementEnd(xmlParserCtxtPtr ctxt); + */ + #define XML_PARSER_NON_LINEAR 10 + +-/* +- * xmlParserEntityCheck +- * +- * Function to check non-linear entity expansion behaviour +- * This is here to detect and stop exponential linear entity expansion +- * This is not a limitation of the parser but a safety +- * boundary feature. It can be disabled with the XML_PARSE_HUGE +- * parser option. +- */ +-static int +-xmlParserEntityCheck(xmlParserCtxtPtr ctxt, size_t size, +- xmlEntityPtr ent, size_t replacement) +-{ +- size_t consumed = 0; +- int i; +- +- if ((ctxt == NULL) || (ctxt->options & XML_PARSE_HUGE)) +- return (0); +- if (ctxt->lastError.code == XML_ERR_ENTITY_LOOP) +- return (1); +- +- /* +- * This may look absurd but is needed to detect +- * entities problems +- */ +- if ((ent != NULL) && (ent->etype != XML_INTERNAL_PREDEFINED_ENTITY) && +- (ent->content != NULL) && (ent->checked == 0) && +- (ctxt->errNo != XML_ERR_ENTITY_LOOP)) { +- unsigned long oldnbent = ctxt->nbentities; +- xmlChar *rep; +- +- ent->checked = 1; +- +- ++ctxt->depth; +- rep = xmlStringDecodeEntities(ctxt, ent->content, +- XML_SUBSTITUTE_REF, 0, 0, 0); +- --ctxt->depth; +- if ((rep == NULL) || (ctxt->errNo == XML_ERR_ENTITY_LOOP)) { +- ent->content[0] = 0; +- } +- +- ent->checked = ctxt->nbentities - oldnbent + 1; +- if (rep != NULL) { +- xmlFree(rep); +- rep = NULL; +- } +- } +- +- /* +- * Prevent entity exponential check, not just replacement while +- * parsing the DTD +- * The check is potentially costly so do that only once in a thousand +- */ +- if ((ctxt->instate == XML_PARSER_DTD) && (ctxt->nbentities > 10000) && +- (ctxt->nbentities % 1024 == 0)) { +- for (i = 0;i < ctxt->inputNr;i++) { +- consumed += ctxt->inputTab[i]->consumed + +- (ctxt->inputTab[i]->cur - ctxt->inputTab[i]->base); +- } +- if (ctxt->nbentities > consumed * XML_PARSER_NON_LINEAR) { +- xmlFatalErr(ctxt, XML_ERR_ENTITY_LOOP, NULL); +- ctxt->instate = XML_PARSER_EOF; +- return (1); +- } +- consumed = 0; +- } +- +- +- +- if (replacement != 0) { +- if (replacement < XML_MAX_TEXT_LENGTH) +- return(0); +- +- /* +- * If the volume of entity copy reaches 10 times the +- * amount of parsed data and over the large text threshold +- * then that's very likely to be an abuse. +- */ +- if (ctxt->input != NULL) { +- consumed = ctxt->input->consumed + +- (ctxt->input->cur - ctxt->input->base); +- } +- consumed += ctxt->sizeentities; +- +- if (replacement < XML_PARSER_NON_LINEAR * consumed) +- return(0); +- } else if (size != 0) { +- /* +- * Do the check based on the replacement size of the entity +- */ +- if (size < XML_PARSER_BIG_ENTITY) +- return(0); +- +- /* +- * A limit on the amount of text data reasonably used +- */ +- if (ctxt->input != NULL) { +- consumed = ctxt->input->consumed + +- (ctxt->input->cur - ctxt->input->base); +- } +- consumed += ctxt->sizeentities; +- +- if ((size < XML_PARSER_NON_LINEAR * consumed) && +- (ctxt->nbentities * 3 < XML_PARSER_NON_LINEAR * consumed)) +- return (0); +- } else if (ent != NULL) { +- /* +- * use the number of parsed entities in the replacement +- */ +- size = ent->checked; +- +- /* +- * The amount of data parsed counting entities size only once +- */ +- if (ctxt->input != NULL) { +- consumed = ctxt->input->consumed + +- (ctxt->input->cur - ctxt->input->base); +- } +- consumed += ctxt->sizeentities; +- +- /* +- * Check the density of entities for the amount of data +- * knowing an entity reference will take at least 3 bytes +- */ +- if (size * 3 < consumed * XML_PARSER_NON_LINEAR) +- return (0); +- } else { +- /* +- * strange we got no data for checking +- */ +- if (((ctxt->lastError.code != XML_ERR_UNDECLARED_ENTITY) && +- (ctxt->lastError.code != XML_WAR_UNDECLARED_ENTITY)) || +- (ctxt->nbentities <= 10000)) +- return (0); +- } +- xmlFatalErr(ctxt, XML_ERR_ENTITY_LOOP, NULL); +- return (1); +-} ++#define XML_ENT_FIXED_COST 50 + + /** + * xmlParserMaxDepth: +@@ -863,6 +723,83 @@ xmlNsWarn(xmlParserCtxtPtr ctxt, xmlParserErrors error, + info1, info2, info3); + } + ++static void ++xmlSaturatedAdd(unsigned long *dst, unsigned long val) { ++ if (val > ULONG_MAX - *dst) ++ *dst = ULONG_MAX; ++ else ++ *dst += val; ++} ++ ++static void ++xmlSaturatedAddSizeT(unsigned long *dst, unsigned long val) { ++ if (val > ULONG_MAX - *dst) ++ *dst = ULONG_MAX; ++ else ++ *dst += val; ++} ++ ++/** ++ * xmlParserEntityCheck: ++ * @ctxt: parser context ++ * @extra: sum of unexpanded entity sizes ++ * ++ * Check for non-linear entity expansion behaviour. ++ * ++ * In some cases like xmlStringDecodeEntities, this function is called ++ * for each, possibly nested entity and its unexpanded content length. ++ * ++ * In other cases like xmlParseReference, it's only called for each ++ * top-level entity with its unexpanded content length plus the sum of ++ * the unexpanded content lengths (plus fixed cost) of all nested ++ * entities. ++ * ++ * Summing the unexpanded lengths also adds the length of the reference. ++ * This is by design. Taking the length of the entity name into account ++ * discourages attacks that try to waste CPU time with abusively long ++ * entity names. See test/recurse/lol6.xml for example. Each call also ++ * adds some fixed cost XML_ENT_FIXED_COST to discourage attacks with ++ * short entities. ++ * ++ * Returns 1 on error, 0 on success. ++ */ ++static int ++xmlParserEntityCheck(xmlParserCtxtPtr ctxt, unsigned long extra) ++{ ++ unsigned long consumed; ++ ++ /* ++ * Compute total consumed bytes so far, including input streams of ++ * external entities. ++ */ ++ consumed = ctxt->input->parentConsumed; ++ xmlSaturatedAdd(&consumed, ctxt->input->consumed); ++ xmlSaturatedAddSizeT(&consumed, ctxt->input->cur - ctxt->input->base); ++ xmlSaturatedAdd(&consumed, ctxt->sizeentities); ++ ++ /* ++ * Add extra cost and some fixed cost. ++ */ ++ xmlSaturatedAdd(&ctxt->sizeentcopy, extra); ++ xmlSaturatedAdd(&ctxt->sizeentcopy, XML_ENT_FIXED_COST); ++ ++ /* ++ * It's important to always use saturation arithmetic when tracking ++ * entity sizes to make the size checks reliable. If "sizeentcopy" ++ * overflows, we have to abort. ++ */ ++ if ((ctxt->sizeentcopy > XML_MAX_TEXT_LENGTH) && ++ ((ctxt->sizeentcopy >= ULONG_MAX) || ++ (ctxt->sizeentcopy / XML_PARSER_NON_LINEAR > consumed))) { ++ xmlFatalErrMsg(ctxt, XML_ERR_ENTITY_LOOP, ++ "Maximum entity amplification factor exceeded"); ++ xmlHaltParser(ctxt); ++ return(1); ++ } ++ ++ return(0); ++} ++ + /************************************************************************ + * * + * Library wide options * +@@ -2228,8 +2165,30 @@ xmlSkipBlankChars(xmlParserCtxtPtr ctxt) { + break; + xmlParsePEReference(ctxt); + } else if (CUR == 0) { ++ unsigned long consumed; ++ xmlEntityPtr ent; ++ + if (ctxt->inputNr <= 1) + break; ++ ++ consumed = ctxt->input->consumed; ++ xmlSaturatedAddSizeT(&consumed, ++ ctxt->input->cur - ctxt->input->base); ++ ++ /* ++ * Add to sizeentities when parsing an external entity ++ * for the first time. ++ */ ++ ent = ctxt->input->entity; ++ if ((ent->etype == XML_EXTERNAL_PARAMETER_ENTITY) && ++ ((ent->flags & XML_ENT_PARSED) == 0)) { ++ ent->flags |= XML_ENT_PARSED; ++ ++ xmlSaturatedAdd(&ctxt->sizeentities, consumed); ++ } ++ ++ xmlParserEntityCheck(ctxt, consumed); ++ + xmlPopInput(ctxt); + } else { + break; +@@ -2623,7 +2582,7 @@ xmlParserHandlePEReference(xmlParserCtxtPtr ctxt) { + } + + /** +- * xmlStringLenDecodeEntities: ++ * xmlStringDecodeEntitiesInt: + * @ctxt: the parser context + * @str: the input string + * @len: the string length +@@ -2631,19 +2590,12 @@ xmlParserHandlePEReference(xmlParserCtxtPtr ctxt) { + * @end: an end marker xmlChar, 0 if none + * @end2: an end marker xmlChar, 0 if none + * @end3: an end marker xmlChar, 0 if none +- * +- * Takes a entity string content and process to do the adequate substitutions. +- * +- * [67] Reference ::= EntityRef | CharRef +- * +- * [69] PEReference ::= '%' Name ';' +- * +- * Returns A newly allocated string with the substitution done. The caller +- * must deallocate it ! ++ * @check: whether to perform entity checks + */ +-xmlChar * +-xmlStringLenDecodeEntities(xmlParserCtxtPtr ctxt, const xmlChar *str, int len, +- int what, xmlChar end, xmlChar end2, xmlChar end3) { ++static xmlChar * ++xmlStringDecodeEntitiesInt(xmlParserCtxtPtr ctxt, const xmlChar *str, int len, ++ int what, xmlChar end, xmlChar end2, xmlChar end3, ++ int check) { + xmlChar *buffer = NULL; + size_t buffer_size = 0; + size_t nbchars = 0; +@@ -2654,8 +2606,8 @@ xmlStringLenDecodeEntities(xmlParserCtxtPtr ctxt, const xmlChar *str, int len, + xmlEntityPtr ent; + int c,l; + +- if ((ctxt == NULL) || (str == NULL) || (len < 0)) +- return(NULL); ++ if (str == NULL) ++ return(NULL); + last = str + len; + + if (((ctxt->depth > 40) && +@@ -2699,7 +2651,6 @@ xmlStringLenDecodeEntities(xmlParserCtxtPtr ctxt, const xmlChar *str, int len, + "String decoding Entity Reference: %.30s\n", + str); + ent = xmlParseStringEntityRef(ctxt, &str); +- xmlParserEntityCheck(ctxt, 0, ent, 0); + if (ent != NULL) + ctxt->nbentities += ent->checked; + if ((ent != NULL) && +@@ -2715,9 +2666,12 @@ xmlStringLenDecodeEntities(xmlParserCtxtPtr ctxt, const xmlChar *str, int len, + goto int_error; + } + } else if ((ent != NULL) && (ent->content != NULL)) { ++ if ((check) && (xmlParserEntityCheck(ctxt, ent->length))) ++ goto int_error; ++ + ctxt->depth++; +- rep = xmlStringDecodeEntities(ctxt, ent->content, what, +- 0, 0, 0); ++ rep = xmlStringDecodeEntitiesInt(ctxt, ent->content, ++ ent->length, what, 0, 0, 0, check); + ctxt->depth--; + if (rep == NULL) { + ent->content[0] = 0; +@@ -2728,8 +2682,6 @@ xmlStringLenDecodeEntities(xmlParserCtxtPtr ctxt, const xmlChar *str, int len, + while (*current != 0) { /* non input consuming loop */ + buffer[nbchars++] = *current++; + if (nbchars + XML_PARSER_BUFFER_SIZE > buffer_size) { +- if (xmlParserEntityCheck(ctxt, nbchars, ent, 0)) +- goto int_error; + growBuffer(buffer, XML_PARSER_BUFFER_SIZE); + } + } +@@ -2752,7 +2704,6 @@ xmlStringLenDecodeEntities(xmlParserCtxtPtr ctxt, const xmlChar *str, int len, + xmlGenericError(xmlGenericErrorContext, + "String decoding PE Reference: %.30s\n", str); + ent = xmlParseStringPEReference(ctxt, &str); +- xmlParserEntityCheck(ctxt, 0, ent, 0); + if (ent != NULL) + ctxt->nbentities += ent->checked; + if (ent != NULL) { +@@ -2773,9 +2724,13 @@ xmlStringLenDecodeEntities(xmlParserCtxtPtr ctxt, const xmlChar *str, int len, + ent->name, NULL); + } + } ++ ++ if ((check) && (xmlParserEntityCheck(ctxt, ent->length))) ++ goto int_error; ++ + ctxt->depth++; +- rep = xmlStringDecodeEntities(ctxt, ent->content, what, +- 0, 0, 0); ++ rep = xmlStringDecodeEntitiesInt(ctxt, ent->content, ++ ent->length, what, 0, 0, 0, check); + ctxt->depth--; + if (rep == NULL) { + if (ent->content != NULL) +@@ -2786,8 +2741,6 @@ xmlStringLenDecodeEntities(xmlParserCtxtPtr ctxt, const xmlChar *str, int len, + while (*current != 0) { /* non input consuming loop */ + buffer[nbchars++] = *current++; + if (nbchars + XML_PARSER_BUFFER_SIZE > buffer_size) { +- if (xmlParserEntityCheck(ctxt, nbchars, ent, 0)) +- goto int_error; + growBuffer(buffer, XML_PARSER_BUFFER_SIZE); + } + } +@@ -2819,6 +2772,35 @@ int_error: + return(NULL); + } + ++/** ++ * xmlStringLenDecodeEntities: ++ * @ctxt: the parser context ++ * @str: the input string ++ * @len: the string length ++ * @what: combination of XML_SUBSTITUTE_REF and XML_SUBSTITUTE_PEREF ++ * @end: an end marker xmlChar, 0 if none ++ * @end2: an end marker xmlChar, 0 if none ++ * @end3: an end marker xmlChar, 0 if none ++ * ++ * Takes a entity string content and process to do the adequate substitutions. ++ * ++ * [67] Reference ::= EntityRef | CharRef ++ * ++ * [69] PEReference ::= '%' Name ';' ++ * ++ * Returns A newly allocated string with the substitution done. The caller ++ * must deallocate it ! ++ */ ++xmlChar * ++xmlStringLenDecodeEntities(xmlParserCtxtPtr ctxt, const xmlChar *str, int len, ++ int what, xmlChar end, xmlChar end2, ++ xmlChar end3) { ++ if ((ctxt == NULL) || (str == NULL) || (len < 0)) ++ return(NULL); ++ return(xmlStringDecodeEntitiesInt(ctxt, str, len, what, ++ end, end2, end3, 0)); ++} ++ + /** + * xmlStringDecodeEntities: + * @ctxt: the parser context +@@ -2841,8 +2823,8 @@ xmlChar * + xmlStringDecodeEntities(xmlParserCtxtPtr ctxt, const xmlChar *str, int what, + xmlChar end, xmlChar end2, xmlChar end3) { + if ((ctxt == NULL) || (str == NULL)) return(NULL); +- return(xmlStringLenDecodeEntities(ctxt, str, xmlStrlen(str), what, +- end, end2, end3)); ++ return(xmlStringDecodeEntitiesInt(ctxt, str, xmlStrlen(str), what, ++ end, end2, end3, 0)); + } + + /************************************************************************ +@@ -3893,9 +3875,10 @@ xmlParseEntityValue(xmlParserCtxtPtr ctxt, xmlChar **orig) { + * so XML_SUBSTITUTE_REF is not set here. + */ + ++ctxt->depth; +- ret = xmlStringDecodeEntities(ctxt, buf, XML_SUBSTITUTE_PEREF, +- 0, 0, 0); ++ ret = xmlStringDecodeEntitiesInt(ctxt, buf, len, XML_SUBSTITUTE_PEREF, ++ 0, 0, 0, /* check */ 1); + --ctxt->depth; ++ + if (orig != NULL) { + *orig = buf; + buf = NULL; +@@ -4014,10 +3997,13 @@ xmlParseAttValueComplex(xmlParserCtxtPtr ctxt, int *attlen, int normalize) { + } else if ((ent != NULL) && + (ctxt->replaceEntities != 0)) { + if (ent->etype != XML_INTERNAL_PREDEFINED_ENTITY) { ++ if (xmlParserEntityCheck(ctxt, ent->length)) ++ goto error; ++ + ++ctxt->depth; +- rep = xmlStringDecodeEntities(ctxt, ent->content, +- XML_SUBSTITUTE_REF, +- 0, 0, 0); ++ rep = xmlStringDecodeEntitiesInt(ctxt, ent->content, ++ ent->length, XML_SUBSTITUTE_REF, 0, 0, 0, ++ /* check */ 1); + --ctxt->depth; + if (rep != NULL) { + current = rep; +@@ -4047,24 +4033,40 @@ xmlParseAttValueComplex(xmlParserCtxtPtr ctxt, int *attlen, int normalize) { + const xmlChar *cur = ent->name; + + /* +- * This may look absurd but is needed to detect +- * entities problems ++ * We also check for recursion and amplification ++ * when entities are not substituted. They're ++ * often expanded later. + */ + if ((ent->etype != XML_INTERNAL_PREDEFINED_ENTITY) && +- (ent->content != NULL) && (ent->checked == 0)) { +- unsigned long oldnbent = ctxt->nbentities; +- +- ++ctxt->depth; +- rep = xmlStringDecodeEntities(ctxt, ent->content, +- XML_SUBSTITUTE_REF, 0, 0, 0); +- --ctxt->depth; +- +- ent->checked = ctxt->nbentities - oldnbent + 1; +- if (rep != NULL) { +- xmlFree(rep); +- rep = NULL; +- } else { +- ent->content[0] = 0; ++ (ent->content != NULL)) { ++ if (ent->checked == 0) { ++ unsigned long oldnbent = ctxt->nbentities; ++ unsigned long oldCopy = ctxt->sizeentcopy; ++ ++ ctxt->sizeentcopy = ent->length; ++ ++ ++ctxt->depth; ++ rep = xmlStringDecodeEntitiesInt(ctxt, ++ ent->content, ent->length, ++ XML_SUBSTITUTE_REF, 0, 0, 0, ++ /* check */ 1); ++ --ctxt->depth; ++ ++ ent->checked = ctxt->nbentities - oldnbent + 1; ++ ent->expandedSize = ctxt->sizeentcopy; ++ ++ if (rep != NULL) { ++ xmlFree(rep); ++ rep = NULL; ++ } else { ++ ent->content[0] = 0; ++ } ++ ++ if (xmlParserEntityCheck(ctxt, oldCopy)) ++ goto error; ++ } else { ++ if (xmlParserEntityCheck(ctxt, ent->expandedSize)) ++ goto error; + } + } + +@@ -7165,6 +7167,7 @@ xmlParseReference(xmlParserCtxtPtr ctxt) { + ((ent->etype != XML_EXTERNAL_GENERAL_PARSED_ENTITY) || + (ctxt->options & (XML_PARSE_NOENT | XML_PARSE_DTDVALID)))) { + unsigned long oldnbent = ctxt->nbentities; ++ unsigned long oldsizeentcopy = ctxt->sizeentcopy; + + /* + * This is a bit hackish but this seems the best +@@ -7177,6 +7180,9 @@ xmlParseReference(xmlParserCtxtPtr ctxt) { + else + user_data = ctxt->userData; + ++ /* Avoid overflow as much as possible */ ++ ctxt->sizeentcopy = 0; ++ + /* + * Check that this entity is well formed + * 4.3.2: An internal general parsed entity is well-formed +@@ -7202,6 +7208,7 @@ xmlParseReference(xmlParserCtxtPtr ctxt) { + } + + ent->flags |= XML_ENT_PARSED; ++ ent->expandedSize = ctxt->sizeentcopy; + /* + * Store the number of entities needing parsing for this entity + * content and do checkings +@@ -7213,7 +7220,7 @@ xmlParseReference(xmlParserCtxtPtr ctxt) { + xmlFreeNodeList(list); + return; + } +- if (xmlParserEntityCheck(ctxt, 0, ent, 0)) { ++ if (xmlParserEntityCheck(ctxt, oldsizeentcopy)) { + xmlFreeNodeList(list); + return; + } +@@ -7264,7 +7271,6 @@ xmlParseReference(xmlParserCtxtPtr ctxt) { + "Entity '%s' failed to parse\n", ent->name); + if (ent->content != NULL) + ent->content[0] = 0; +- xmlParserEntityCheck(ctxt, 0, ent, 0); + } else if (list != NULL) { + xmlFreeNodeList(list); + list = NULL; +@@ -7309,11 +7315,16 @@ xmlParseReference(xmlParserCtxtPtr ctxt) { + ctxt->depth--; + } else if (ent->etype == + XML_EXTERNAL_GENERAL_PARSED_ENTITY) { ++ unsigned long oldsizeentities = ctxt->sizeentities; ++ + ctxt->depth++; + ret = xmlParseExternalEntityPrivate(ctxt->myDoc, ctxt, + ctxt->sax, user_data, ctxt->depth, + ent->URI, ent->ExternalID, NULL); + ctxt->depth--; ++ ++ /* Undo the change to sizeentities */ ++ ctxt->sizeentities = oldsizeentities; + } else { + ret = XML_ERR_ENTITY_PE_INTERNAL; + xmlErrMsgStr(ctxt, XML_ERR_INTERNAL_ERROR, +@@ -7323,6 +7334,8 @@ xmlParseReference(xmlParserCtxtPtr ctxt) { + xmlFatalErr(ctxt, XML_ERR_ENTITY_LOOP, NULL); + return; + } ++ if (xmlParserEntityCheck(ctxt, 0)) ++ return; + } + if ((ctxt->sax != NULL) && (ctxt->sax->reference != NULL) && + (ctxt->replaceEntities == 0) && (!ctxt->disableSAX)) { +@@ -7335,6 +7348,14 @@ xmlParseReference(xmlParserCtxtPtr ctxt) { + return; + } + ++ /* ++ * We also check for amplification if entities aren't substituted. ++ * They might be expanded later. ++ */ ++ if ((was_checked != 0) && ++ (xmlParserEntityCheck(ctxt, ent->expandedSize))) ++ return; ++ + /* + * If we didn't get any children for the entity being built + */ +@@ -7371,13 +7392,6 @@ xmlParseReference(xmlParserCtxtPtr ctxt) { + (ctxt->parseMode == XML_PARSE_READER)) { + xmlNodePtr nw = NULL, cur, firstChild = NULL; + +- /* +- * We are copying here, make sure there is no abuse +- */ +- ctxt->sizeentcopy += ent->length + 5; +- if (xmlParserEntityCheck(ctxt, 0, ent, ctxt->sizeentcopy)) +- return; +- + /* + * when operating on a reader, the entities definitions + * are always owning the entities subtree. +@@ -7419,13 +7433,6 @@ xmlParseReference(xmlParserCtxtPtr ctxt) { + xmlNodePtr nw = NULL, cur, next, last, + firstChild = NULL; + +- /* +- * We are copying here, make sure there is no abuse +- */ +- ctxt->sizeentcopy += ent->length + 5; +- if (xmlParserEntityCheck(ctxt, 0, ent, ctxt->sizeentcopy)) +- return; +- + /* + * Copy the entity child list and make it the new + * entity child list. The goal is to make sure any +@@ -7608,7 +7615,6 @@ xmlParseEntityRef(xmlParserCtxtPtr ctxt) { + ctxt->sax->reference(ctxt->userData, name); + } + } +- xmlParserEntityCheck(ctxt, 0, ent, 0); + ctxt->valid = 0; + } + +@@ -7805,7 +7811,6 @@ xmlParseStringEntityRef(xmlParserCtxtPtr ctxt, const xmlChar ** str) { + "Entity '%s' not defined\n", + name); + } +- xmlParserEntityCheck(ctxt, 0, ent, 0); + /* TODO ? check regressions ctxt->valid = 0; */ + } + +@@ -7976,7 +7981,6 @@ xmlParsePEReference(xmlParserCtxtPtr ctxt) + name, NULL); + ctxt->valid = 0; + } +- xmlParserEntityCheck(ctxt, 0, NULL, 0); + } else { + /* + * Internal checking in case the entity quest barfed +@@ -7989,9 +7993,7 @@ xmlParsePEReference(xmlParserCtxtPtr ctxt) + } else { + xmlChar start[4]; + xmlCharEncoding enc; +- +- if (xmlParserEntityCheck(ctxt, 0, entity, 0)) +- return; ++ unsigned long parentConsumed; + + if ((entity->etype == XML_EXTERNAL_PARAMETER_ENTITY) && + ((ctxt->options & XML_PARSE_NOENT) == 0) && +@@ -8002,12 +8004,19 @@ xmlParsePEReference(xmlParserCtxtPtr ctxt) + (ctxt->validate == 0)) + return; + ++ parentConsumed = ctxt->input->parentConsumed; ++ xmlSaturatedAdd(&parentConsumed, ctxt->input->consumed); ++ xmlSaturatedAddSizeT(&parentConsumed, ++ ctxt->input->cur - ctxt->input->base); ++ + input = xmlNewEntityInputStream(ctxt, entity); + if (xmlPushInput(ctxt, input) < 0) { + xmlFreeInputStream(input); + return; + } + ++ input->parentConsumed = parentConsumed; ++ + if (entity->etype == XML_EXTERNAL_PARAMETER_ENTITY) { + /* + * Get the 4 first bytes and decode the charset +@@ -8124,6 +8133,7 @@ xmlLoadEntityContent(xmlParserCtxtPtr ctxt, xmlEntityPtr entity) { + } + + if ((ctxt->input == input) && (ctxt->input->cur >= ctxt->input->end)) { ++ xmlSaturatedAdd(&ctxt->sizeentities, ctxt->input->consumed); + xmlPopInput(ctxt); + } else if (!IS_CHAR(c)) { + xmlFatalErrMsgInt(ctxt, XML_ERR_INVALID_CHAR, +@@ -8133,6 +8143,7 @@ xmlLoadEntityContent(xmlParserCtxtPtr ctxt, xmlEntityPtr entity) { + return(-1); + } + entity->content = buf->content; ++ entity->length = buf->use; + buf->content = NULL; + xmlBufferFree(buf); + +@@ -8241,7 +8252,6 @@ xmlParseStringPEReference(xmlParserCtxtPtr ctxt, const xmlChar **str) { + name, NULL); + ctxt->valid = 0; + } +- xmlParserEntityCheck(ctxt, 0, NULL, 0); + } else { + /* + * Internal checking in case the entity quest barfed +@@ -9155,6 +9165,8 @@ xmlParseAttribute2(xmlParserCtxtPtr ctxt, + NEXT; + SKIP_BLANKS; + val = xmlParseAttValueInternal(ctxt, len, alloc, normalize); ++ if (val == NULL) ++ return (NULL); + if (normalize) { + /* + * Sometimes a second normalisation pass for spaces is needed +@@ -13187,8 +13199,15 @@ xmlParseExternalEntityPrivate(xmlDocPtr doc, xmlParserCtxtPtr oldctxt, + * Also record the size of the entity parsed + */ + if (ctxt->input != NULL && oldctxt != NULL) { +- oldctxt->sizeentities += ctxt->input->consumed; +- oldctxt->sizeentities += (ctxt->input->cur - ctxt->input->base); ++ unsigned long consumed = ctxt->input->consumed; ++ ++ xmlSaturatedAddSizeT(&consumed, ctxt->input->cur - ctxt->input->base); ++ ++ xmlSaturatedAdd(&oldctxt->sizeentities, consumed); ++ xmlSaturatedAdd(&oldctxt->sizeentities, ctxt->sizeentities); ++ ++ xmlSaturatedAdd(&oldctxt->sizeentcopy, consumed); ++ xmlSaturatedAdd(&oldctxt->sizeentcopy, ctxt->sizeentcopy); + } + /* + * And record the last error if any +@@ -13452,6 +13471,18 @@ xmlParseBalancedChunkMemoryInternal(xmlParserCtxtPtr oldctxt, + if (oldctxt != NULL) + oldctxt->nbentities += ctxt->nbentities; + ++ /* ++ * Also record the size of the entity parsed ++ */ ++ if (ctxt->input != NULL && oldctxt != NULL) { ++ unsigned long consumed = ctxt->input->consumed; ++ ++ xmlSaturatedAddSizeT(&consumed, ctxt->input->cur - ctxt->input->base); ++ ++ xmlSaturatedAdd(&oldctxt->sizeentcopy, consumed); ++ xmlSaturatedAdd(&oldctxt->sizeentcopy, ctxt->sizeentcopy); ++ } ++ + /* + * Also record the last error if any + */ +diff --git a/parserInternals.c b/parserInternals.c +index c5c0b16d..7e03efcd 100644 +--- a/parserInternals.c ++++ b/parserInternals.c +@@ -1446,8 +1446,11 @@ xmlNewEntityInputStream(xmlParserCtxtPtr ctxt, xmlEntityPtr entity) { + break; + case XML_EXTERNAL_GENERAL_PARSED_ENTITY: + case XML_EXTERNAL_PARAMETER_ENTITY: +- return(xmlLoadExternalEntity((char *) entity->URI, +- (char *) entity->ExternalID, ctxt)); ++ input = xmlLoadExternalEntity((char *) entity->URI, ++ (char *) entity->ExternalID, ctxt); ++ if (input != NULL) ++ input->entity = entity; ++ return(input); + case XML_INTERNAL_GENERAL_ENTITY: + xmlErrInternal(ctxt, + "Internal entity %s without content !\n", +@@ -1478,6 +1481,7 @@ xmlNewEntityInputStream(xmlParserCtxtPtr ctxt, xmlEntityPtr entity) { + input->cur = entity->content; + input->length = entity->length; + input->end = &entity->content[input->length]; ++ input->entity = entity; + return(input); + } + +diff --git a/testrecurse.c b/testrecurse.c +index 0cbe25a6..a9000fe9 100644 +--- a/testrecurse.c ++++ b/testrecurse.c +@@ -166,7 +166,9 @@ static const char *start = " \ + "; + +-static const char *segment = " &e; &f; &d;\n"; ++static const char *segment = ++ " &e; &f; &d;\n" ++ " _123456789_123456789_123456789_123456789\n"; + static const char *finish = ""; + + static int curseg = 0; + +From 1541c761e2ccdb6424e5619446445ee2380246e1 Mon Sep 17 00:00:00 2001 +From: Nick Wellnhofer +Date: Tue, 17 Jan 2023 13:50:51 +0100 +Subject: [PATCH 5/6] parser: Fix entity check in attributes + +Don't set the "checked" flag when checking entities in default attribute +values. These entities could reference other entities which weren't +defined yet, so the check isn't reliable. + +This fixes a short-lived regression which could lead to a call stack +overflow later in xmlStringGetNodeList. +--- + parser.c | 12 ++++++++++-- + result/errors/rec_att_default.xml.ent | 6 ++++++ + result/errors/rec_att_default.xml.err | 6 ++++++ + result/errors/rec_att_default.xml.str | 7 +++++++ + test/errors/rec_att_default.xml | 6 ++++++ + 5 files changed, 35 insertions(+), 2 deletions(-) + create mode 100644 result/errors/rec_att_default.xml.ent + create mode 100644 result/errors/rec_att_default.xml.err + create mode 100644 result/errors/rec_att_default.xml.str + create mode 100644 test/errors/rec_att_default.xml + +diff --git a/parser.c b/parser.c +index b36eae6b..1a2d1844 100644 +--- a/parser.c ++++ b/parser.c +@@ -4052,8 +4052,16 @@ xmlParseAttValueComplex(xmlParserCtxtPtr ctxt, int *attlen, int normalize) { + /* check */ 1); + --ctxt->depth; + +- ent->checked = ctxt->nbentities - oldnbent + 1; +- ent->expandedSize = ctxt->sizeentcopy; ++ /* ++ * If we're parsing DTD content, the entity ++ * might reference other entities which ++ * weren't defined yet, so the check isn't ++ * reliable. ++ */ ++ if (ctxt->inSubset == 0) { ++ ent->checked = ctxt->nbentities - oldnbent + 1; ++ ent->expandedSize = ctxt->sizeentcopy; ++ } + + if (rep != NULL) { + xmlFree(rep); +diff --git a/result/errors/rec_att_default.xml.ent b/result/errors/rec_att_default.xml.ent +new file mode 100644 +index 00000000..375a0d65 +--- /dev/null ++++ b/result/errors/rec_att_default.xml.ent +@@ -0,0 +1,6 @@ ++./test/errors/rec_att_default.xml:3: parser error : Entity 'b' not defined ++ ++ ^ ++./test/errors/rec_att_default.xml:6: parser error : Detected an entity reference loop ++ ++ ^ +diff --git a/result/errors/rec_att_default.xml.err b/result/errors/rec_att_default.xml.err +new file mode 100644 +index 00000000..375a0d65 +--- /dev/null ++++ b/result/errors/rec_att_default.xml.err +@@ -0,0 +1,6 @@ ++./test/errors/rec_att_default.xml:3: parser error : Entity 'b' not defined ++ ++ ^ ++./test/errors/rec_att_default.xml:6: parser error : Detected an entity reference loop ++ ++ ^ +diff --git a/result/errors/rec_att_default.xml.str b/result/errors/rec_att_default.xml.str +new file mode 100644 +index 00000000..11e6556b +--- /dev/null ++++ b/result/errors/rec_att_default.xml.str +@@ -0,0 +1,7 @@ ++./test/errors/rec_att_default.xml:3: parser error : Entity 'b' not defined ++ ++ ^ ++./test/errors/rec_att_default.xml:6: parser error : Detected an entity reference loop ++ ++ ^ ++./test/errors/rec_att_default.xml : failed to parse +diff --git a/test/errors/rec_att_default.xml b/test/errors/rec_att_default.xml +new file mode 100644 +index 00000000..9a336008 +--- /dev/null ++++ b/test/errors/rec_att_default.xml +@@ -0,0 +1,6 @@ ++ ++ ++ ++]> ++ + +From 0b4434e809c54052dea1219db67d09e358ce1a6e Mon Sep 17 00:00:00 2001 +From: RHEL Packaging Agent +Date: Mon, 27 Jul 2026 08:08:58 +0000 +Subject: [PATCH 6/6] Add XML_HIDDEN macro definition to libxml.h + +The XML_HIDDEN macro is used in include/private/entities.h but was not +defined in the v2.9.13 codebase. Add the definition from upstream to +libxml.h to fix compilation errors in parser.c and tree.c. +--- + libxml.h | 9 +++++++++ + 1 file changed, 9 insertions(+) + +diff --git a/libxml.h b/libxml.h +index 1090729c..52fe795c 100644 +--- a/libxml.h ++++ b/libxml.h +@@ -79,6 +79,15 @@ int vfprintf(FILE *, const char *, va_list); + #define ATTRIBUTE_NO_SANITIZE(arg) + #endif + ++#if !defined(_WIN32) && \ ++ !defined(__CYGWIN__) && \ ++ (defined(__clang__) || \ ++ (defined(__GNUC__) && (__GNUC__ >= 4))) ++ #define XML_HIDDEN __attribute__((visibility("hidden"))) ++#else ++ #define XML_HIDDEN ++#endif ++ + /* + * Internal variable indicating if a callback has been registered for + * node creation/destruction. It avoids spending a lot of time in locking diff --git a/SPECS/libxml2.spec b/SPECS/libxml2.spec index 4945c40..f1bdf6f 100644 --- a/SPECS/libxml2.spec +++ b/SPECS/libxml2.spec @@ -1,6 +1,6 @@ Name: libxml2 Version: 2.9.13 -Release: 14%{?dist}.2 +Release: 14%{?dist}.4 Summary: Library providing XML and HTML support License: MIT @@ -48,6 +48,12 @@ Patch19: libxml2-2.9.13-CVE-2024-34459.patch # https://gitlab.gnome.org/GNOME/libxml2/-/merge_requests/321 # https://redhat.atlassian.net/browse/RHEL-182016 Patch20: libxml2-2.12.5-CVE-2025-6170.patch +# https://gitlab.gnome.org/GNOME/libxml2/-/commit/463bbeeca1805b5c4828f50d0fefc4eebaf620df +# https://issues.redhat.com/browse/RHEL-215619 +Patch21: libxml2-2.9.13-CVE-2026-6653.patch +# https://issues.redhat.com/browse/RHEL-215568 +# https://gitlab.gnome.org/GNOME/libxml2/-/commit/c2e233fc1b341685fc99621b2768b503f777a72e +Patch22: libxml2-2.9.13-CVE-2026-11979.patch BuildRequires: cmake-rpm-macros BuildRequires: gcc @@ -176,6 +182,12 @@ gzip -9 -c doc/libxml2-api.xml > doc/libxml2-api.xml.gz %{python3_sitearch}/libxml2mod.so %changelog +* Mon Jul 27 2026 RHEL Packaging Agent - 2.9.13-14.4 +- Fix CVE-2026-11979 (RHEL-215568) + +* Mon Jul 27 2026 RHEL Packaging Agent - 2.9.13-14.3 +- Fix CVE-2026-6653 (RHEL-215619) + * Tue Jun 16 2026 David King - 2.9.13-14.2 - Fix CVE-2025-6170 (RHEL-182016)