Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 199 additions & 0 deletions SPECS/expat/CVE-2026-66046.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
From 708c61bcbf22219cc55b791c02869e1197ff7f97 Mon Sep 17 00:00:00 2001
From: Sebastian Pipping <sebastian@pipping.org>
Date: Thu, 13 Aug 2026 15:47:24 +0200
Subject: [PATCH 1/2] lib: Rename hash table `defaultAttsNames` to
`defaultAttForName`

It was previously used as a "set". This prepares for the upcoming
change to a true "dictionary".
---
lib/xmlparse.c | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/lib/xmlparse.c b/lib/xmlparse.c
index ac79b9c..36105f8 100644
--- a/lib/xmlparse.c
+++ b/lib/xmlparse.c
@@ -394,7 +394,7 @@ typedef struct {
size_t nDefaultAtts;
size_t allocDefaultAtts;
DEFAULT_ATTRIBUTE *defaultAtts;
- HASH_TABLE defaultAttsNames;
+ HASH_TABLE defaultAttForName;
} ELEMENT_TYPE;

typedef struct {
@@ -3837,8 +3837,8 @@ storeAtts(XML_Parser parser, const ENCODING *enc, const char *attStr,
sizeof(ELEMENT_TYPE));
if (! elementType)
return XML_ERROR_NO_MEMORY;
- if (! elementType->defaultAttsNames.parser)
- hashTableInit(&(elementType->defaultAttsNames), parser);
+ if (! elementType->defaultAttForName.parser)
+ hashTableInit(&(elementType->defaultAttForName), parser);
if (parser->m_ns && ! setElementTypePrefix(parser, elementType))
return XML_ERROR_NO_MEMORY;
}
@@ -7239,7 +7239,7 @@ defineAttribute(ELEMENT_TYPE *type, ATTRIBUTE_ID *attId, XML_Bool isCdata,
/* The handling of default attributes gets messed up if we have
a default which duplicates a non-default. */
NAMED *const nameFound
- = lookup(parser, &(type->defaultAttsNames), attId->name, 0);
+ = lookup(parser, &(type->defaultAttForName), attId->name, 0);
if (nameFound)
return 1;
if (isId && ! type->idAtt && ! attId->xmlns)
@@ -7276,7 +7276,7 @@ defineAttribute(ELEMENT_TYPE *type, ATTRIBUTE_ID *attId, XML_Bool isCdata,
attId->maybeTokenized = XML_TRUE;

NAMED *const nameAddedOrFound
- = lookup(parser, &(type->defaultAttsNames), attId->name, sizeof(NAMED));
+ = lookup(parser, &(type->defaultAttForName), attId->name, sizeof(NAMED));
if (! nameAddedOrFound)
return 0;

@@ -7597,7 +7597,7 @@ dtdReset(DTD *p, XML_Parser parser) {
ELEMENT_TYPE *e = (ELEMENT_TYPE *)hashTableIterNext(&iter);
if (! e)
break;
- hashTableDestroy(&(e->defaultAttsNames));
+ hashTableDestroy(&(e->defaultAttForName));
FREE(parser, e->defaultAtts);
}
hashTableClear(&(p->generalEntities));
@@ -7639,7 +7639,7 @@ dtdDestroy(DTD *p, XML_Bool isDocEntity, XML_Parser parser) {
ELEMENT_TYPE *e = (ELEMENT_TYPE *)hashTableIterNext(&iter);
if (! e)
break;
- hashTableDestroy(&(e->defaultAttsNames));
+ hashTableDestroy(&(e->defaultAttForName));
FREE(parser, e->defaultAtts);
}
hashTableDestroy(&(p->generalEntities));
@@ -7732,8 +7732,8 @@ dtdCopy(XML_Parser oldParser, DTD *newDtd, const DTD *oldDtd,
if (! newE)
return 0;

- if (! newE->defaultAttsNames.parser)
- hashTableInit(&(newE->defaultAttsNames), parser);
+ if (! newE->defaultAttForName.parser)
+ hashTableInit(&(newE->defaultAttForName), parser);

if (oldE->nDefaultAtts) {
/* Detect and prevent integer overflow. */
@@ -7766,7 +7766,7 @@ dtdCopy(XML_Parser oldParser, DTD *newDtd, const DTD *oldDtd,
} else
newE->defaultAtts[i].value = NULL;

- NAMED *const nameAddedOrFound = lookup(parser, &(newE->defaultAttsNames),
+ NAMED *const nameAddedOrFound = lookup(parser, &(newE->defaultAttForName),
attributeName, sizeof(NAMED));
if (! nameAddedOrFound) {
return 0;
@@ -8535,8 +8535,8 @@ getElementType(XML_Parser parser, const ENCODING *enc, const char *ptr,
sizeof(ELEMENT_TYPE));
if (! ret)
return NULL;
- if (! ret->defaultAttsNames.parser)
- hashTableInit(&(ret->defaultAttsNames), getRootParserOf(parser, NULL));
+ if (! ret->defaultAttForName.parser)
+ hashTableInit(&(ret->defaultAttForName), getRootParserOf(parser, NULL));
if (ret->name != name)
poolDiscard(&dtd->pool);
else {
--
2.45.4


From e8fbef3543aecddd6aada3be013dc6123d08ed48 Mon Sep 17 00:00:00 2001
From: Sebastian Pipping <sebastian@pipping.org>
Date: Thu, 13 Aug 2026 16:39:35 +0200
Subject: [PATCH 2/2] lib: Migrate .isCdata lookup from a linear loop to a hash
table lookup

.. to resolve quadratic runtime

Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
Upstream-reference: https://github.com/libexpat/libexpat/pull/1321.patch
---
lib/xmlparse.c | 48 ++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 40 insertions(+), 8 deletions(-)

diff --git a/lib/xmlparse.c b/lib/xmlparse.c
index 36105f8..0865d4b 100644
--- a/lib/xmlparse.c
+++ b/lib/xmlparse.c
@@ -381,6 +381,22 @@ typedef struct {
const XML_Char *value;
} DEFAULT_ATTRIBUTE;

+// This structure allows mapping attribute names to instances of
+// `DEFAULT_ATTRIBUTE`.
+typedef struct {
+ // Member `name` goes first to make this structure compatible with structure
+ // `NAMED` (further up), which is needed to support use of structure
+ // `NAME_AND_DEFAULT_ATTRIBUTE` in a hash table as implemented by function
+ // `lookup` (further down).
+ const XML_Char *name;
+ // We would store a `DEFAULT_ATTRIBUTE *` here but the backing array
+ // can be reallocated which would invalidate the pointer. Using an index
+ // into the array instead, avoids that problem.
+ size_t attIndex;
+ // This is set to `false` by function `lookup`.
+ bool initialized;
+} NAME_AND_DEFAULT_ATTRIBUTE;
+
typedef struct {
unsigned long version;
unsigned long hash;
@@ -3951,11 +3967,14 @@ storeAtts(XML_Parser parser, const ENCODING *enc, const char *attStr,

/* figure out whether declared as other than CDATA */
if (attId->maybeTokenized) {
- for (size_t j = 0; j < nDefaultAtts; j++) {
- if (attId == elementType->defaultAtts[j].id) {
- isCdata = elementType->defaultAtts[j].isCdata;
- break;
- }
+ NAME_AND_DEFAULT_ATTRIBUTE *const nameAndDefaultAttribute
+ = (NAME_AND_DEFAULT_ATTRIBUTE *)lookup(
+ parser, &(elementType->defaultAttForName), attId->name, 0);
+ if (nameAndDefaultAttribute != NULL) {
+ assert(nameAndDefaultAttribute->attIndex < elementType->nDefaultAtts);
+ const DEFAULT_ATTRIBUTE *const att
+ = elementType->defaultAtts + nameAndDefaultAttribute->attIndex;
+ isCdata = att->isCdata;
}
}

@@ -7275,11 +7294,24 @@ defineAttribute(ELEMENT_TYPE *type, ATTRIBUTE_ID *attId, XML_Bool isCdata,
if (! isCdata)
attId->maybeTokenized = XML_TRUE;

- NAMED *const nameAddedOrFound
- = lookup(parser, &(type->defaultAttForName), attId->name, sizeof(NAMED));
- if (! nameAddedOrFound)
+ NAME_AND_DEFAULT_ATTRIBUTE *const nameAndDefaultAttribute
+ = (NAME_AND_DEFAULT_ATTRIBUTE *)lookup(
+ parser, &(type->defaultAttForName), attId->name,
+ sizeof(NAME_AND_DEFAULT_ATTRIBUTE));
+ if (! nameAndDefaultAttribute)
return 0;

+ assert(nameAndDefaultAttribute->name == attId->name);
+
+ // NOTE: The XML 1.0r4 spec says:
+ // "When more than one definition is provided for the same attribute of a
+ // given element type, the first declaration is binding and later
+ // declarations are ignored."
+ if (! nameAndDefaultAttribute->initialized) {
+ nameAndDefaultAttribute->attIndex = type->nDefaultAtts;
+ nameAndDefaultAttribute->initialized = true;
+ }
+
type->nDefaultAtts += 1;
return 1;
}
--
2.45.4

162 changes: 162 additions & 0 deletions SPECS/expat/CVE-2026-76641.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
From f3c587cc0228957f897294ad6d6728d14528aa3b Mon Sep 17 00:00:00 2001
From: Zeyou Liu <zeyouliu@tencent.com>
Date: Thu, 20 Aug 2026 20:29:56 +0800
Subject: [PATCH] lib: Fix out-of-bounds read from hash table entries created
by dtdCopy

Commit f8f7c4ff grew the entries of ELEMENT_TYPE member
.defaultAttForName from structure NAMED to the larger structure
NAME_AND_DEFAULT_ATTRIBUTE and adjusted function defineAttribute
accordingly, but function dtdCopy kept creating entries of size
sizeof(NAMED). Because function lookup allocates exactly createSize
bytes, function storeAtts reads member .attIndex past the end of those
entries whenever attributes are parsed by a parser that was created by
XML_ExternalEntityParserCreate.

That out-of-bounds value is then used as an index into member
.defaultAtts, so the effects range from silently not normalizing
whitespace in attributes that are not of type CDATA, to dereferencing a
wild pointer: a release build of master segfaults in function storeAtts
on the document used by the new test. A zero-filled heap happens to
yield index 0, which is why the existing tests did not catch this.

Member .attIndex is now stored the way function defineAttribute stores
it, i.e. keeping the index of the first declaration, so that a copied
DTD resolves attributes exactly like the DTD that it was copied from.

This was found while backporting the fix for CVE-2026-66046 onto Expat
2.6.4 for the OpenCloudOS Stream distribution. Only master is affected,
no released version of Expat contains commit f8f7c4ff.

Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
Upstream-reference: https://github.com/libexpat/libexpat/pull/1331.patch
---
lib/xmlparse.c | 17 +++++++++--
tests/basic_tests.c | 74 +++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 88 insertions(+), 3 deletions(-)

diff --git a/lib/xmlparse.c b/lib/xmlparse.c
index 0865d4b..828fba9 100644
--- a/lib/xmlparse.c
+++ b/lib/xmlparse.c
@@ -7798,11 +7798,22 @@ dtdCopy(XML_Parser oldParser, DTD *newDtd, const DTD *oldDtd,
} else
newE->defaultAtts[i].value = NULL;

- NAMED *const nameAddedOrFound = lookup(parser, &(newE->defaultAttForName),
- attributeName, sizeof(NAMED));
- if (! nameAddedOrFound) {
+ NAME_AND_DEFAULT_ATTRIBUTE *const nameAndDefaultAttribute
+ = (NAME_AND_DEFAULT_ATTRIBUTE *)lookup(
+ parser, &(newE->defaultAttForName), attributeName,
+ sizeof(NAME_AND_DEFAULT_ATTRIBUTE));
+ if (! nameAndDefaultAttribute) {
return 0;
}
+
+ // NOTE: The XML 1.0r4 spec says:
+ // "When more than one definition is provided for the same attribute of a
+ // given element type, the first declaration is binding and later
+ // declarations are ignored."
+ if (! nameAndDefaultAttribute->initialized) {
+ nameAndDefaultAttribute->attIndex = i;
+ nameAndDefaultAttribute->initialized = true;
+ }
}
}

diff --git a/tests/basic_tests.c b/tests/basic_tests.c
index 308adf6..6c2d328 100644
--- a/tests/basic_tests.c
+++ b/tests/basic_tests.c
@@ -2809,6 +2809,79 @@ START_TEST(test_duplicate_id_attribute_multiple_attlistdecl) {
}
END_TEST

+static void XMLCALL
+check_second_attr_normalization(void *userData, const XML_Char *name,
+ const XML_Char **atts) {
+ int *const seen_second = userData;
+ UNUSED_P(name);
+
+ for (size_t i = 0; atts[i] != NULL; i += 2) {
+ const XML_Char *const key = atts[i];
+ const XML_Char *const value = atts[i + 1];
+ if (xcstrcmp(key, XCS("second")) != 0)
+ continue;
+ *seen_second = 1;
+ /* Attribute "second" is not of type CDATA, so leading, trailing and
+ * repeated whitespace is to be normalized away. */
+ if (xcstrcmp(value, XCS("a b")) != 0)
+ fail("Attribute of non-CDATA type was not whitespace-normalized");
+ }
+}
+
+static int XMLCALL
+external_entity_attr_checker(XML_Parser parser, const XML_Char *context,
+ const XML_Char *base, const XML_Char *systemId,
+ const XML_Char *publicId) {
+ const char *const text = "<tag second=' a b '/>";
+ UNUSED_P(base);
+ UNUSED_P(systemId);
+ UNUSED_P(publicId);
+
+ XML_Parser ext_parser = XML_ExternalEntityParserCreate(parser, context, NULL);
+ if (ext_parser == NULL)
+ fail("Could not create external entity parser");
+
+ if (_XML_Parse_SINGLE_BYTES(ext_parser, text, (int)strlen(text), XML_TRUE)
+ != XML_STATUS_OK)
+ xml_failure(ext_parser);
+
+ XML_ParserFree(ext_parser);
+ return XML_STATUS_OK;
+}
+
+START_TEST(test_default_attr_index_after_dtd_copy) {
+ /* Function storeAtts resolves member .attIndex of structure
+ * NAME_AND_DEFAULT_ATTRIBUTE to tell whether an attribute value needs
+ * whitespace normalization, so function dtdCopy needs to carry that index
+ * over to the copy. Attribute "first" is declared before attribute
+ * "second" so that a mixed-up index resolves to the wrong declaration.
+ */
+ const char *text = "<!DOCTYPE doc [\n"
+ " <!ENTITY e SYSTEM 'entity.ent'>\n"
+ " <!ELEMENT doc ANY>\n"
+ " <!ELEMENT tag EMPTY>\n"
+ " <!ATTLIST tag first CDATA #IMPLIED>\n"
+ " <!ATTLIST tag second NMTOKENS #IMPLIED>\n"
+ "]>\n"
+ "<doc>&e;</doc>\n";
+ int seen_second = 0;
+
+ XML_Parser parser = XML_ParserCreate(NULL);
+ assert_true(parser != NULL);
+ XML_SetUserData(parser, &seen_second);
+ XML_SetExternalEntityRefHandler(parser, external_entity_attr_checker);
+ XML_SetStartElementHandler(parser, check_second_attr_normalization);
+
+ if (_XML_Parse_SINGLE_BYTES(parser, text, (int)strlen(text), XML_TRUE)
+ != XML_STATUS_OK)
+ xml_failure(parser);
+ if (! seen_second)
+ fail("Attribute \"second\" has not been reported");
+
+ XML_ParserFree(parser);
+}
+END_TEST
+
/* Test reset works correctly in the middle of processing an internal
* entity. Exercises some obscure code in XML_ParserReset().
*/
@@ -6737,6 +6810,7 @@ make_basic_test_case(Suite *s) {
tcase_add_test(tc_basic,
test_duplicate_cdata_attribute_multiple_attlistdecl_3);
tcase_add_test(tc_basic, test_duplicate_id_attribute_multiple_attlistdecl);
+ tcase_add_test__if_xml_ge(tc_basic, test_default_attr_index_after_dtd_copy);
tcase_add_test__if_xml_ge(tc_basic, test_reset_in_entity);
tcase_add_test(tc_basic, test_resume_invalid_parse);
tcase_add_test(tc_basic, test_resume_resuspended);
--
2.45.4

28 changes: 28 additions & 0 deletions SPECS/expat/CVE-2026-76956.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
From 43364f5316d0b5e18ca098f06d3ea13e37685cf5 Mon Sep 17 00:00:00 2001
From: Sorrachat <32319737+Sorrashut-K@users.noreply.github.com>
Date: Fri, 14 Aug 2026 17:29:50 -0400
Subject: [PATCH] lib: Fix inverted getentropy() return in
writeRandomBytes_getentropy

Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
Upstream-reference: https://github.com/libexpat/libexpat/pull/1326.patch
---
lib/random_getentropy.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/random_getentropy.c b/lib/random_getentropy.c
index d258df6..ad8b198 100644
--- a/lib/random_getentropy.c
+++ b/lib/random_getentropy.c
@@ -54,7 +54,7 @@
bool
writeRandomBytes_getentropy(void *target, size_t count) {
errno = 0;
- const bool success = getentropy(target, count);
+ const bool success = (getentropy(target, count) == 0);
// MSan does not understand `getentropy`, so explain its effects
if (success)
MSAN_UNPOISON(target, count);
--
2.45.4

Loading
Loading